前言
本文是接上一章Spring Security源碼分析一:Spring Security認(rèn)證過程進(jìn)一步分析Spring Security
用戶名密碼登錄授權(quán)是如何實(shí)現(xiàn)得;
類圖
調(diào)試過程
使用debug方式啟動(dòng)https://github.com/longfeizheng/logback該項(xiàng)目,瀏覽器輸入http://localhost:8080/persons,用戶名隨意习绢,密碼123456即可鞭铆;
源碼分析
如圖所示岭洲,顯示了登錄認(rèn)證過程中的 filters
相關(guān)的調(diào)用流程,作者將幾個(gè)自認(rèn)為重要的 filters 標(biāo)注了出來取刃,
從圖中可以看出執(zhí)行的順序吧彪。來看看幾個(gè)作者認(rèn)為比較重要的 Filter 的處理邏輯待侵,UsernamePasswordAuthenticationFilter
,AnonymousAuthenticationFilter
姨裸,ExceptionTranslationFilter
诫给,FilterSecurityInterceptor
以及相關(guān)的處理流程如下所述;
UsernamePasswordAuthenticationFilter
整個(gè)調(diào)用流程是啦扬,先調(diào)用其父類 AbstractAuthenticationProcessingFilter.doFilter() 方法凫碌,然后再執(zhí)行 UsernamePasswordAuthenticationFilter.attemptAuthentication() 方法進(jìn)行驗(yàn)證扑毡;
AbstractAuthenticationProcessingFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
#1.判斷當(dāng)前的filter是否可以處理當(dāng)前請求,不可以的話則交給下一個(gè)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實(shí)現(xiàn)
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
#2.認(rèn)證成功后盛险,處理一些與session相關(guān)的方法
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
#3.認(rèn)證失敗后的的一些操作
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
#3. 認(rèn)證成功后的相關(guān)回調(diào)方法 主要將當(dāng)前的認(rèn)證放到SecurityContextHolder中
successfulAuthentication(request, response, chain, authResult);
}
整個(gè)程序的執(zhí)行流程如下:
- 判斷filter是否可以處理當(dāng)前的請求瞄摊,如果不可以則放行交給下一個(gè)filter
- 調(diào)用抽象方法
attemptAuthentication
進(jìn)行驗(yàn)證勋又,該方法由子類UsernamePasswordAuthenticationFilter
實(shí)現(xiàn) - 認(rèn)證成功以后,回調(diào)一些與 session 相關(guān)的方法换帜;
- 認(rèn)證成功以后楔壤,認(rèn)證成功后的相關(guān)回調(diào)方法;認(rèn)證成功以后惯驼,認(rèn)證成功后的相關(guān)回調(diào)方法蹲嚣;
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. 將當(dāng)前認(rèn)證成功的 Authentication 放置到 SecurityContextHolder 中;
2. 將當(dāng)前認(rèn)證成功的 Authentication 放置到 SecurityContextHolder 中祟牲;
3. 調(diào)用其它可擴(kuò)展的 handlers 繼續(xù)處理該認(rèn)證成功以后的回調(diào)事件隙畜;(實(shí)現(xiàn)`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.構(gòu)建UsernamePasswordAuthenticationToken(兩個(gè)參數(shù)的構(gòu)造方法setAuthenticated(false))
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
#4. 調(diào)用 AuthenticationManager 進(jìn)行驗(yàn)證(子類ProviderManager遍歷所有的AuthenticationProvider認(rèn)證)
return this.getAuthenticationManager().authenticate(authRequest);
}
- 認(rèn)證請求的方法必須為
POST
- 從request中獲取 username 和 password
- 封裝
Authenticaiton
的實(shí)現(xiàn)類UsernamePasswordAuthenticationToken
,(UsernamePasswordAuthenticationToken
調(diào)用兩個(gè)參數(shù)的構(gòu)造方法setAuthenticated(false)) - 調(diào)用
AuthenticationManager
的authenticate
方法進(jìn)行驗(yàn)證说贝;可參考ProviderManager部分议惰;
AnonymousAuthenticationFilter
從上圖中過濾器的執(zhí)行順序圖中可以看出AnonymousAuthenticationFilter
過濾器是在UsernamePasswordAuthenticationFilter
等過濾器之后,如果它前面的過濾器都沒有認(rèn)證成功乡恕,Spring Security
則為當(dāng)前的SecurityContextHolder
中添加一個(gè)Authenticaiton
的匿名實(shí)現(xiàn)類AnonymousAuthenticationToken
;
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
#1.如果前面的過濾器都沒認(rèn)證通過言询,則SecurityContextHolder中Authentication為空
if (SecurityContextHolder.getContext().getAuthentication() == null) {
#2.為當(dāng)前的SecurityContextHolder中添加一個(gè)匿名的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.創(chuàng)建匿名的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
*/
##.創(chuàng)建一個(gè)用戶名為anonymousUser 授權(quán)為ROLE_ANONYMOUS
public AnonymousAuthenticationFilter(String key) {
this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
}
- 判斷
SecurityContextHolder中Authentication
為否為空; - 如果空則為當(dāng)前的
SecurityContextHolder
中添加一個(gè)匿名的AnonymousAuthenticationToken
(用戶名為 anonymousUser 的AnonymousAuthenticationToken
)
ExceptionTranslationFilter
ExceptionTranslationFilter
異常處理過濾器,該過濾器用來處理在系統(tǒng)認(rèn)證授權(quán)過程中拋出的異常(也就是下一個(gè)過濾器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
此過濾器為認(rèn)證授權(quán)過濾器鏈中最后一個(gè)過濾器运杭,該過濾器之后就是請求真正的/persons
服務(wù)
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 服務(wù)
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
#3. after Invocation
super.afterInvocation(token, null);
}
}
- before invocation重要
- 請求真正的 /persons 服務(wù)
- after Invocation
三個(gè)部分中,最重要的是 #1蛋哭,該過程中會(huì)調(diào)用 AccessDecisionManager
來驗(yàn)證當(dāng)前已認(rèn)證成功的用戶是否有權(quán)限訪問該資源县习;
before invocation: AccessDecisionManager
protected InterceptorStatusToken beforeInvocation(Object object) {
...
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
...
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
#1.重點(diǎn)
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException));
throw accessDeniedException;
}
...
}
authenticated
就是當(dāng)前認(rèn)證的Authentication
,那么object
和attributes
又是什么呢谆趾?
attributes和object 是什么躁愿?
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
我們發(fā)現(xiàn)object
為當(dāng)前請求的 url:/persons
, 那么getAttributes
方法就是使用當(dāng)前的訪問資源路徑去匹配
我們自己定義的匹配規(guī)則。
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//使用表單登錄沪蓬,不再使用默認(rèn)httpBasic方式
.loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//如果請求的URL需要認(rèn)證則跳轉(zhuǎn)的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()//以上的請求都不需要認(rèn)證
.anyRequest()//剩下的請求
.authenticated()//都需要認(rèn)證
.and()
.csrf().disable()//關(guān)閉csrd攔截
;
}
0-7
返回 permitALL
即不需要認(rèn)證 ,8
對應(yīng)anyRequest
返回 authenticated
即當(dāng)前請求需要認(rèn)證;
可以看到當(dāng)前的authenticated
為匿名AnonymousAuthentication
用戶名為anonymousUser
AccessDecisionManager 是如何授權(quán)的彤钟?
Spring Security
默認(rèn)使用AffirmativeBased
實(shí)現(xiàn)AccessDecisionManager
的 decide
方法來實(shí)現(xiàn)授權(quán)
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
#1.調(diào)用AccessDecisionVoter 進(jìn)行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.如果有兩個(gè)及以上AccessDecisionVoter(姑且稱之為投票者吧)都投ACCESS_DENIED逸雹,則直接就不通過了
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
- 調(diào)用AccessDecisionVoter 進(jìn)行vote(投票)
- 只要有投通過(ACCESS_GRANTED)票,則直接判為通過云挟。
- 如果沒有投通過則
deny++
,最后判斷if(deny>0
拋出AccessDeniedException
(未授權(quán))
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
當(dāng)前用戶信息梆砸,fl
當(dāng)前訪問的資源路徑及attributes
當(dāng)前資源路徑的決策(即是否需要認(rèn)證)。剩下就是判斷當(dāng)前用戶的角色Authentication.authorites
是否權(quán)限訪問決策訪問當(dāng)前資源fi
园欣。