转载请注明作者及出处:
作者:银河架构师
原文链接: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的途径之一。注释有如下含义:
- 方法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"); .... 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相关电子书。