解決Spring Security中AuthenticationEntryPoint不生效相關(guān)問(wèn)題
之前由于項(xiàng)目需要比較詳細(xì)地學(xué)習(xí)了Spring Security的相關(guān)知識(shí),并打算實(shí)現(xiàn)一個(gè)較為通用的權(quán)限管理模塊。由于項(xiàng)目是前后端分離的,所以當(dāng)認(rèn)證或授權(quán)失敗后不應(yīng)該使用formLogin()的重定向,而是返回一個(gè)json形式的對(duì)象來(lái)提示沒(méi)有授權(quán)或認(rèn)證。 ??
這時(shí),我們可以使用AuthenticationEntryPoint對(duì)認(rèn)證失敗異常提供處理入口,而通過(guò)AccessDeniedHandler對(duì)用戶無(wú)授權(quán)異常提供處理入口
在這里我的代碼如下
/** * 對(duì)已認(rèn)證用戶無(wú)權(quán)限的處理 */ @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"); // 提示無(wú)權(quán)限 httpServletResponse.getWriter().print(JSONObject.toJSONString(new BaseResult<String>(NO_PERMISSION, false, null))); } }
/** * 對(duì)匿名用戶無(wú)權(quán)限的處理 */ @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"); // 認(rèn)證失敗 httpServletResponse.getWriter().print(JSONObject.toJSONString(new BaseResult<String>(e.getMessage(), false, null))); } }
在這樣的設(shè)置下,如果認(rèn)證失敗的話會(huì)提示具體認(rèn)證失敗的原因;而用戶進(jìn)行無(wú)權(quán)限訪問(wèn)的時(shí)候會(huì)返回?zé)o權(quán)限的提示。 ??
用不存在的用戶名密碼登錄后會(huì)出現(xiàn)以下返回?cái)?shù)據(jù)
與我所設(shè)置的認(rèn)證異常返回值不一致。
在繼續(xù)講解前,我先簡(jiǎn)單說(shuō)下我當(dāng)前的Spring Security配置,我是將不同的登錄方式整合在一起,并模仿Spring Security中的UsernamePasswordAuthenticationFilter實(shí)現(xiàn)了不同登錄方式的過(guò)濾器。 ??
設(shè)想通過(guò)郵件、短信、驗(yàn)證碼和微信等登錄方式登錄(這里暫時(shí)只實(shí)現(xiàn)了驗(yàn)證碼登錄的模板)。
??
以下是配置信息
/** * @Author chongyahhh * 驗(yàn)證碼登錄配置 */ @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) // 用戶名密碼驗(yàn)證碼登錄配置導(dǎo)入 .and() .exceptionHandling() .authenticationEntryPoint(jsonAuthenticationEntryPoint) // 注冊(cè)自定義認(rèn)證異常入口 .accessDeniedHandler(jsonAccessDeniedHandler) // 注冊(cè)自定義授權(quán)異常入口 .and() .anonymous() .and() .formLogin() .and() .csrf().disable(); // 關(guān)閉 csrf,防止首次的 POST 請(qǐng)求被攔截 } @Bean("customSecurityExpressionHandler") public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler(){ DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler(); handler.setPermissionEvaluator(new CustomPermissionEvaluator()); return handler; } }
以下是實(shí)現(xiàn)的驗(yàn)證碼登錄過(guò)濾器
模仿UsernamePasswordAuthenticationFilter繼承AbstractAuthenticationProcessingFilter實(shí)現(xiàn)。
/** * @Author chongyahhh * 驗(yàn)證碼登錄過(guò)濾器 */ 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")); // 繼續(xù)執(zhí)行攔截器鏈,執(zhí)行被攔截的 url 對(duì)應(yīng)的接口 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("驗(yàn)證中..."); 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; } // 不分區(qū)大小寫 return StringUtils.equalsIgnoreCase((String)validateCode, verificationCode); } }
其它的設(shè)置與本問(wèn)題無(wú)關(guān),就先不放出來(lái)了。 ??
首先我們要知道,AuthenticationEntryPoint和AccessDeniedHandler是過(guò)濾器ExceptionTranslationFilter中的一部分,當(dāng)ExceptionTranslationFilter捕獲到之后過(guò)濾器的執(zhí)行異常后,會(huì)調(diào)用AuthenticationEntryPoint和AccessDeniedHandler中的對(duì)應(yīng)方法來(lái)進(jìn)行異常處理。
以下是對(duì)應(yīng)的源碼
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { // 認(rèn)證異常 ... sendStartAuthentication(request, response, chain, (AuthenticationException) exception); // 在這里調(diào)用 AuthenticationEntryPoint 的 commence 方法 } else if (exception instanceof AccessDeniedException) { // 無(wú)權(quán)限 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"))); // 在這里調(diào)用 AuthenticationEntryPoint 的 commence 方法 } else { ... accessDeniedHandler.handle(request, response, (AccessDeniedException) exception); // 在這里調(diào)用 AccessDeniedHandler 的 handle 方法 } } }
在ExceptionTranslationFilter抓到之后的攔截器拋出的異常后就進(jìn)行以上判斷:
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); } // 這里進(jìn)入上面的方法?。?! 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); } } }
?
綜上,我們考慮攔截器鏈沒(méi)有到達(dá)ExceptionTranslationFilter便拋出異常并結(jié)束處理;或是經(jīng)過(guò)了ExceptionTranslationFilter,但之后的異常沒(méi)被其抓取便處理結(jié)束。 ??
我們首先看一下當(dāng)前Security的攔截器鏈
??
很明顯可以發(fā)現(xiàn),我們自定義的過(guò)濾器在ExceptionTranslationFilter之前,所以在拋出異常后,應(yīng)該會(huì)處理后直接終止執(zhí)行鏈。 ??
由于篇幅原因,這里不具體給出debug過(guò)程,直接給出結(jié)果。 ??
我們查看VerificationAuthenticationFilter繼承的AbstractAuthenticationProcessingFilter中的doFilter方法:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 在此處進(jìn)行 url 匹配,如果不是該攔截器攔截的 url,就直接執(zhí)行下一個(gè)攔截器的攔截 if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { // 調(diào)用我們實(shí)現(xiàn)的 VerificationAuthenticationFilter 中的 attemptAuthentication 方法,進(jìn)行登錄邏輯驗(yàn)證 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) { // // 注意這里,如果登錄失敗,我們拋出的異常會(huì)在這里被抓取,然后通過(guò) unsuccessfulAuthentication 進(jìn)行處理 // 翻閱 unsuccessfulAuthentication 中的代碼我們可以發(fā)現(xiàn),如果我們沒(méi)有設(shè)置認(rèn)證失敗后的重定向url,就會(huì)封裝一個(gè)401的響應(yīng),也就是我們上面出現(xiàn)的情況 // unsuccessfulAuthentication(request, response, failed); // 執(zhí)行完成后直接中斷攔截器鏈的執(zhí)行 return; } // 如果登錄成功就繼續(xù)執(zhí)行,我們?cè)O(shè)置的 continueChainBeforeSuccessfulAuthentication 為 true if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authResult); }
通過(guò)這段代碼的分析,原因就一目了然了,如果我們繼承AbstractAuthenticationProcessingFilter來(lái)實(shí)現(xiàn)我們的登錄驗(yàn)證邏輯,無(wú)論該過(guò)濾器在ExceptionTranslationFilter的前面或后面,都無(wú)法順利觸發(fā)ExceptionTranslationFilter中的異常處理邏輯,因?yàn)锳bstractAuthenticationProcessingFilter會(huì)對(duì)認(rèn)證異常進(jìn)行自我消化并中斷攔截器鏈的進(jìn)行,所以我們只能通過(guò)其他的Filter來(lái)封裝我們的登錄邏輯攔截器,如:GenericFilterBean。 ??
為了保證攔截器鏈能順利到達(dá)ExceptionTranslationFilter
我們需要滿足兩個(gè)條件: ????
1、自定義的認(rèn)證過(guò)濾器不能通過(guò)繼承AbstractAuthenticationProcessingFilter實(shí)現(xiàn); ????
2、自定義的認(rèn)證過(guò)濾器應(yīng)在ExceptionTranslationFilter后面:
??
此外,我們也可以通過(guò)實(shí)現(xiàn)AuthenticationFailureHandler的方式來(lái)處理認(rèn)證異常。
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")); // 繼續(xù)執(zhí)行攔截器鏈,執(zhí)行被攔截的 url 對(duì)應(yīng)的接口 super.setContinueChainBeforeSuccessfulAuthentication(true); // 設(shè)置認(rèn)證失敗處理入口 setAuthenticationFailureHandler(new JsonAuthenticationFailureHandler()); } ... }
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot?RESTful?應(yīng)用中的異常處理梳理小結(jié)
這篇文章主要介紹了SpringBoot?RESTful?應(yīng)用中的異常處理梳理小結(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05實(shí)現(xiàn)java文章點(diǎn)擊量記錄實(shí)例
這篇文章主要為大家介紹了實(shí)現(xiàn)java文章點(diǎn)擊量記錄實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10java 引用類型的數(shù)據(jù)傳遞的是內(nèi)存地址實(shí)例
這篇文章主要介紹了java 引用類型的數(shù)據(jù)傳遞的是內(nèi)存地址實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10MyBatis中select語(yǔ)句中使用String[]數(shù)組作為參數(shù)的操作方法
在 MyBatis 中,如何在 mapper.xml 配置文件中 select 語(yǔ)句中使用 String[] 數(shù)組作為參數(shù)呢,并且使用IN關(guān)鍵字來(lái)匹配數(shù)據(jù)庫(kù)中的記錄,這篇文章主要介紹了MyBatis中select語(yǔ)句中使用String[]數(shù)組作為參數(shù),需要的朋友可以參考下2023-12-12Springboot+QueryDsl實(shí)現(xiàn)融合數(shù)據(jù)查詢
這篇文章主要將介紹的是 Springboot 使用 QueryDsl 實(shí)現(xiàn)融合數(shù)據(jù)查詢,文中有詳細(xì)的代碼講解,對(duì) SpringBoot?Querydsl?查詢操作感興趣的朋友一起看看吧2023-08-08解決springmvc關(guān)于前臺(tái)日期作為實(shí)體類對(duì)象參數(shù)類型轉(zhuǎn)換錯(cuò)誤的問(wèn)題
下面小編就為大家?guī)?lái)一篇解決springmvc關(guān)于前臺(tái)日期作為實(shí)體類對(duì)象參數(shù)類型轉(zhuǎn)換錯(cuò)誤的問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06Java調(diào)用打印機(jī)的2種方式舉例(無(wú)驅(qū)/有驅(qū))
我們平時(shí)使用某些軟件或者在超市購(gòu)物的時(shí)候都會(huì)發(fā)現(xiàn)可以使用打印機(jī)進(jìn)行打印,這篇文章主要給大家介紹了關(guān)于Java調(diào)用打印機(jī)的2種方式,分別是無(wú)驅(qū)/有驅(qū)的相關(guān)資料,需要的朋友可以參考下2023-11-11