添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
避坑指南(三):Spring Oauth2框架初始化AuthenticationManager剖析

避坑指南(三):Spring Oauth2框架初始化AuthenticationManager剖析

2 年前 · 来自专栏 程序员避坑指南

说明

顾名思义,即为“身份认证管理器”, 说白了,就是各种类型登录方式、或者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");
      .and()
       .requestMatchers()
           .antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)

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

AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
configure(configurer);  
       .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 {
                .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 {
                .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())