• 避坑指南(三):Spring Security Oauth2框架如何初始化AuthenticationManager


    转载请注明作者及出处:

    作者:银河架构师

    原文链接:https://www.cnblogs.com/luas/p/12188967.html

    说明

    顾名思义,即为“身份认证管理器”,   说白了,就是各种类型登录方式、或者Authentication(*AuthenticationToken)辅助认证Provider的管理对象。

     比如,如果只是采用表单登录认证,那么完全可以使用默认的AuthenticationManager,认证用户势必要提供UserDetailsService、PasswordEncoder,如此一来,系统会根据这两个对象,非常贴心的自动初始化默认的AuthenticationManager(至于如何初始化默认的AuthenticationManager,下面再说)。

     其中就有一个能认证表单登录方式-UsernamePasswordAuthenticationToken的DaoAuthenticationProvider,DaoAuthenticationProvider中又初始化了UserDetailsService、PasswordEncoder,这样,就可以顺利进行认证(认证过程,详见Spring Security探秘-表单认证过程)。

    Spring Oauth2配置

    @EnableAuthorizationServer

     开启授权服务器关键注解。

    AuthorizationServerSecurityConfiguration

     排序为0,最先配置。

     

    其中,关键逻辑如下:

    一、configure(ClientDetailsServiceConfigurer clientDetails)

    客户端详情相关配置,对应AuthorizationServerConfigurer接口的configure(ClientDetailsServiceConfigurer clients)方法,如下图所示:

     

    声明个性客户端及其相关属性,且必须声明至少一个自定义的客户端信息系统才会启动。需要注意,如果需要启用password授权模式,需要特别为AuthorizationServerEndpointsConfigurer提供AuthenticationManager。

    二、configure(AuthenticationManagerBuilder auth)

     AuthenticationManager辅助构造方法。看设计者的意图,是故意空覆盖父类方法,目的是不想让此配置查找且沿用全局的AuthenticationManager并作为其父AuthenticationManager。此处的AuthenticationManager只需和认证clients的ClientDetailsService一起,为身份认证提供认证服务即可。空覆盖以后,怎么就不能查找且沿用全局的AuthenticationManager并作为其父AuthenticationManager,下面会一一解释。

    先来看如何配置AuthenticationManager,这个问题非常复杂。网上各种例子,基本就是默认配置,或者稍微的自定义配置。但是到真正项目中,根本不够用,可是高级自定义又非常复杂且不好理解。

    AuthenticationManager配置

    先来看WebSecurityConfigurerAdapter的几个方法:

    configure(AuthenticationManagerBuilder auth)

     

    此方法为开发者自定义AuthenticationManager的途径之一。注释有如下含义:

    1. 方法authenticationManager()使用此方法来生成默认的AuthenticationManager。如果覆盖,需使用AuthenticationManagerBuilder来自定义AuthenticationManager。
    2. authenticationManagerBean()方法可用于将最终生成的AuthenticationManager对象暴露为Bean。userDetailsServiceBean()可用于将AuthenticationManagerBuilder中创建的UserDetailsService对象暴露为Bean,UserDetailsService对象也会存储在HttpSecurity对象的全局共享对象中,以便其它SecurityContextConfigurer实现使用,比如RememberMeConfigurer。

    关于此方法的用处,下面会有更详细的说明。

    authenticationManagerBean()

     

     意思很明了,即为开发者如需使用AuthenticationManager,则可以通过覆盖此方法,将configure(AuthenticationManagerBuilder)方法构造的AuthenticationManager暴露为Bean。

    authenticationManager()

     

     此方法逻辑看似简单,短短几行,实则内在略微复杂。首先authenticationManagerInitialized变量的引入是为了不重复进行相关配置,如其它需要沿用此配置的Configure(RememberMeConfigure)。紧接着,调用configure(AuthenticationManagerBuilder)方法。其目的,就是为了获取开发者是否提供了自定义的AuthenticationManager。如已提供,则disableLocalConfigureAuthenticationBldr默认为false,便不会执行调用AuthenticationConfiguration.getAuthenticationManager()获取AuthenticationManager的逻辑。如未提供,则会其基类WebSecurityConfigurerAdapter的默认方法,前面已说明过,此时disableLocalConfigureAuthenticationBldr为true,便会调用本地创建对象localConfigureAuthenticationBldr创建AuthenticationManager,如下图:

     这里非常关键,先说明两点:

    1、localConfigureAuthenticationBldr、authenticationManagerBuilder怎么初始化的?

    WebSecurityConfigurerAdapter.setApplicationContext(ApplicationContext context),如图:

    二者均为WebSecurityConfigurerAdapter子类内部类DefaultPasswordEncoderAuthenticationManagerBuilder对象,目的都是创建AuthenticationManager,所谓的localConfigureAuthenticationBldr不过是开发者通过覆盖configure(AuthenticationManagerBuilder)进行一系列自定义的WebSecurityConfigurerAdapter子类内部类DefaultPasswordEncoderAuthenticationManagerBuilder对象。如图所示:

     

    2、AuthenticationConfiguration.getAuthenticationManager()

    如果开发者没有覆盖configure(AuthenticationManagerBuilder)进行自定义,则会调用其基类WebSecurityConfigurerAdapter的默认方法(前面已说明过),进而调用AuthenticationConfiguration.getAuthenticationManager()方法。如图:

     

     getAuthenticationManager方法调用authenticationManagerBuilder方法创建DefaultPasswordEncoderAuthenticationManagerBuilder对象并初始化了EventPublisher。紧接着的if判断,也只是为了减少初始化的工作,如已初始化过,则直接从authenticationManagerBuilder对象中获取。之后循环GlobalAuthenticationConfigurerAdapter对象对authenticationManagerBuilder进行操作,也无非是属性赋值。

    GlobalAuthenticationConfigurerAdapter对象的定义如图所示:

     

    EnableGlobalAuthenticationAutowiredConfigurer没有什么实质性内容:

     

    InitializeAuthenticationProviderBeanManagerConfigurer,查询上下文中是否存在AuthenticationProvider对象,如存在,就初始化到authenticationManagerBuilder中。

     

    InitializeUserDetailsBeanManagerConfigurer,查找上下问中是否存在passwordEncoder、UserDetailsPasswordService对象,如存在,则分别初始化DaoAuthenticationProvider中,然后再将DaoAuthenticationProvider初始化到authenticationManagerBuilder中。

     

    另说明一下DefaultPasswordEncoderAuthenticationManagerBuilder类,代码如下:

     

     其中引用的DaoAuthenticationConfigurer类,好像这类没啥大用,查看其基类。

     

    DaoAuthenticationConfigurer的基类AbstractDaoAuthenticationConfigurer,一开始就定义并初始化provider成员变量为DaoAuthenticationProvider,以后的所有操作,如设置passwordEncoder、userDetailsService等均是为DaoAuthenticationProvider对象设置。

     

    说了这么多,那么getAuthenticationManager()创建的AuthenticationManager到底作为何用?看下一个方法。

    getHttp()

     

    红色部分即为调用authenticationManager()逻辑之处,旨在为AuthenticationManagerBuilder对象设置父AuthenticationManager,此AuthenticationManagerBuilder对象,为HttpSecurity对象中全局共享。后续在WebSecurityConfigurerAdapter子类中的configure(HttpSecurity http)方法中,还可调用AuthenticationManagerBuilder的方法,对AuthenticationManager属性进行设置,如图所示:

     

     

    三、configure(HttpSecurity http)

    非常重要,主要表现在如下几点:

    1、创建3个需要匹配的Request路径(requestMatchers().antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)),将以此为匹配规则,创建DefaultSecurityFilterChain(重中之重,后续相关文章会进行说明,不在此展开)。代码如下:

    String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
    String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
    String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
    
    ....
        
    http
          .and()
           .requestMatchers()
               .antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)

    "/oauth/token"需要非匿名登录才可以访问,而"/oauth/token_key"、"/oauth/check_token"则根据开发者定义的配置进行配置(默认为不允许访问,denyAll),代码如下:

    AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
    ....
        
    configure(configurer);  
    
    ....
        
    http
           .authorizeRequests()
               .antMatchers(tokenEndpointPath).fullyAuthenticated()
               .antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
               .antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())

    而configure(configurer)结果就是调用AuthorizationServerConfigurerAdapter子类进行配置,如图:

     

    2、如AuthorizationServerEndpointsConfiguration尚未设置UserDetailsService,则从HttpSecurity全局共享对象中取出,并赋值。

    if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
       UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
       endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
    }

    四、configure(AuthorizationServerSecurityConfigurer oauthServer)

    将开发者定义的AuthorizationServerConfigurer实现一一应用到AuthorizationServerSecurityConfigurer,对应AuthorizationServerConfigurer接口的configure(ClientDetailsServiceConfigurer clients)方法,如下图所示:

     

    为认证服务配置安全策略,即意味着配置/oauth/token的安全策略。而/oauth/authorize端点也需要安全,但是此端点只是普通的和用户UI同级别安全配置,无需在此处配置。默认配置遵循OAuth2规范中的建议,已经覆盖了大多数场景,所以无需在此做任何事情,即可启动、运行基本的认证服务。具体的默认配置见方法三的说明。

    AuthorizationServerEndpointsConfiguration

    认证服务端点配置类。主要逻辑如下:

    1、将开发者定义的AuthorizationServerConfigurer实现一一应用到AuthorizationServerSecurityConfigurer,对应AuthorizationServerConfigurer接口的configure(ClientDetailsServiceConfigurer clients)方法(见之前说明),如图:

     

    2、根据相关配置,创建端点/oauth/token、/oauth/token_key、/oauth/check_token

     

    3、创建AuthorizationServerTokenServices

     

    4、TokenKey端点钩子,添加构造方法参数-JwtAccessTokenConverter对象

     

    AuthorizationServerConfiguration

    AuthorizationServerConfigurerAdapter子类,自定义授权配置类

     

    @EnableResourceServer

    开启资源服务器关键注解。

    ResourceServerConfiguration

    排序为3,仅次于AuthorizationServerSecurityConfiguration配置

     

    1、匹配规则定义,不拦截oauth2相关端点,包括默认的、修改默认路径的等。

    2、应用开发者定义的ResourceServerConfigurer子类ResourceServerSecurityConfigurer相关配置,对应ResourceServerConfigurer.configure(ResourceServerSecurityConfigurer resources)

     

    具体到配置中,即为如下配置:

     

    3、资源服务器Spring Security永远不会创建HttpSession,也永远不会使用其获取SecurityContext

    4、添加相关oauth2端点到非oauth2请求匹配器,非oauth2请求才会拦截,oauth2相关端点请求不会拦截,如/oauth/token、/oauth/token_key等。

    5、应用开发者定义的ResourceServerConfigurer子类HttpSecurity相关配置,对应ResourceServerConfigurer.configure(HttpSecurity http)

     

    使用此方法,可以配置安全访问策略。默认所有的oauth2端点,即所有"/oauth/**"请求为受保护资源。

    具体到配置中,即为如下配置:

     

    6、解析ResourceServerTokenServices

    一般来说,会使用默认的ResourceServerTokenServices,如果没有注入,则会从上下文中获取,因为有可能用户自定义。

    自定义ResourceServerConfiguration

    ResourceServerConfigurerAdapter子类,自定义ResourceServer配置

     

    关于resourceId、拦截请求规则等问题,后续文章会逐一说明,敬请期待。

    Spring Security配置

    大部分内容与之前AuthorizationServerSecurityConfiguration的说明一致,如AuthenticationManager创建等。这里只说明一下不同的部分:

    AuthenticationManager创建

    SpringSecurity相关子类配置并不覆盖configure(AuthenticationManagerBuilder auth)方法,如前述,那么localConfigureAuthenticationBldr将会被基类默认方法设置为true,则将会由authenticationConfiguration.getAuthenticationManager()创建AuthenticationManager的父AuthenticationManager,而不是本地创建对象localConfigureAuthenticationBldr。其它逻辑如前所述。

    userDetailsServiceBean()

     

    覆盖此方法可将AuthenticationManagerBuilder创建的UserDetailsService暴露为Bean。需要特别注意,如果需要变更返回的userDetailsService实例,需修改userDetailsService()方法

    userDetailsService()

     

    此方法允许开发者脱离上下文影响修改、访问userDetailsServiceBean()所创建的UserDetailsService,即可以直接创建UserDetailsService,而不必暴露Bean。因为会通过相关逻辑将此对象放入HttpSecurity全局共享对象,如下图所示:

     

    如果有其它用途,还是需要暴露为Bean。

    需要特别注意,如果userDetailsServiceBean()返回的实例发生变化,需覆盖此方法,返回同样的实例

    configure(HttpSecurity http)

    其它说明。

    UserDetailsServiceDelegator

    UserDetailsService委托者类

     

    调用地方如下:

     

    AuthenticationManagerDelegator

    AuthenticationManager委托者类

     

    LazyPasswordEncoder

    懒加载PasswordEncoder,如果用户提供了PasswordEncoder,则使用。如果未提供,则创建委托式多加密方式的PasswordEncoder。

     

    最终配置

    AuthorizationServerConfiguration

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
        @Autowired
        private DataSource dataSource;
        
        @Autowired
        private SysProperties sysProperties;
        
        @Autowired
        private PasswordEncoder passwordEncoder;
        
        @Autowired
        private UserDetailsService userDetailsService;
        
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            security.
                    allowFormAuthenticationForClients()
                    .tokenKeyAccess("permitAll()")
                    .checkTokenAccess("permitAll()")
            ;
        }
        
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(jdbcTokenStore())
                    .authenticationManager(authorizationAuthenticationManager())
                    .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
                    .tokenServices(defaultTokenServices())
            ;
        }
        
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.withClientDetails(jdbcClientDetailsServiceBuilder());
        }
        
        @Bean
        public ClientDetailsService jdbcClientDetailsServiceBuilder() throws Exception {
            return new JdbcClientDetailsServiceBuilder().dataSource(dataSource).passwordEncoder(passwordEncoder).build();
        }
        
        @Bean
        public TokenStore jdbcTokenStore() {
            return new JdbcTokenStore(dataSource);
        }
        
        @Bean
        public DefaultTokenServices defaultTokenServices() throws Exception {
            DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
            defaultTokenServices.setAuthenticationManager(authorizationAuthenticationManager());
            defaultTokenServices.setTokenStore(jdbcTokenStore());
            defaultTokenServices.setClientDetailsService(jdbcClientDetailsServiceBuilder());
            
            SysProperties.AccessToken accessToken = sysProperties.getAccessToken();
            defaultTokenServices.setAccessTokenValiditySeconds(accessToken.getAccessTokenValiditySeconds());
            defaultTokenServices.setRefreshTokenValiditySeconds(accessToken.getRefreshTokenValiditySeconds());
            defaultTokenServices.setSupportRefreshToken(accessToken.isSupportRefreshToken());
            defaultTokenServices.setReuseRefreshToken(accessToken.isReuseRefreshToken());
            return defaultTokenServices;
        }
        
        private PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider() {
            PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider = new PreAuthenticatedAuthenticationProvider();
            preAuthenticatedAuthenticationProvider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper(userDetailsService));
            return preAuthenticatedAuthenticationProvider;
        }
        
        private AuthenticationManager authorizationAuthenticationManager() {
            List<AuthenticationProvider> providers = new ArrayList<>();
            providers.add(daoAuthenticationProvider());
            providers.add(preAuthenticatedAuthenticationProvider());
            return new ProviderManager(providers);
        }
        
        private AuthenticationProvider daoAuthenticationProvider() {
            DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
            daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
            daoAuthenticationProvider.setUserDetailsService(userDetailsService);
            return daoAuthenticationProvider;
        }
    } 

     

    ResourceServerConfiguration

    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId("auth").stateless(true);
        }
        
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .requestMatchers()
                    .antMatchers("/oauth/user")
                    .and()
                    .authorizeRequests()
                    .anyRequest()
                    .authenticated();
        }
    }

    SpringWebSecurityConfiguration

    使用默认AuthenticationManager

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .requestMatchers()
                    .anyRequest()
                    .and()
                    .authorizeRequests()
                    .anyRequest()
                    .authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .csrf().disable();
        }
        
        @Bean(name = "userDetailsService")
        @Override
        public UserDetailsService userDetailsServiceBean() throws Exception {
            return createUserDetailsService();
        }
        
        @Override
        protected UserDetailsService userDetailsService() {
            return createUserDetailsService();
        }
        
        private UserDetailsService createUserDetailsService() {
            return new CustomUserDetailsService();
        }
        
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }

    UserController

    用户信息端点。

    @RestController
    @RequestMapping("/oauth")
    public class UserController {
        
        @RequestMapping("/user")
        public Principal user(Principal user) {
            return user;
        }
    }

    CustomUserDetailsService

    public class CustomUserDetailsService implements UserDetailsService {
        
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            if (StringUtils.isBlank(username)) {
                throw new UsernameNotFoundException("username is null.");
            }
            
            // 自行实现,可根据username查询数据库、缓存等,获取用户信息
            UserInfo userInfo = xxxx;
            
            if (userInfo== null) {
                throw new UsernameNotFoundException("username do not exist.");
            }
            
            User.UserBuilder userBuilder = User.builder()
                    .username(userInfo.getUsername())
                    .password(userInfo.getPassword())
                    .authorities(userInfo.getUserRole())
                    ;
            
            return userBuilder.build();
        }
    }

    尾声

    关于AuthorizationServerSecurityConfiguration,ResourceServerConfiguration、SpringWebSecurityConfiguration三个配置类,均配置了Spring Security,那么他们之间如何协调,如何拦截呢?请期待后续内容,会有专门篇幅来说明这些问题。

    技术资料领取方法:关注公众号,回复微服务,领取微服务相关电子书;回复MK精讲,领取MK精讲系列电子书;回复JAVA 进阶,领取JAVA进阶知识相关电子书;回复JAVA面试,领取JAVA面试相关电子书,回复JAVA WEB领取JAVA WEB相关电子书。

    image.png

  • 相关阅读:
    Jenkins构建项目
    jenkins的理解及安装
    GitLab初识以及代码迁移
    深入浅出Git(偏向理论)
    理解下所谓的ssh隧道
    洛谷-P1908 逆序对
    洛谷-P1010 幂次方
    洛谷-P1226 【模板】快速幂||取余运算
    洛谷-P1074 靶形数独
    洛谷-P1433 吃奶酪
  • 原文地址:https://www.cnblogs.com/luas/p/12188967.html
Copyright © 2020-2023  润新知