Spring Security是一个可以为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组能够在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减小了为企业系统安全控制编写大量重复代码的工做。css
本文是接上一章Spring Security认证过程进一步分析Spring Security
用户名密码登陆受权是如何实现得;java
使用debug方式启动https://github.com/longfeizheng/logback该项目,浏览器输入http://localhost:8080/persons,用户名随意,密码123456便可;git
如图所示,显示了登陆认证过程当中的 filters
相关的调用流程,做者将几个自认为重要的 filters 标注了出来,github
从图中能够看出执行的顺序。来看看几个做者认为比较重要的 Filter 的处理逻辑,UsernamePasswordAuthenticationFilter
,AnonymousAuthenticationFilter
,ExceptionTranslationFilter
,FilterSecurityInterceptor
以及相关的处理流程以下所述;express
整个调用流程是,先调用其父类 AbstractAuthenticationProcessingFilter.doFilter() 方法,而后再执行 UsernamePasswordAuthenticationFilter.attemptAuthentication() 方法进行验证;编程
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; #1.判断当前的filter是否能够处理当前请求,不能够的话则交给下一个filter处理 if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { #2.抽象方法由子类UsernamePasswordAuthenticationFilter实现 authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication return; } #2.认证成功后,处理一些与session相关的方法 sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); #3.认证失败后的的一些操做 unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } #3. 认证成功后的相关回调方法 主要将当前的认证放到SecurityContextHolder中 successfulAuthentication(request, response, chain, authResult); }
整个程序的执行流程以下:浏览器
attemptAuthentication
进行验证,该方法由子类UsernamePasswordAuthenticationFilter
实现protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (logger.isDebugEnabled()) { logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } successHandler.onAuthenticationSuccess(request, response, authResult); }
1. 将当前认证成功的 Authentication 放置到 SecurityContextHolder 中; 2. 将当前认证成功的 Authentication 放置到 SecurityContextHolder 中; 3. 调用其它可扩展的 handlers 继续处理该认证成功之后的回调事件;(实现`AuthenticationSuccessHandler`接口便可)
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { #1.判断请求的方法必须为POST请求 if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } #2.从request中获取username和password String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); #3.构建UsernamePasswordAuthenticationToken(两个参数的构造方法setAuthenticated(false)) UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); #4. 调用 AuthenticationManager 进行验证(子类ProviderManager遍历全部的AuthenticationProvider认证) return this.getAuthenticationManager().authenticate(authRequest); }
POST
Authenticaiton
的实现类UsernamePasswordAuthenticationToken
,(UsernamePasswordAuthenticationToken
调用两个参数的构造方法setAuthenticated(false))AuthenticationManager
的 authenticate
方法进行验证;可参考ProviderManager部分;从上图中过滤器的执行顺序图中能够看出AnonymousAuthenticationFilter
过滤器是在UsernamePasswordAuthenticationFilter
等过滤器以后,若是它前面的过滤器都没有认证成功,Spring Security
则为当前的SecurityContextHolder
中添加一个Authenticaiton
的匿名实现类AnonymousAuthenticationToken
;安全
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { #1.若是前面的过滤器都没认证经过,则SecurityContextHolder中Authentication为空 if (SecurityContextHolder.getContext().getAuthentication() == null) { #2.为当前的SecurityContextHolder中添加一个匿名的AnonymousAuthenticationToken SecurityContextHolder.getContext().setAuthentication( createAuthentication((HttpServletRequest) req)); if (logger.isDebugEnabled()) { logger.debug("Populated SecurityContextHolder with anonymous token: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } } else { if (logger.isDebugEnabled()) { logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } } chain.doFilter(req, res); } #3.建立匿名的AnonymousAuthenticationToken protected Authentication createAuthentication(HttpServletRequest request) { AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key, principal, authorities); auth.setDetails(authenticationDetailsSource.buildDetails(request)); return auth; } /** * Creates a filter with a principal named "anonymousUser" and the single authority * "ROLE_ANONYMOUS". * * @param key the key to identify tokens created by this filter */ ##.建立一个用户名为anonymousUser 受权为ROLE_ANONYMOUS public AnonymousAuthenticationFilter(String key) { this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); }
SecurityContextHolder中Authentication
为否为空;SecurityContextHolder
中添加一个匿名的AnonymousAuthenticationToken
(用户名为 anonymousUser 的AnonymousAuthenticationToken
)ExceptionTranslationFilter
异常处理过滤器,该过滤器用来处理在系统认证受权过程当中抛出的异常(也就是下一个过滤器FilterSecurityInterceptor
),主要是 处理 AuthenticationException
和 AccessDeniedException
。session
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; try { chain.doFilter(request, response); logger.debug("Chain processed normally"); } catch (IOException ex) { throw ex; } catch (Exception ex) { // Try to extract a SpringSecurityException from the stacktrace #.判断是否是AuthenticationException Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); RuntimeException ase = (AuthenticationException) throwableAnalyzer .getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { #. 判断是否是AccessDeniedException ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType( AccessDeniedException.class, causeChain); } if (ase != null) { handleSpringSecurityException(request, response, chain, ase); } else { // Rethrow ServletExceptions and RuntimeExceptions as-is if (ex instanceof ServletException) { throw (ServletException) ex; } else if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } // Wrap other Exceptions. This shouldn't actually happen // as we've already covered all the possibilities for doFilter throw new RuntimeException(ex); } } }
此过滤器为认证受权过滤器链中最后一个过滤器,该过滤器以后就是请求真正的/persons
服务app
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } public void invoke(FilterInvocation fi) throws IOException, ServletException { if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } #1. before invocation重要 InterceptorStatusToken token = super.beforeInvocation(fi); try { #2. 能够理解开始请求真正的 /persons 服务 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } #3. after Invocation super.afterInvocation(token, null); } }
三个部分中,最重要的是 #1,该过程当中会调用 AccessDecisionManager
来验证当前已认证成功的用户是否有权限访问该资源;
protected InterceptorStatusToken beforeInvocation(Object object) { ... Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); ... Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { #1.重点 this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException)); throw accessDeniedException; } ... }
authenticated
就是当前认证的Authentication
,那么object
和attributes
又是什么呢?
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object);
咱们发现object
为当前请求的 url:/persons
, 那么getAttributes
方法就是使用当前的访问资源路径去匹配
咱们本身定义的匹配规则。
protected void configure(HttpSecurity http) throws Exception { http.formLogin()//使用表单登陆,再也不使用默认httpBasic方式 .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//若是请求的URL须要认证则跳转的URL .loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)//处理表单中自定义的登陆URL .and() .authorizeRequests().antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL, SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM, SecurityConstants.DEFAULT_REGISTER_URL, "/**/*.js", "/**/*.css", "/**/*.jpg", "/**/*.png", "/**/*.woff2") .permitAll()//以上的请求都不须要认证 .anyRequest()//剩下的请求 .authenticated()//都须要认证 .and() .csrf().disable()//关闭csrd拦截 ; }
0-7
返回 permitALL
即不须要认证 ,8
对应anyRequest
返回 authenticated
即当前请求须要认证;
能够看到当前的authenticated
为匿名AnonymousAuthentication
用户名为anonymousUser
Spring Security
默认使用AffirmativeBased
实现AccessDecisionManager
的 decide
方法来实现受权
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { int deny = 0; #1.调用AccessDecisionVoter 进行vote(投票) for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, configAttributes); if (logger.isDebugEnabled()) { logger.debug("Voter: " + voter + ", returned: " + result); } switch (result) { #1.1只要有voter投票为ACCESS_GRANTED,则经过 直接返回 case AccessDecisionVoter.ACCESS_GRANTED://1 return; @#1.2只要有voter投票为ACCESS_DENIED,则记录一下 case AccessDecisionVoter.ACCESS_DENIED://-1 deny++; break; default: break; } } if (deny > 0) { #2.若是有两个及以上AccessDecisionVoter(姑且称之为投票者吧)都投ACCESS_DENIED,则直接就不经过了 throw new AccessDeniedException(messages.getMessage( "AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // To get this far, every AccessDecisionVoter abstained checkAllowIfAllAbstainDecisions(); }
deny++
,最后判断if(deny>0
抛出AccessDeniedException
(未受权)public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) { assert authentication != null; assert fi != null; assert attributes != null; WebExpressionConfigAttribute weca = findConfigAttribute(attributes); if (weca == null) { return ACCESS_ABSTAIN; } EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, fi); ctx = weca.postProcess(ctx, fi); return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED : ACCESS_DENIED; }
到此位置authentication
当前用户信息,fl
当前访问的资源路径及attributes
当前资源路径的决策(便是否须要认证)。剩下就是判断当前用户的角色Authentication.authorites
是否权限访问决策访问当前资源fi
。