解決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整合Security安全框架實(shí)現(xiàn)控制權(quán)限
本文主要介紹了SpringBoot整合Security安全框架實(shí)現(xiàn)控制權(quán)限,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
深入理解SpringMVC的參數(shù)綁定與數(shù)據(jù)響應(yīng)機(jī)制
本文將深入探討SpringMVC的參數(shù)綁定方式,包括基本類型、對(duì)象、集合等類型的綁定方式,以及如何處理參數(shù)校驗(yàn)和異常。同時(shí),本文還將介紹SpringMVC的數(shù)據(jù)響應(yīng)機(jī)制,包括如何返回JSON、XML等格式的數(shù)據(jù),以及如何處理文件上傳和下載。2023-06-06
mybatis多表查詢的實(shí)現(xiàn)(xml方式)
本文主要介紹了mybatis多表查詢的實(shí)現(xiàn)(xml方式),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
關(guān)于Java垃圾回收開(kāi)銷降低的幾條建議
垃圾回收(Garbage Collection)是Java虛擬機(jī)(JVM)垃圾回收器提供的一種用于在空閑時(shí)間不定時(shí)回收無(wú)任何對(duì)象引用的對(duì)象占據(jù)的內(nèi)存空間的一種機(jī)制,下面這篇文章主要介紹了關(guān)于Java垃圾回收開(kāi)銷降低的幾條建議,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-02-02
Java結(jié)構(gòu)型設(shè)計(jì)模式之適配器模式詳解
適配器模式,即將某個(gè)類的接口轉(zhuǎn)換成客戶端期望的另一個(gè)接口的表示,主要目的是實(shí)現(xiàn)兼容性,讓原本因?yàn)榻涌诓黄ヅ?,沒(méi)辦法一起工作的兩個(gè)類,可以協(xié)同工作。本文將通過(guò)示例詳細(xì)介紹適配器模式,需要的可以參考一下2022-09-09
簡(jiǎn)單學(xué)習(xí)Java抽象類要點(diǎn)及實(shí)例
這篇文章主要介紹了Java抽象類要點(diǎn)及實(shí)例,有需要的朋友可以參考一下2014-01-01
Java使用@EnableEurekaServer實(shí)現(xiàn)自動(dòng)裝配詳解
這篇文章主要介紹了Java使用@EnableEurekaServer實(shí)現(xiàn)自動(dòng)裝配過(guò)程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-10-10
仿京東平臺(tái)框架開(kāi)發(fā)開(kāi)放平臺(tái)(包含需求,服務(wù)端代碼,SDK代碼)
現(xiàn)在開(kāi)放平臺(tái)越來(lái)越多了,下面針對(duì)仿京東開(kāi)放平臺(tái)框架,封裝自己的開(kāi)放平臺(tái),分享給大家。先感謝一下京東開(kāi)放平臺(tái)的技術(shù)大佬們,下面從開(kāi)放平臺(tái)需求,服務(wù)端代碼,SDK代碼三大塊進(jìn)行分享2021-06-06

