1. 程式人生 > >Spring Security Web 5.1.2 原始碼解析 -- BasicAuthenticationFilter

Spring Security Web 5.1.2 原始碼解析 -- BasicAuthenticationFilter

概述

處理HTTP請求中的BASIC authorization頭部,把認證結果寫入SecurityContextHolder

當一個HTTP請求中包含一個名字為Authorization的頭部,並且其值格式是Basic xxx時,該Filter會認為這是一個BASIC authorization頭部,其中xxx部分應該是一個base64編碼的{username}:{password}字串。比如使用者名稱/密碼分別為 admin/secret, 則對應的該頭部是 : Basic YWRtaW46c2VjcmV0

該過濾器會從 HTTP BASIC authorization

頭部解析出相應的使用者名稱和密碼然後呼叫AuthenticationManager進行認證,成功的話會把認證了的結果寫入到SecurityContextHolderSecurityContext的屬性authentication上面。同時還會做其他一些處理,比如Remember Me相關處理等等。

如果頭部分析失敗,該過濾器會丟擲異常BadCredentialsException

如果認證失敗,則會清除SecurityContextHolder中的SecurityContext。並且不再繼續filter chain的執行。

原始碼解析

package org.springframework.
security.web.authentication.www; import java.io.IOException; import java.util.Base64; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.
AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.NullRememberMeServices; import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; public class BasicAuthenticationFilter extends OncePerRequestFilter { // ~ Instance fields // ================================================================================================ // 建立Authentication物件時設定details屬性所使用的詳情來源 private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource(); private AuthenticationEntryPoint authenticationEntryPoint; private AuthenticationManager authenticationManager; private RememberMeServices rememberMeServices = new NullRememberMeServices(); private boolean ignoreFailure = false; private String credentialsCharset = "UTF-8"; /** * Creates an instance which will authenticate against the supplied * AuthenticationManager and which will ignore failed authentication attempts, * allowing the request to proceed down the filter chain. * * @param authenticationManager the bean to submit authentication requests to */ public BasicAuthenticationFilter(AuthenticationManager authenticationManager) { Assert.notNull(authenticationManager, "authenticationManager cannot be null"); this.authenticationManager = authenticationManager; this.ignoreFailure = true; } /** * Creates an instance which will authenticate against the supplied * AuthenticationManager and use the supplied AuthenticationEntryPoint * to handle authentication failures. * * @param authenticationManager the bean to submit authentication requests to * @param authenticationEntryPoint will be invoked when authentication fails. * Typically an instance of BasicAuthenticationEntryPoint. */ public BasicAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) { Assert.notNull(authenticationManager, "authenticationManager cannot be null"); Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null"); this.authenticationManager = authenticationManager; this.authenticationEntryPoint = authenticationEntryPoint; } // ~ Methods // ======================================================================================================== @Override public void afterPropertiesSet() { Assert.notNull(this.authenticationManager, "An AuthenticationManager is required"); if (!isIgnoreFailure()) { Assert.notNull(this.authenticationEntryPoint, "An AuthenticationEntryPoint is required"); } } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { final boolean debug = this.logger.isDebugEnabled(); // 獲取請求頭部 Authorization String header = request.getHeader("Authorization"); if (header == null || !header.toLowerCase().startsWith("basic ")) { // 如果頭部 Authorization 未設定或者不是 basic 認證頭部,則當前 // 請求不是該過濾器關注的物件,直接放行,繼續filter chain 的執行 chain.doFilter(request, response); return; } // 這是一個 http basic authentication 請求的情況,也就是說,已經檢測到 // 請求頭部 Authorization 的值符合格式(大小寫不敏感) : "basic xxxxxx" try { // 分析頭部 Authorization 獲取使用者名稱和密碼 String[] tokens = extractAndDecodeHeader(header, request); assert tokens.length == 2; // 現在 tokens[0] 表示使用者名稱, tokens[1] 表示密碼 String username = tokens[0]; if (debug) { this.logger .debug("Basic Authentication Authorization header found for user '" + username + "'"); } // 檢測針對所請求的使用者名稱 username 是否需要認證 if (authenticationIsRequired(username)) { // 如果需要認證,使用所獲取到的使用者名稱/密碼構建一個 UsernamePasswordAuthenticationToken, // 然後執行認證流程 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, tokens[1]); authRequest.setDetails( this.authenticationDetailsSource.buildDetails(request)); // 執行認證 Authentication authResult = this.authenticationManager .authenticate(authRequest); if (debug) { this.logger.debug("Authentication success: " + authResult); } // 認證成功,將完全認證的Authentication authRequest設定到 SecurityContextHolder // 中的 SecurityContext 上。 SecurityContextHolder.getContext().setAuthentication(authResult); // 認證成功時 RememberMe 相關處理 this.rememberMeServices.loginSuccess(request, response, authResult); // 認證成功時的其他處理: 其實這個個空方法,什麼都沒做 onSuccessfulAuthentication(request, response, authResult); } } catch (AuthenticationException failed) { // 認證失敗,清除 SecurityContextHolder 的安全上下文 SecurityContextHolder.clearContext(); if (debug) { this.logger.debug("Authentication request for failed: " + failed); } // 認證失敗 RememberMe 相關處理 this.rememberMeServices.loginFail(request, response); // 認證失敗時的其他處理: 其實這個個空方法,什麼都沒做 onUnsuccessfulAuthentication(request, response, failed); if (this.ignoreFailure) { chain.doFilter(request, response); } else { this.authenticationEntryPoint.commence(request, response, failed); } return; } // 如果當前請求並非含有 http basic authentication 頭部的請求,則直接放行,繼續filter chain的執行 chain.doFilter(request, response); } /** * Decodes the header into a username and password. * 從指定的 http basic authentication 請求頭部解碼出一個使用者名稱和密碼 * * @throws BadCredentialsException if the Basic header is not present or is not valid * Base64 */ private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException { // 擷取頭部前6個字元之後的內容部分,使用字符集編碼方式UTF-8 // 舉例: 整個頭部值為 "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", // 這裡則是獲取"QWxhZGRpbjpvcGVuIHNlc2FtZQ=="部分 byte[] base64Token = header.substring(6).getBytes("UTF-8"); // 使用 Base64 字元編碼方式解碼 base64Token byte[] decoded; try { decoded = Base64.getDecoder().decode(base64Token); } catch (IllegalArgumentException e) { throw new BadCredentialsException( "Failed to decode basic authentication token"); } // 使用指定的字符集credentialsCharset重新構建"{使用者名稱}:{密碼}"字串 // credentialsCharset 預設也是 UTF-8 String token = new String(decoded, getCredentialsCharset(request)); // 提取使用者名稱,密碼並返回之 int delim = token.indexOf(":"); if (delim == -1) { throw new BadCredentialsException("Invalid basic authentication token"); } return new String[] { token.substring(0, delim), token.substring(delim + 1) }; } private boolean authenticationIsRequired(String username) { // Only reauthenticate if username doesn't match SecurityContextHolder and user // isn't authenticated // (see SEC-53) Authentication existingAuth = SecurityContextHolder.getContext() .getAuthentication(); // 檢測 SecurityContextHolder 中 SecurityContext 的 Authentication, // 如果它為 null 或者尚未認證,則認為需要認證 if (existingAuth == null || !existingAuth.isAuthenticated()) { return true; } // Limit username comparison to providers which use usernames (ie // UsernamePasswordAuthenticationToken) // (see SEC-348) // 如果 SecurityContextHolder 中 SecurityContext 的 Authentication 是 // 已經認證狀態,但是其中的使用者名稱和這裡的 username 不相同,也認為需要認證 if (existingAuth instanceof UsernamePasswordAuthenticationToken && !existingAuth.getName().equals(username)) { return true; } // Handle unusual condition where an AnonymousAuthenticationToken is already // present // This shouldn't happen very often, as BasicProcessingFitler is meant to be // earlier in the filter // chain than AnonymousAuthenticationFilter. Nevertheless, presence of both an // AnonymousAuthenticationToken // together with a BASIC authentication request header should indicate // reauthentication using the // BASIC protocol is desirable. This behaviour is also consistent with that // provided by form and digest, // both of which force re-authentication if the respective header is detected (and // in doing so replace // any existing AnonymousAuthenticationToken). See SEC-610. // 如果 SecurityContextHolder 中 SecurityContext 的 Authentication 是匿名認證, // 則認為需要認證 if (existingAuth instanceof AnonymousAuthenticationToken) { return true; } // 如果 SecurityContextHolder 中 SecurityContext 的 Authentication 是已認證狀態, // 並且是針對當前username的,則認為不需要認證 return false; } protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException { } protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException { } protected AuthenticationEntryPoint getAuthenticationEntryPoint() { return this.authenticationEntryPoint; } protected AuthenticationManager getAuthenticationManager() { return this.authenticationManager; } protected boolean isIgnoreFailure() { return this.ignoreFailure; } public void setAuthenticationDetailsSource( AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required"); this.authenticationDetailsSource = authenticationDetailsSource; } public void setRememberMeServices(RememberMeServices rememberMeServices) { Assert.notNull(rememberMeServices, "rememberMeServices cannot be null"); this.rememberMeServices = rememberMeServices; } public void setCredentialsCharset(String credentialsCharset) { Assert.hasText(credentialsCharset, "credentialsCharset cannot be null or empty"); this.credentialsCharset = credentialsCharset; } protected String getCredentialsCharset(HttpServletRequest httpRequest) { return this.credentialsCharset; } }

參考文章