由前兩篇博客大概瞭解了SS的啓動初始化和表單登錄的過程java
http.authorizeRequests() // .antMatchers("/") // .permitAll() // 請求路徑"/"容許訪問 // .anyRequest() // .authenticated() // 其它請求都不須要校驗才能訪問 .antMatchers("/home") .hasRole("LOGOPR") .and() .formLogin() .loginPage("/login") // 定義登陸的頁面"/login",容許訪問 .permitAll() .failureUrl("/login?#error=1111") // .failureHandler(failHander) .and() .logout() // 默認的"/logout", 容許訪問 .permitAll(); }
這裏面就包含了請求及對應的權限,好比/home須要LOGOPR角色,等等web
那麼在訪問的時候,SS就能根據訪問路徑的權限與訪問用戶的權限進行比對,符合則經過,不符合就拋出異常重定向到指定界面spring
看看訪問受權的時序圖:express
代碼的調用比較複雜,上圖簡單化了,主要是一個受權的過程,下面就根據源碼來瞧瞧吧!app
以上面的代碼爲例,訪問/home須要LOGOPR權限,登錄用戶默認擁有ROLE_USER權限ide
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } //invoke 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 && observeOncePerRequest) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }
看到代碼中調用父類AbstractSecurityInterceptor的beforeInvocation(),調用前,返回一個InterceptorStatusToken,後續還會調用afterInvocation,那麼繼續跟進beforeInvocation:post
protected InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); final boolean debug = logger.isDebugEnabled(); if (!getSecureObjectClass().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException( "Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + getSecureObjectClass()); } Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); if (attributes == null || attributes.isEmpty()) { if (rejectPublicInvocations) { throw new IllegalArgumentException( "Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. " + "This indicates a configuration error because the " + "rejectPublicInvocations property is set to 'true'"); } if (debug) { logger.debug("Public object - authentication not attempted"); } publishEvent(new PublicInvocationEvent(object)); return null; // no further work post-invocation } if (debug) { logger.debug("Secure object: " + object + "; Attributes: " + attributes); } if (SecurityContextHolder.getContext().getAuthentication() == null) { credentialsNotFound(messages.getMessage( "AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes); } Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } if (debug) { logger.debug("Authorization successful"); } if (publishAuthorizationSuccess) { publishEvent(new AuthorizedEvent(object, attributes, authenticated)); } // Attempt to run as a different user Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { logger.debug("RunAsManager did not change Authentication object"); } // no further work post-invocation return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); // need to revert to token.Authenticated post-invocation return new InterceptorStatusToken(origCtx, true, attributes, object); } }
先調用DefaultFilterInvocationSecurityMetadataSource的getAttributes方法,返回一個Collection<ConfigAttribute>,是一個ConfigAttribute集合,代碼比較簡單:ui
public Collection<ConfigAttribute> getAttributes(Object object) { final HttpServletRequest request = ((FilterInvocation) object).getRequest(); for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap .entrySet()) { if (entry.getKey().matches(request)) { return entry.getValue(); } } return null; }
注意requestMap,以下:this
{
ExactUrl [processUrl='/login?#error=1111']=[permitAll],
ExactUrl [processUrl='/login']=[permitAll],
ExactUrl [processUrl='/login']=[permitAll],
ExactUrl [processUrl='/login?logout']=[permitAll],
Ant [pattern='/home']=[hasRole('ROLE_LOGOPR'),
Ant [pattern='/logout', POST]=[permitAll],]
}lua
能夠看到這個requestMap保存的就是在自定義HttpSecurity的配置的路徑-權限信息,遍歷此Map,和請求的request進行匹配,請求的路徑是/home,匹配成功,以下:
返回的value:
能夠看到,/home匹配的權限信息就是上面配置的ROLE_LOGOPR
繼續跟進,直到:
Authentication authenticated = authenticateIfRequired();
從方法命名(是否必須受權)能夠看出,這裏就是返回受權用戶的方法,從登錄流程就直到,登錄成功的結果就是產生一個Authentication對象的過程,返回的對象以下:
org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b43690ca:
Principal: org.springframework.security.core.userdetails.User@f02988d6:
Username: username;
Password: [PROTECTED];
Enabled: true;
AccountNonExpired: true;
credentialsNonExpired: true;
AccountNonLocked: true;
Granted Authorities: ROLE_USER;
Credentials: [PROTECTED];
Authenticated: true;
Details: org.springframework.security.web.authentication.WebAuthenticationDetails@21a2c:
RemoteIpAddress: 0:0:0:0:0:0:0:1;
SessionId: A08566CA24D4246E04EFC436D3738400;
Granted Authorities: ROLE_USER
這個時候,訪問請求的路徑及權限,登錄用戶的信息都已經獲取了,下面就是決定是否能訪問了!!!
執行:this.accessDecisionManager.decide(authenticated, object, attributes);
調用AffirmativeBased的decide方法:
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { int deny = 0; for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, configAttributes); if (logger.isDebugEnabled()) { logger.debug("Voter: " + voter + ", returned: " + result); } switch (result) { case AccessDecisionVoter.ACCESS_GRANTED: return; case AccessDecisionVoter.ACCESS_DENIED: deny++; break; default: break; } } if (deny > 0) { throw new AccessDeniedException(messages.getMessage( "AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // To get this far, every AccessDecisionVoter abstained checkAllowIfAllAbstainDecisions(); }
經過遍歷一個投票器集合來決定int deny的值,由其判斷受權的結果!
查看投票器集合,僅有一個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; }
由attributes生成WebExpressionConfigAttribute對象weca,由authentication生成EvaluationContext對象ctx,關鍵調用:ctx = weca.postProcess(ctx, fi);
返回更新後的ctx:
後續主要調用spring的方法,後面再跟進
執行WebExpressionVoter完畢,返回int結果爲-1,那麼deny++,deny=1,代碼拋出異常:
if (deny > 0) { throw new AccessDeniedException(messages.getMessage( "AbstractAccessDecisionManager.accessDenied", "Access is denied")); }
異常捕獲,直到doFilter結束。。。
先暫停!