避坑指南(三):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

posted @ 2020-01-13 19:27  银河架构师  阅读(8458)  评论(0编辑  收藏  举报