日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
SpringSecurity權(quán)限控制系列(三)

環(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ù)解決您的所有建站問題。

本篇主要內(nèi)容:自定義異常處理

上一篇:《??Spring Security權(quán)限控制系列(二)??》

注意:記得不要忘記關(guān)閉CSRF功能,由于之前的案例演示開啟了CSRF,忘記關(guān)閉,導(dǎo)致在本篇案例中在登錄時(shí)總是403狀態(tài)碼,點(diǎn)登錄后通過調(diào)試發(fā)現(xiàn)請(qǐng)求的url是總是/error(我自定義登錄頁面并沒有添加_csrf隱藏域字段)。

默認(rèn)異常原理

基于前面兩篇的內(nèi)容我們發(fā)現(xiàn)只要沒有無權(quán)限訪問接口,就會(huì)報(bào)錯(cuò)誤,錯(cuò)誤信息如下:

登錄成功后五權(quán)限訪問接口時(shí)默認(rèn)的返回錯(cuò)誤信息

錯(cuò)誤的用戶名或密碼時(shí)

接下來我們看看系統(tǒng)默認(rèn)是如何提供該錯(cuò)誤頁面信息的。

錯(cuò)誤的用戶名密碼

當(dāng)?shù)卿洉r(shí)填寫的錯(cuò)誤用戶名或密碼時(shí),再次返回了登錄頁面,并且攜帶了錯(cuò)誤信息。接下來通過源碼查看這部分路徑。

當(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") ;
}
}

上面我們自定義了登錄頁面/custom/login,所以我們的過濾器鏈中有個(gè)核心的過濾器UsernamePasswordAuthenticationFilter 該過濾器專門用來處理POST提交的登錄URI,我們這里自定義了所以該過濾器攔截的是/custom/login,該過濾器在判斷當(dāng)前請(qǐng)求的時(shí)候會(huì)先判斷是不是POST方式提交的,然后判斷URI,所以我們?cè)跒g覽器直接訪問該uri的時(shí)候是不會(huì)發(fā)生任何認(rèn)證邏輯處理的。

登錄認(rèn)證的流程:

  1. UsernamePasswordAuthenticationFilter#attemptAuthentication。
  2. ProviderManager#authenticate。
  3. 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ó)際化資源獲取key = AbstractUserDetailsAuthenticationProvider.badCredentials
// 的錯(cuò)誤信息,如果沒有自定義,則默認(rèn)顯示Bad credentials。
// 該異常信息拋到了ProviderManager中
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}

父ProviderManager處理異常。

// 這里需要注意,在默認(rèn)的情況下,我們當(dāng)前的認(rèn)證出來邏輯已經(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;
}
}

過濾器UsernamePasswordAuthenticationFilter接收到異常,該異常是有該過濾器的父類中進(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)整到登錄頁面
// 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
// 在頁面中就可以通過Session獲取異常的信息了
// 在上一篇的文章中自定義登錄頁面中就有從該session中獲取異常信息
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
}
}
}

以上就是Spring Security在處理登錄失敗的情景下如何進(jìn)行處理的,同時(shí)我們也知道了為UsernamePasswordAuthenticationFilter(父類) 配置AuthenticationFailureHandler是一個(gè)自定義的擴(kuò)展點(diǎn),我們可以在自定義的SecurityConfig中配置該失敗句柄。

無權(quán)限的異常

在上面的自定義配置中我們配置了兩個(gè)用戶:

  1. guest ADMIN
  2. test USERS
  • /demos/** 一類的請(qǐng)求必須擁有 USERS 權(quán)限(角色)。
  • /api/** 一類的請(qǐng)求必須擁有 ADMIN 權(quán)限(角色)。

接下來通過guest用戶登錄后,訪問/demos/home接口查看默認(rèn)的錯(cuò)誤顯示。

該授權(quán)檢查的流程:

  1. FilterSecurityInterceptor#invoke。
  2. AbstractSecurityInterceptor#beforeInvocation。
  3. 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
// 如果沒有配置,則默認(rèn)信息:Access is denied
if (deny > 0) {
throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
}
}

最終異常AccessDeniedException并沒在FilterSecurityInterceptor中進(jìn)行處理,那么該異常就會(huì)被過濾器鏈中的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
// 訪問拒絕句柄的默認(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)我們沒有權(quán)限訪問資源時(shí)默認(rèn)是如何處理的,同時(shí)也了解到了如何進(jìn)行自定義異常處理句柄。

自定義異常配置

上面介紹了錯(cuò)誤產(chǎn)生的原理及了解到了自定義異常處理句柄的方法,接下來通過自定義的方式展示錯(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è)試:

無權(quán)限的異常

上面介紹了當(dāng)沒有權(quán)限訪問指定的資源時(shí)錯(cuò)誤產(chǎn)生的原理及了解到了自定義拒絕訪問句柄的方法,接下來通過自定義的方式展示錯(cuò)誤信息。

自定義訪問拒絕頁面的方式

在如下位置新建denied.html頁面。

// 自定義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") ;
// 自定義訪問拒絕頁面
http.exceptionHandling().accessDeniedPage("/access/denied") ;
}

簡(jiǎn)單的頁面內(nèi)容。

Access Denied

測(cè)試:

自定義403錯(cuò)誤頁面

將上面的http.exceptionHandling().accessDeniedPage("/access/denied") 代碼注釋了。

然后在下面位置新建403.html頁面。

簡(jiǎn)單的頁面內(nèi)容。

Denied Access This is page

測(cè)試:

自定義訪問拒絕句柄的方式

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") ;
// 自定義訪問拒絕頁面
// 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é):

  1. 認(rèn)證失敗后的處理原理及自定義配置。
  2. 授權(quán)失敗后的處理原理及自定義配置。

網(wǎng)站標(biāo)題:SpringSecurity權(quán)限控制系列(三)
分享地址:http://m.5511xx.com/article/ccedcgd.html