避坑指南(三):Spring Oauth2框架初始化AuthenticationManager剖析
说明
顾名思义,即为“身份认证管理器”, 说白了,就是各种类型登录方式、或者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的途径之一。注释有如下含义:
- 方法authenticationManager()使用此方法来生成默认的AuthenticationManager。如果覆盖,需使用AuthenticationManagerBuilder来自定义AuthenticationManager。
- 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())