新聞中心
環(huán)境:Springboot2.4.12 + Spring Security 5.4.9

創(chuàng)新互聯(lián)2013年至今,先為大足等服務(wù)建站,大足等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為大足企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問(wèn)題。
本篇主要內(nèi)容:自定義異常處理
上一篇:《??Spring Security權(quán)限控制系列(二)??》
注意:記得不要忘記關(guān)閉CSRF功能,由于之前的案例演示開啟了CSRF,忘記關(guān)閉,導(dǎo)致在本篇案例中在登錄時(shí)總是403狀態(tài)碼,點(diǎn)登錄后通過(guò)調(diào)試發(fā)現(xiàn)請(qǐng)求的url是總是/error(我自定義登錄頁(yè)面并沒(méi)有添加_csrf隱藏域字段)。
默認(rèn)異常原理
基于前面兩篇的內(nèi)容我們發(fā)現(xiàn)只要沒(méi)有無(wú)權(quán)限訪問(wèn)接口,就會(huì)報(bào)錯(cuò)誤,錯(cuò)誤信息如下:
登錄成功后五權(quán)限訪問(wèn)接口時(shí)默認(rèn)的返回錯(cuò)誤信息
錯(cuò)誤的用戶名或密碼時(shí)
接下來(lái)我們看看系統(tǒng)默認(rèn)是如何提供該錯(cuò)誤頁(yè)面信息的。
錯(cuò)誤的用戶名密碼
當(dāng)?shù)卿洉r(shí)填寫的錯(cuò)誤用戶名或密碼時(shí),再次返回了登錄頁(yè)面,并且攜帶了錯(cuò)誤信息。接下來(lái)通過(guò)源碼查看這部分路徑。
當(dāng)前配置:
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(NoOpPasswordEncoder.getInstance())
.withUser("guest").password("123456").roles("ADMIN")
.and()
.withUser("test").password("666666").roles("USERS") ;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() ;
http.authorizeRequests().antMatchers("/resources/**", "/cache/**").permitAll() ;
http.authorizeRequests().antMatchers("/demos/**").hasRole("USERS") ;
http.authorizeRequests().antMatchers("/api/**").hasRole("ADMIN") ;
http.formLogin().loginPage("/custom/login") ;
}
}上面我們自定義了登錄頁(yè)面/custom/login,所以我們的過(guò)濾器鏈中有個(gè)核心的過(guò)濾器UsernamePasswordAuthenticationFilter 該過(guò)濾器專門用來(lái)處理POST提交的登錄URI,我們這里自定義了所以該過(guò)濾器攔截的是/custom/login,該過(guò)濾器在判斷當(dāng)前請(qǐng)求的時(shí)候會(huì)先判斷是不是POST方式提交的,然后判斷URI,所以我們?cè)跒g覽器直接訪問(wèn)該uri的時(shí)候是不會(huì)發(fā)生任何認(rèn)證邏輯處理的。
登錄認(rèn)證的流程:
- UsernamePasswordAuthenticationFilter#attemptAuthentication。
- ProviderManager#authenticate。
- AuthenticationProvider#authenticate。
在第三步中首先判斷的是用戶名是否存在,如果不存在則會(huì)拋出BadCredentialsException 異常。
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider {
public Authentication authenticate(Authentication authentication) {
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
} catch (UsernameNotFoundException ex) {
// 通過(guò)國(guó)際化資源獲取key = AbstractUserDetailsAuthenticationProvider.badCredentials
// 的錯(cuò)誤信息,如果沒(méi)有自定義,則默認(rèn)顯示Bad credentials。
// 該異常信息拋到了ProviderManager中
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}父ProviderManager處理異常。
// 這里需要注意,在默認(rèn)的情況下,我們當(dāng)前的認(rèn)證出來(lái)邏輯已經(jīng)是在父ProviderManager中進(jìn)行處理了
public class ProviderManager {
public Authentication authenticate(Authentication authentication) {
AuthenticationException lastException = null;
// ...
for (AuthenticationProvider provider : getProviders()) {
try {
result = provider.authenticate(authentication);
} catch (AuthenticationException ex) {
lastException = ex;
}
}
// ...
// 注意這里其實(shí)繼續(xù)將異常拋給了子ProviderManager對(duì)象
throw lastException;
}
}
子ProviderManager處理異常。
public class ProviderManager {
public Authentication authenticate(Authentication authentication) {
AuthenticationException lastException = null;
AuthenticationException parentException = null;
// ...
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
} catch (AuthenticationException ex) {
// 進(jìn)入該處
parentException = ex;
lastException = ex;
}
}
// ...
throw lastException;
}
}過(guò)濾器UsernamePasswordAuthenticationFilter接收到異常,該異常是有該過(guò)濾器的父類中進(jìn)行處理。
public abstract class AbstractAuthenticationProcessingFilter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
Authentication authenticationResult = attemptAuthentication(request, response);
} catch (AuthenticationException ex) {
unsuccessfulAuthentication(request, response, ex);
}
}
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
// ...
// 默認(rèn)failureHandler = SimpleUrlAuthenticationFailureHandler
// 這里也就是我們自定義的一個(gè)功能點(diǎn)
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
}
public class SimpleUrlAuthenticationFailureHandler {
public void onAuthenticationFailure(...) {
// 將異常保存到Session對(duì)象中
saveException(request, exception);
// 最后直接Redirect調(diào)整到登錄頁(yè)面
// defaultFailureUrl = /custom/login?error
this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
}
protected final void saveException(HttpServletRequest request, AuthenticationException exception) {
HttpSession session = request.getSession(false);
if (session != null || this.allowSessionCreation) {
// AUTHENTICATION_EXCEPTION = SPRING_SECURITY_LAST_EXCEPTION
// 在頁(yè)面中就可以通過(guò)Session獲取異常的信息了
// 在上一篇的文章中自定義登錄頁(yè)面中就有從該session中獲取異常信息
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
}
}
}以上就是Spring Security在處理登錄失敗的情景下如何進(jìn)行處理的,同時(shí)我們也知道了為UsernamePasswordAuthenticationFilter(父類) 配置AuthenticationFailureHandler是一個(gè)自定義的擴(kuò)展點(diǎn),我們可以在自定義的SecurityConfig中配置該失敗句柄。
無(wú)權(quán)限的異常
在上面的自定義配置中我們配置了兩個(gè)用戶:
- guest ADMIN
- test USERS
- /demos/** 一類的請(qǐng)求必須擁有 USERS 權(quán)限(角色)。
- /api/** 一類的請(qǐng)求必須擁有 ADMIN 權(quán)限(角色)。
接下來(lái)通過(guò)guest用戶登錄后,訪問(wèn)/demos/home接口查看默認(rèn)的錯(cuò)誤顯示。
該授權(quán)檢查的流程:
- FilterSecurityInterceptor#invoke。
- AbstractSecurityInterceptor#beforeInvocation。
- AbstractSecurityInterceptor#attemptAuthorization。
在上面的流程中主要核心方法是attemptAuthorization嘗試授權(quán)操作。
public abstract class AbstractSecurityInterceptor {
protected InterceptorStatusToken beforeInvocation(Object object) {
// ...
attemptAuthorization(object, attributes, authenticated);
// ...
}
private void attemptAuthorization(...) {
try {
// accessDecisionManager = AffirmativeBased
this.accessDecisionManager.decide(authenticated, object, attributes);
} catch (AccessDeniedException ex) {
// ...
// 異常拋給了子類處理
throw ex;
}
}
}
public class AffirmativeBased extends AbstractAccessDecisionManager {
// 該方法開始判斷當(dāng)前登錄的用戶信息是否具有相應(yīng)的權(quán)限信息
public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
// 當(dāng)拒絕次數(shù) > 0 那么將會(huì)拋出AccessDeniedException異常
// 默認(rèn)的異常信息會(huì)先從國(guó)際化資源中獲取key = AbstractAccessDecisionManager.accessDenied
// 如果沒(méi)有配置,則默認(rèn)信息:Access is denied
if (deny > 0) {
throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
}
} 最終異常AccessDeniedException并沒(méi)在FilterSecurityInterceptor中進(jìn)行處理,那么該異常就會(huì)被過(guò)濾器鏈中的ExceptionTranslationFilter中得到處理。
public class ExceptionTranslationFilter extends GenericFilterBean {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(request, response);
} catch (Exception ex) {
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
// 處理異常
handleSpringSecurityException(request, response, chain, securityException);
}
}
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
} else if (exception instanceof AccessDeniedException) {
// 處理被拒絕的異常
handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
}
}
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
// ...
// accessDeniedHandler = AccessDeniedHandlerImpl
// 訪問(wèn)拒絕句柄的默認(rèn)實(shí)現(xiàn)
// 這里也就成為了我們的一個(gè)自定義處理點(diǎn)
this.accessDeniedHandler.handle(request, response, exception);
}
}
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
if (this.errorPage == null) {
// 默認(rèn)這里的errorPage = null ,所以執(zhí)行這里的邏輯
// 這設(shè)置響應(yīng)狀態(tài)碼403
response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
return;
}
// Put exception into request scope (perhaps of use to a view)
request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
// Set the 403 status code.
response.setStatus(HttpStatus.FORBIDDEN.value());
request.getRequestDispatcher(this.errorPage).forward(request, response);
}
}
到此你應(yīng)該了解到了,當(dāng)我們沒(méi)有權(quán)限訪問(wèn)資源時(shí)默認(rèn)是如何處理的,同時(shí)也了解到了如何進(jìn)行自定義異常處理句柄。
自定義異常配置
上面介紹了錯(cuò)誤產(chǎn)生的原理及了解到了自定義異常處理句柄的方法,接下來(lái)通過(guò)自定義的方式展示錯(cuò)誤信息。
錯(cuò)誤的用戶名密碼
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() ;
http.authorizeRequests().antMatchers("/resources/**", "/cache/**").permitAll() ;
http.authorizeRequests().antMatchers("/demos/**").hasRole("USERS") ;
http.authorizeRequests().antMatchers("/api/**").hasRole("ADMIN") ;
http
.formLogin()
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8") ;
PrintWriter out = response.getWriter() ;
out.println("{\"code\": -1, \"message\": \"" + exception.getMessage() + "\"}") ;
out.close();
}
})
.loginPage("/custom/login") ;
}我們也可以將上面的AuthenticationFailureHandler 定義為一個(gè)Bean對(duì)象這樣方便我們做其它的一些操作。
登錄測(cè)試:
無(wú)權(quán)限的異常
上面介紹了當(dāng)沒(méi)有權(quán)限訪問(wèn)指定的資源時(shí)錯(cuò)誤產(chǎn)生的原理及了解到了自定義拒絕訪問(wèn)句柄的方法,接下來(lái)通過(guò)自定義的方式展示錯(cuò)誤信息。
自定義訪問(wèn)拒絕頁(yè)面的方式
在如下位置新建denied.html頁(yè)面。
// 自定義Controller
@Controller
public class ErrorController {
@GetMapping("/access/denied")
public String denied() {
return "denied" ;
}
}
// 自定義配置
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() ;
http.authorizeRequests().antMatchers("/resources/**", "/cache/**").permitAll() ;
http.authorizeRequests().antMatchers("/demos/**").hasRole("USERS") ;
http.authorizeRequests().antMatchers("/api/**").hasRole("ADMIN") ;
http
.formLogin()
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8") ;
PrintWriter out = response.getWriter() ;
out.println("{\"code\": -1, \"message\": \"" + exception.getMessage() + "\"}") ;
out.close();
}
})
.loginPage("/custom/login") ;
// 自定義訪問(wèn)拒絕頁(yè)面
http.exceptionHandling().accessDeniedPage("/access/denied") ;
}
簡(jiǎn)單的頁(yè)面內(nèi)容。
Access Denied
測(cè)試:
自定義403錯(cuò)誤頁(yè)面
將上面的http.exceptionHandling().accessDeniedPage("/access/denied") 代碼注釋了。
然后在下面位置新建403.html頁(yè)面。
簡(jiǎn)單的頁(yè)面內(nèi)容。
Denied Access This is page
測(cè)試:
自定義訪問(wèn)拒絕句柄的方式
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() ;
http.authorizeRequests().antMatchers("/resources/**", "/cache/**").permitAll() ;
http.authorizeRequests().antMatchers("/demos/**").hasRole("USERS") ;
http.authorizeRequests().antMatchers("/api/**").hasRole("ADMIN") ;
http
.formLogin()
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8") ;
PrintWriter out = response.getWriter() ;
out.println("{\"code\": -1, \"message\": \"" + exception.getMessage() + "\"}") ;
out.close();
}
})
.loginPage("/custom/login") ;
// 自定義訪問(wèn)拒絕頁(yè)面
// http.exceptionHandling().accessDeniedPage("/access/denied") ;
http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8") ;
PrintWriter out = response.getWriter() ;
out.println("{\"code\": -1, \"message\": \"" + accessDeniedException.getMessage() + "\"}") ;
out.close();
}
}) ;
}
測(cè)試:
總結(jié):
- 認(rèn)證失敗后的處理原理及自定義配置。
- 授權(quán)失敗后的處理原理及自定義配置。
當(dāng)前名稱:SpringSecurity權(quán)限控制系列(三)
網(wǎng)址分享:http://m.fisionsoft.com.cn/article/ccedcgd.html


咨詢
建站咨詢
