Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

Spring Security系列之授权过程(七)

0
0

Spring Security系列之授权过程(七)
前言

本文是接上一章 Spring Security系列之认证过程(六) 进一步分析Spring Security用户名密码登录授权是如何实现得;

类图
Spring Security系列之授权过程(七)
调试过程

使用debug方式启动https://github.com/longfeizheng/logback该项目,浏览器输入http://localhost:8080/persons,用户名随意,密码123456即可;

源码分析

如图所示,显示了登录认证过程中的 filters 相关的调用流程,将几个自认为重要的 filters 标注了出来,


Spring Security系列之授权过程(七)
从图中可以看出执行的顺序。来看看几个作者认为比较重要的 Filter 的处理逻辑, UsernamePasswordAuthenticationFilter , AnonymousAuthenticationFilter , ExceptionTranslationFilter , FilterSecurityInterceptor

以及相关的处理流程如下所述;

UsernamePasswordAuthenticationFilter

整个调用流程是,先调用其父类 AbstractAuthenticationProcessingFilter.doFilter() 方法,然后再执行 UsernamePasswordAuthenticationFilter.attemptAuthentication() 方法进行验证;

AbstractAuthenticationProcessingFilter 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); } 复制代码

整个程序的执行流程如下:

判断filter是否可以处理当前的请求,如果不可以则放行交给下一个filter 调用抽象方法attemptAuthentication进行验证,该方法由子类UsernamePasswordAuthenticationFilter实现 认证成功以后,回调一些与 session 相关的方法; 认证成功以后,认证成功后的相关回调方法;认证成功以后,认证成功后的相关回调方法; 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); } 复制代码 将当前认证成功的 Authentication 放置到 SecurityContextHolder 中; 将当前认证成功的 Authentication 放置到 SecurityContextHolder 中; 调用其它可扩展的 handlers 继续处理该认证成功以后的回调事件;(实现AuthenticationSuccessHandler接口即可) UsernamePasswordAuthenticationFilter 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 从request中获取 username 和 password 封装Authenticaiton的实现类UsernamePasswordAuthenticationToken,(UsernamePasswordAuthenticationToken调用两个参数的构造方法setAuthenticated(false)) 调用 AuthenticationManager 的 authenticate 方法进行验证;可参考ProviderManager部分; AnonymousAuthenticationFilter

从上图中过滤器的执行顺序图中可以看出 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

ExceptionTranslationFilter 异常处理过滤器,该过滤器用来处理在系统认证授权过程中抛出的异常(也就是下一个过滤器 FilterSecurityInterceptor ),主要是处理 AuthenticationException 和 AccessDeniedException 。

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); } } } 复制代码 FilterSecurityInterceptor

此过滤器为认证授权过滤器链中最后一个过滤器,该过滤器之后就是请求真正的 /persons 服务

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); } } 复制代码 before invocation重要 请求真正的 /persons 服务 after Invocation

三个部分中,最重要的是 #1,该过程中会调用 AccessDecisionManager 来验证当前已认证成功的用户是否有权限访问该资源;

AccessDecisionManager: beforeInvocation 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又是什么呢?

attributes和object 是什么? Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); 复制代码

调试


Spring Security系列之授权过程(七)
我们发现 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 即当前请求需要认证;


Spring Security系列之授权过程(七)
可以看到当前的 authenticated 为匿名 AnonymousAuthentication 用户名为 anonymousUser AccessDecisionManager 是如何授权的?

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(); } 复制代码 调用AccessDecisionVoter 进行vote(投票) 只要有投通过(ACCESS_GRANTED)票,则直接判为通过。 如果没有投通过则 deny++ ,最后判断if(deny>0 抛出AccessDeniedException(未授权) WebExpressionVoter.vote() 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。


Viewing all articles
Browse latest Browse all 12749

Latest Images

Trending Articles



click here for Latest and Popular articles on Mesothelioma and Asbestos


Latest Images