Spring Security 是一个多模块的项目,以前梳理了一下 Spring Security 认证流程,如今才发现,梳理的那部份内容更多的只是 Spring Security Core 这个核心模块中的内容。html
平常使用时,还会更多的涉及 Spring Security Web 和 Spring Security OAuth2 中的东西,这篇博客的主要内容即是梳理一下这三者之间的关系,了解一下各自发挥的做用。java
Spring Security Core 在整个 Spring Security 框架中扮演着重要的角色,提供了有关于认证和权限控制相关的抽象。git
然而,在使用的过程当中,咱们接触的更多的多是和认证相关的抽象,好比:github
AuthenticationManager
提供了进行用户认证方法的抽象,容许经过 ProviderManager
和 AuthenticationProvider
来组装和实现本身的认证方法UserDetails
和 UserDetailsService
提供了用户详细信息和获取用户详细信息方式的抽象Authentication
提供了用户认证信息和认证结果的抽象SecurityContext
和 SecurityContextHolder
提供了保存认证结果的方式这些东西其实就是将传统的认证流程中的关键组成单独抽象了出来,结合传统的认证流程能够很容易的理解这些组件之间的关系,也能够看这张来自 Spring Security(一) —— Architecture Overview | 芋道源码 —— 纯源码解析博客 的一张图片:web
<img src="https://user-gold-cdn.xitu.io/2019/11/2/16e2c58cc7573921?w=995&h=562&f=png&s=45816">spring
而权限控制部分的抽象,主要就是 AccessDecisionManager
和 AccessDecisionVoter
了,这两个东西我目前尚未手动操做过,只能说,Spring Security Web 提供的服务太贴心, 权限控制部分的实现并不须要我操太多心。安全
关于 Spring Security Core 模块更多的内容能够参考:服务器
若是说 Spring Security Core 只是提供了认证和权限控制相关的抽象的话,Spring Security Web 便为咱们提供了这些抽象的具体实现与应用。session
Spring Security Web 经过 过滤器链 来实现了和 Web 安全相关的一系列功能,而用户的认证和权限控制只是其中的一部分,在这部分的实现中,过滤器充当 Spring Security Core 调用者的身份,通常流程为:架构
Authentication
传递给 AuthenticationManager
进行认证,而后将认证结果放到 SecurityContext
中供后续过滤器使用AccessDecisionManager
判断是否具有相应的权限在这里,Spring Security Core 只是 Spring Security Web 利用的一部分功能,更为重要的是,整个过滤器链。
以前原本只是想了解一下过滤器链的调用过程,可是看着看着,就跑到源码去了。反应过来的时候才发现,已经搞了这么多了停下来的话有点吃亏,就干脆把过滤器链的构建逻辑理了一下。
<details><summary><i></i></summary>
在梳理完构建器链的构建和调用逻辑后感受,过滤器链的构建逻辑貌似没有好多用,还不如直接看过滤器链的调用逻辑……
</details>
这部分逻辑的梳理过程有些复杂,反正我调试的时候断点就在 build()
方法附近反复横跳,这里为了简单,就直接放结果了<sup><a id="fnr.1" class="footref" href="#fn.1">1</a></sup>:
<img src="https://user-gold-cdn.xitu.io/2019/11/2/16e2c58c73711fa5?w=1125&h=777&f=png&s=86478">
时序图画的不是很标准,大体意思一下就能够了哈( ̄▽ ̄),解析以下:
WebSecurity
和 HttpSecurity
完成的WebSecurity
根据上下文中的 WebSecurityConfigurer
构建出 HttpSecurity
对象,而后经过 HttpSecurity
构建出 SecurityFilterChain
后,将 SecurityFilterChain
放到 FilterChainProxy
中。 其中,WebSecurityConfigurer 的经常使用实现为 WebMvcConfigurerAdapter
, 而 SecurityFilterChain
的经常使用实现为 DefaultSecurityFilterChain
HttpSecurity
根据直接添加的 Filter
和经过 AbstractHttpConfigurer
实现类构建的 Filter
生成过滤器链这部分逻辑中,关键的对象分别是 WebSecurity
和它依赖的配置类 WebSecurityConfigurer
, HttpSecurity
和它依赖的配置类 AbstractHttpConfigurer
.
在实际的使用中,咱们一般会继承 WebMvcConfigurerAdapter
这个 WebSecurityConfigurer
的实现类,而后在重写它的 configure(HttpSecurity)
方法:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { // @formatter:off http .authorizeRequests() .antMatchers("/oauth/**") .authenticated() .and() .requestMatchers() .antMatchers("/oauth/**","/login/**","/logout/**") .and() .csrf() .disable() .formLogin() .permitAll(); // @formatter:on } }
在上面这个类中,咱们继承了 WebSecurityConfigurerAdapter
这个类,当咱们将自定义的类放到 Spring 上下文中后,就能够被 WebSecurity 拿到用于构建 HttpSecurity, 而重写的 configure(HttpSecurity)
则会在 HttpSecurity 构建过滤器以前调用,完成过滤器链的配置。
其中,诸如 csrf()
之类的方法都会返回一个 AbstractHttpConfigurer
实现,容许咱们对特定的过滤器进行配置。
到了最后,HttpSecurity 就能够根据相应的配置完成过滤器链的构建,而后再由 WebSecurity 将它们放到 FilterChainProxy
实例中返回。
过滤器链的调用的话,主要涉及两个对象:FilterChainProxy 和 DefaultSecurityFilterChain,关键其实仍是在 FilterChainProxy 上。
然而,这两个对象的源码都挺简单的,这里就不贴了,有兴趣的能够去看一下,这里简单说一下结果:
这里的关键点其实就是,存在多条过滤器链,每条过滤器链匹配必定的请求。以前看文档的时候不仔细,没有意识到这一点,饶了很多弯路 QAQ
附图:
<img src="https://user-gold-cdn.xitu.io/2019/11/2/16e2c58cc7967f3e?w=1190&h=839&f=png&s=65564">
Spring Security Web 过滤器的使用主要就是自定义过滤器链,默认的过滤器链会添加一些 Spring Security Web 自带的一些过滤器,使用时,须要考虑是否去掉默认的一些过滤器器(或者不使用默认配置), 并将自定义的过滤器添加到过滤器链中的一个合适的位置上。
这里会简要介绍部份内置过滤器的做用和过滤器的顺序,首先是内置的几个过滤器:
SecurityContextPersistenceFilter
能够从 Session 中取出已认证用户的信息AnonymousAuthenticationFilter
在发现 SecurityContextHolder
中尚未认证信息时,会生成一个匿名认证信息放到 SecurityContextHolder
ExceptionTranslationFilter
能够处理 FilterSecurityInterceptor
中抛出的异常,进行重定向、输出错误信息等FilterSecurityInterceptor
对认证信息的权限进行判断,权限不足时抛出异常在自定义过滤器时(一般是认证过滤器),咱们须要考虑自定义过滤器的位置,好比,咱们不该该把自定义的认证过滤器放在 AnonymousAuthenticationFilter
的后面,官方文档对过滤器的顺序给出了解释: 在去除一些过滤器后,大体顺序就为:
<img src="https://user-gold-cdn.xitu.io/2019/11/3/16e2f52abed85b6f?w=247&h=269&f=png&s=17760">
其中,AuthenticationProcessingFilter 是指认证过滤器实现,好比经常使用的 UsernamePasswordAuthenticationFilter
这个过滤器。
完整的顺序能够参考:
在这个顺序中,因为 SecurityContextPersistenceFilter
可能从 Session 中取出已认证用户的信息,所以,自定义过滤器时应该考虑 SecurityContextHolder 是否是已经存在用户认证信息。 或者在登陆/注册相关 URL 的过滤器链中设置认证用户帐户密码的过滤器,在其余过滤器链中设置认证 token 的过滤器。
Spring Security OAuth2 创建在 Spring Security Core 和 Spring Security Web 的基础上,提供了对 OAuth2 受权框架的支持。
其中,最为复杂的部分是在 受权服务器 上,相对的,资源服务器基本上就是重用 Spring Security Web 提供的过滤器链,经过过滤器 OAuth2AuthenticationProcessingFilter
和请求携带的 Token
获取认证信息, 所以,这里的重心会放在受权服务器上。
对于传统的认证方式来讲,简单认证用户的信息基本上就足够了,可是对于 OAuth2 来讲是不够的,对于 OAuth2 受权服务器来讲,除了须要完成用户的认证之外,还需完成客户端的认证,还须要效验客户端请求的 Scope, 所以,单凭过滤器链是不足以完成二者的认证的,由于 SecurityContextHolder 只能持有一个认证结果。
因而,Spring Security OAuth2 采用的认证策略即是:在过滤器链中完成客户端或用户的认证,而后再在端点的内部逻辑中完成剩余信息的效验。而这个认证策略,在不一样模式中也是不同的。
这里主要会对 受权码模式 和 密码模式 中的认证策略进行介绍,由于这两个模式中使用到的端点 AuthorizationEndpoint
和 TokenEndpoint
已经涵盖了两条主要的过滤器链。
首先是受权码模式,对于受权码模式来讲,请求流程一般是先到 /oauth/authorize
获取受权码,而后再到 /oauth/token
获取 Token,对于 /oauth/authorize
这个端点的过滤器链来讲, 认证的是用户的信息,认证经过后进入端点内部,会对客户端请求 Scope
和用户的 Approval
进行效验,效验经过会生成受权码返回给客户端。
其实这里也就能够明白为何 /oauth/authorize
这个端点须要对用户进行认证了,由于,这里须要获取的是 用户 的受权。
而后客户端拿着受权码去 /oauth/token
这个端点获取 Token 时,该端点的过滤器链会对客户端进行认证,认证经过后进入端点内部,这时端点内部会对客户端请求的 Scope 进行效验, 效验经过后就会经过 TokenGranter
生成 Token 返回给客户端。
也就是说,对于受权码模式来讲:
/oauth/authorize
完成用户的认证、客户端请求的 Scope 的效验、用户的受权检查/oauth/token
完成客户端的认证,客户端请求的 Scope 的效验、客户端受权码的检查这其实就能够看作时对受权码模式的代码解释,由于,在受权码模式中,去获取 Token 的每每不是用户操做的客户端,所以,须要认证客户端是不是受信任的。
相关逻辑对应的源码,去掉了一部分效验代码:
@RequestMapping(value = "/oauth/authorize") public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters, SessionStatus sessionStatus, Principal principal) { AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters); try { // 未经过认证的请求会抛异常 if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) { throw new InsufficientAuthenticationException("User must be authenticated with Spring Security before authorization can be completed."); } ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId()); // 效验 Scope oauth2RequestValidator.validateScope(authorizationRequest, client); // 效验用户的受权 authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest, (Authentication) principal); boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal); authorizationRequest.setApproved(approved); // Validation is all done, so we can check for auto approval... if (authorizationRequest.isApproved()) { if (responseTypes.contains("token")) { return getImplicitGrantResponse(authorizationRequest); } if (responseTypes.contains("code")) { return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal)); } } return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal); } catch (RuntimeException e) { sessionStatus.setComplete(); throw e; } } @RequestMapping(value = "/oauth/token", method=RequestMethod.POST) public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { // 能够看到,经过效验的是客户端 String clientId = getClientId(principal); ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId); TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient); // 效验请求的 Scope if (authenticatedClient != null) { oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient); } if (isAuthCodeRequest(parameters)) { // The scope was requested or determined during the authorization step if (!tokenRequest.getScope().isEmpty()) { tokenRequest.setScope(Collections.<String> emptySet()); } } // 调用 TokenGranter 进行受权 OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest); if (token == null) { throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType()); } return getResponse(token); }
受权码模式流程图:
<img src="https://user-gold-cdn.xitu.io/2019/11/2/16e2c58c6bb340ca?w=710&h=406&f=png&s=30973">
密码模式,或者说简化模式,只有一个端点即 /oauth/token
这个端点,也就是说,这个端点要同时完成用户和客户端的认证。
可是,这个端点不可能同时拥有两个过滤器链,而为了支持受权码模式,这个端点的过滤器链的职责已经肯定了,就是完成客户端的认证。所以,用户的认证就只能在端点内部逻辑完成。
当 TokenEndpoint
发现受权模式为 密码模式 时,会将 ResourceOwnerPasswordTokenGranter
放入 TokenGranter
, 而 ResourceOwnerPasswordTokenGranter
进行受权时会调用 AuthenticationManager
来完成对用户的认证,认证成功才会经过 TokenService
生成 Token 返回。
// AuthorizationServerEndpointsConfigurer.getDefaultTokenGranters private List<TokenGranter> getDefaultTokenGranters() { List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>(); tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails, requestFactory)); tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory)); tokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory)); tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory)); if (authenticationManager != null) { tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetails, requestFactory)); } return tokenGranters; }
密码模式流程图:
<img src="https://user-gold-cdn.xitu.io/2019/11/2/16e2c58cc7efb8fe?w=700&h=504&f=png&s=35288">
经过对 受权码模式 和 密码模式 的了解咱们知道了客户端的认证是在过滤器链中完成的,这个认证能够经过 BasicAuthenticationFilter
完成,但更通用的大概是 ClientCredentialsTokenEndpointFilter
这个过滤器。
其内部的认证流程实际上是很简单的,最为重要的一点是,它用的仍是 Spring Security Core 那一套!
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { String clientId = request.getParameter("client_id"); String clientSecret = request.getParameter("client_secret"); // If the request is already authenticated we can assume that this filter is not needed Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { return authentication; } UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId, clientSecret); // 经过 AuthenticationManager 完成认证 return this.getAuthenticationManager().authenticate(authRequest); }
咱们知道,Spring Security OAuth2 提供了 ClientDetails 和 ClientDetailsService 这两种抽象,它们和 UserDetails 和 UserDetailsService 是不兼容的,这时,能够选择本身实现一个 AuthenticationProvider 使用 ClientDetails 和 ClientDetailsService, 但也能够将 ClientDetails 和 ClientDetailsService 转换为 UserDetails 和 UserDetailsService,Spring Security OAuth2 经过 ClientDetailsUserDetailsService 来完成这一转换:
public class ClientDetailsUserDetailsService implements UserDetailsService { private final ClientDetailsService clientDetailsService; public ClientDetailsUserDetailsService(ClientDetailsService clientDetailsService) { this.clientDetailsService = clientDetailsService; } public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { ClientDetails clientDetails; try { clientDetails = clientDetailsService.loadClientByClientId(username); } catch (NoSuchClientException e) { throw new UsernameNotFoundException(e.getMessage(), e); } String clientSecret = clientDetails.getClientSecret(); if (clientSecret== null || clientSecret.trim().length()==0) { clientSecret = emptyPassword; } return new User(username, clientSecret, clientDetails.getAuthorities()); } }
Spring Security OAuth2 中受权码的生成时经过 TokenGranter 来完成的,进行受权码的生成时,会遍历拥有的各个 TokenGranter 实现,直到成功生成 Token 或者全部 TokenGranter 实现都不能生成 Token。
生成 Token 也是一个能够抽象出来的环节,所以,Spring Security OAuth2 经过 TokenService 和 TokenStore 来生成、获取和保存 Token。
public abstract class AbstractTokenGranter implements TokenGranter { private final AuthorizationServerTokenServices tokenServices; private final ClientDetailsService clientDetailsService; private final OAuth2RequestFactory requestFactory; private final String grantType; protected AbstractTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) { this.clientDetailsService = clientDetailsService; this.grantType = grantType; this.tokenServices = tokenServices; this.requestFactory = requestFactory; } public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { // 每一个 TokenGranter 对应一种受权类型 if (!this.grantType.equals(grantType)) { return null; } String clientId = tokenRequest.getClientId(); ClientDetails client = clientDetailsService.loadClientByClientId(clientId); validateGrantType(grantType, client); // 获取受权码 return getAccessToken(client, tokenRequest); } protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) { return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest)); } } // 默认的 TokenServices 的部分代码 public class DefaultTokenServices { @Transactional public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { // 首先从 TokenStore 中获取 Token OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication); OAuth2RefreshToken refreshToken = null; if (existingAccessToken != null) { if (existingAccessToken.isExpired()) { if (existingAccessToken.getRefreshToken() != null) { refreshToken = existingAccessToken.getRefreshToken(); tokenStore.removeRefreshToken(refreshToken); } tokenStore.removeAccessToken(existingAccessToken); } else { // Re-store the access token in case the authentication has changed tokenStore.storeAccessToken(existingAccessToken, authentication); return existingAccessToken; } } if (refreshToken == null) { refreshToken = createRefreshToken(authentication); } OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); // 保存 accessToken tokenStore.storeAccessToken(accessToken, authentication); refreshToken = accessToken.getRefreshToken(); if (refreshToken != null) { tokenStore.storeRefreshToken(refreshToken, authentication); } return accessToken; } // 从 TokenStore 中获取 Token public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { return tokenStore.getAccessToken(authentication); } }
简单来讲就是:
资源服务器相较于受权服务器来讲就要简单多了,和传统的流程差很少,经过过滤器 OAuth2AuthenticationProcessingFilter
和 OAuth2AuthenticationManager
验证 Token 并获取认证信息:
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) req; final HttpServletResponse response = (HttpServletResponse) res; // 从请求头中提取 Token Authentication authentication = tokenExtractor.extract(request); Authentication authResult = authenticationManager.authenticate(authentication); SecurityContextHolder.getContext().setAuthentication(authResult); chain.doFilter(request, response); } } public class OAuth2AuthenticationManager implements AuthenticationManager, InitializingBean { public Authentication authenticate(Authentication authentication) throws AuthenticationException { String token = (String) authentication.getPrincipal(); // 经过 TokenService 获取认证信息 OAuth2Authentication auth = tokenServices.loadAuthentication(token); if (auth == null) { throw new InvalidTokenException("Invalid token: " + token); } checkClientDetails(auth); if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) { OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails(); // Guard against a cached copy of the same details if (!details.equals(auth.getDetails())) { // Preserve the authentication details from the one loaded by token services details.setDecodedDetails(auth.getDetails()); } } auth.setDetails(authentication.getDetails()); auth.setAuthenticated(true); return auth; } }
不少地方均可以看到 JWT 在 OAuth2 中的使用,Spring Security JWT 在 Spring Security OAuth2 中便扮演了 TokenService 和 TokenStore 的角色,用于生成和效验 Token。
可是,我仍是很想吐槽一下 JWT 这个东西。当初刚看到的时候感受颇有趣,使用 JWT 能够直接在 Token 中携带一些信息,同时服务端还不用存储 Token 的信息。
然而,在实际的一些使用中,可能会碰见须要做废还有效的 JWT Token 的需求,这对于 JWT 来讲是没法实现的。为了实现这一需求,就只能在服务端存储一些信息。
可是,既然都要在服务端存储信息了,那干吗还用 JWT 呢?只要须要在服务端存储信息,那么,用不用 JWT 都没多大区别了啊……
Spring Security 真的是一个很复杂的框架,目前设计的还只是在 Servlet 程序中的应用,然鹅我目前忽然对 Spring WebFlux 产生了一点兴趣, 不知道 Spring Security 在 Spring WebFlux 中是啥样的……
另外,我想说的是,Spring Security 的官方教程真的很棒,将大致的架构都解释清楚了,惋惜吃了英语的亏 T<sub>T</sub>
Spring Security 总体相关的资料:
Spring Security Web 相关的资料:
Spring Security OAuth2 相关的资料:
<sup><a id="fn.1" href="#fnr.1">1</a></sup> 对详细过程有兴趣的,能够看个人笔记 Spring Security Web 过滤器链的构建