解決Spring Security中AuthenticationEntryPoint不生效相關問題
之前由於項目需要比較詳細地學習瞭Spring Security的相關知識,並打算實現一個較為通用的權限管理模塊。由於項目是前後端分離的,所以當認證或授權失敗後不應該使用formLogin()的重定向,而是返回一個json形式的對象來提示沒有授權或認證。
這時,我們可以使用AuthenticationEntryPoint對認證失敗異常提供處理入口,而通過AccessDeniedHandler對用戶無授權異常提供處理入口
在這裡我的代碼如下
/** * 對已認證用戶無權限的處理 */ @Component public class JsonAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json;charset=utf-8"); // 提示無權限 httpServletResponse.getWriter().print(JSONObject.toJSONString(new BaseResult<String>(NO_PERMISSION, false, null))); } }
/** * 對匿名用戶無權限的處理 */ @Component public class JsonAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json;charset=utf-8"); // 認證失敗 httpServletResponse.getWriter().print(JSONObject.toJSONString(new BaseResult<String>(e.getMessage(), false, null))); } }
在這樣的設置下,如果認證失敗的話會提示具體認證失敗的原因;而用戶進行無權限訪問的時候會返回無權限的提示。
用不存在的用戶名密碼登錄後會出現以下返回數據
與我所設置的認證異常返回值不一致。
在繼續講解前,我先簡單說下我當前的Spring Security配置,我是將不同的登錄方式整合在一起,並模仿Spring Security中的UsernamePasswordAuthenticationFilter實現瞭不同登錄方式的過濾器。
設想通過郵件、短信、驗證碼和微信等登錄方式登錄(這裡暫時隻實現瞭驗證碼登錄的模板)。
以下是配置信息
/** * @Author chongyahhh * 驗證碼登錄配置 */ @Component @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class VerificationLoginConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private final VerificationAuthenticationProvider verificationAuthenticationProvider; @Qualifier("tokenAuthenticationDetailsSource") private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource; @Override public void configure(HttpSecurity http) throws Exception { VerificationAuthenticationFilter verificationAuthenticationFilter = new VerificationAuthenticationFilter(); verificationAuthenticationFilter.setAuthenticationManager(http.getSharedObject((AuthenticationManager.class))); http .authenticationProvider(verificationAuthenticationProvider) .addFilterAfter(verificationAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // 將VerificationAuthenticationFilter加到UsernamePasswordAuthenticationFilter後面 } }
/** * @Author chongyahhh * Spring Security 配置 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class SecurityConfig extends WebSecurityConfigurerAdapter { private final AuthenticationEntryPoint jsonAuthenticationEntryPoint; private final AccessDeniedHandler jsonAccessDeniedHandler; private final VerificationLoginConfig verificationLoginConfig; @Override protected void configure(HttpSecurity http) throws Exception { http .apply(verificationLoginConfig) // 用戶名密碼驗證碼登錄配置導入 .and() .exceptionHandling() .authenticationEntryPoint(jsonAuthenticationEntryPoint) // 註冊自定義認證異常入口 .accessDeniedHandler(jsonAccessDeniedHandler) // 註冊自定義授權異常入口 .and() .anonymous() .and() .formLogin() .and() .csrf().disable(); // 關閉 csrf,防止首次的 POST 請求被攔截 } @Bean("customSecurityExpressionHandler") public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler(){ DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler(); handler.setPermissionEvaluator(new CustomPermissionEvaluator()); return handler; } }
以下是實現的驗證碼登錄過濾器
模仿UsernamePasswordAuthenticationFilter繼承AbstractAuthenticationProcessingFilter實現。
/** * @Author chongyahhh * 驗證碼登錄過濾器 */ public class VerificationAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private static final String USERNAME = "username"; private static final String PASSWORD = "password"; private static final String VERIFICATION_CODE = "verificationCode"; private boolean postOnly = true; public VerificationAuthenticationFilter() { super(new AntPathRequestMatcher(SECURITY_VERIFICATION_CODE_LOGIN, "POST")); // 繼續執行攔截器鏈,執行被攔截的 url 對應的接口 super.setContinueChainBeforeSuccessfulAuthentication(true); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String verificationCode = this.obtainVerificationCode(request); System.out.println("驗證中..."); String username = this.obtainUsername(request); String password = this.obtainPassword(request); username = (username == null) ? "" : username; password = (password == null) ? "" : password; username = username.trim(); VerificationAuthenticationToken authRequest = new VerificationAuthenticationToken(username, password); //this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } private String obtainPassword(HttpServletRequest request) { return request.getParameter(PASSWORD); } private String obtainUsername(HttpServletRequest request) { return request.getParameter(USERNAME); } private String obtainVerificationCode(HttpServletRequest request) { return request.getParameter(VERIFICATION_CODE); } private void setDetails(HttpServletRequest request, VerificationAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } private boolean validate(String verificationCode) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpSession session = request.getSession(); Object validateCode = session.getAttribute(VERIFICATION_CODE); if(validateCode == null) { return false; } // 不分區大小寫 return StringUtils.equalsIgnoreCase((String)validateCode, verificationCode); } }
其它的設置與本問題無關,就先不放出來瞭。
首先我們要知道,AuthenticationEntryPoint和AccessDeniedHandler是過濾器ExceptionTranslationFilter中的一部分,當ExceptionTranslationFilter捕獲到之後過濾器的執行異常後,會調用AuthenticationEntryPoint和AccessDeniedHandler中的對應方法來進行異常處理。
以下是對應的源碼
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { // 認證異常 ... sendStartAuthentication(request, response, chain, (AuthenticationException) exception); // 在這裡調用 AuthenticationEntryPoint 的 commence 方法 } else if (exception instanceof AccessDeniedException) { // 無權限 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) { ... sendStartAuthentication( request, response, chain, new InsufficientAuthenticationException( messages.getMessage( "ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource"))); // 在這裡調用 AuthenticationEntryPoint 的 commence 方法 } else { ... accessDeniedHandler.handle(request, response, (AccessDeniedException) exception); // 在這裡調用 AccessDeniedHandler 的 handle 方法 } } }
在ExceptionTranslationFilter抓到之後的攔截器拋出的異常後就進行以上判斷:
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 Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); RuntimeException ase = (AuthenticationException) throwableAnalyzer .getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType( AccessDeniedException.class, causeChain); } if (ase != null) { if (response.isCommitted()) { throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex); } // 這裡進入上面的方法!!! 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); } } }
綜上,我們考慮攔截器鏈沒有到達ExceptionTranslationFilter便拋出異常並結束處理;或是經過瞭ExceptionTranslationFilter,但之後的異常沒被其抓取便處理結束。
我們首先看一下當前Security的攔截器鏈
很明顯可以發現,我們自定義的過濾器在ExceptionTranslationFilter之前,所以在拋出異常後,應該會處理後直接終止執行鏈。
由於篇幅原因,這裡不具體給出debug過程,直接給出結果。
我們查看VerificationAuthenticationFilter繼承的AbstractAuthenticationProcessingFilter中的doFilter方法:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 在此處進行 url 匹配,如果不是該攔截器攔截的 url,就直接執行下一個攔截器的攔截 if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { // 調用我們實現的 VerificationAuthenticationFilter 中的 attemptAuthentication 方法,進行登錄邏輯驗證 authResult = attemptAuthentication(request, response); if (authResult == null) { return; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // // 註意這裡,如果登錄失敗,我們拋出的異常會在這裡被抓取,然後通過 unsuccessfulAuthentication 進行處理 // 翻閱 unsuccessfulAuthentication 中的代碼我們可以發現,如果我們沒有設置認證失敗後的重定向url,就會封裝一個401的響應,也就是我們上面出現的情況 // unsuccessfulAuthentication(request, response, failed); // 執行完成後直接中斷攔截器鏈的執行 return; } // 如果登錄成功就繼續執行,我們設置的 continueChainBeforeSuccessfulAuthentication 為 true if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authResult); }
通過這段代碼的分析,原因就一目瞭然瞭,如果我們繼承AbstractAuthenticationProcessingFilter來實現我們的登錄驗證邏輯,無論該過濾器在ExceptionTranslationFilter的前面或後面,都無法順利觸發ExceptionTranslationFilter中的異常處理邏輯,因為AbstractAuthenticationProcessingFilter會對認證異常進行自我消化並中斷攔截器鏈的進行,所以我們隻能通過其他的Filter來封裝我們的登錄邏輯攔截器,如:GenericFilterBean。
為瞭保證攔截器鏈能順利到達ExceptionTranslationFilter
我們需要滿足兩個條件:
1、自定義的認證過濾器不能通過繼承AbstractAuthenticationProcessingFilter實現;
2、自定義的認證過濾器應在ExceptionTranslationFilter後面:
此外,我們也可以通過實現AuthenticationFailureHandler的方式來處理認證異常。
public class JsonAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.setCharacterEncoding("utf-8"); response.setContentType("application/json;charset=utf-8"); response.getWriter().print(JSONObject.toJSONString(new BaseResult<String>(exception.getMessage(), false, null))); } }
public class VerificationAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private static final String USERNAME = "username"; private static final String PASSWORD = "password"; private static final String VERIFICATION_CODE = "verificationCode"; private boolean postOnly = true; public VerificationAuthenticationFilter() { super(new AntPathRequestMatcher(SECURITY_VERIFICATION_CODE_LOGIN, "POST")); // 繼續執行攔截器鏈,執行被攔截的 url 對應的接口 super.setContinueChainBeforeSuccessfulAuthentication(true); // 設置認證失敗處理入口 setAuthenticationFailureHandler(new JsonAuthenticationFailureHandler()); } ... }
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- Spring security如何重寫Filter實現json登錄
- Spring Security登陸流程講解
- Spring Security 核心過濾器鏈講解
- Spring Security實現自動登陸功能示例
- Spring Security源碼解析之權限訪問控制是如何做到的