Spring Cloud實戰 | 第九篇:Spring Cloud整合Spring Security OAuth2認證伺服器統一認證自定義異常處理

你好,舊時光發表於2020-11-24

本文完整程式碼下載點選

Spring Cloud實戰 | 第九篇:Spring Cloud整合Spring Security OAuth2認證伺服器統一認證自定義異常處理

一. 前言

相信瞭解過我或者看過我之前的系列文章應該多少知道點我寫這些文章包括建立 有來商城youlai-mall 這個專案的目的,想給那些真的想提升自己或者迷茫的人(包括自己--一個工作6年覺得一無是處的菜鳥)提供一塊上升的基石。專案是真的從無到有(往期文章佐證),且使用當前主流的開發模式(微服務+前後端分離),最新主流的技術棧(Spring Boot+ Spring Cloud +Spring Cloud Alibaba + Vue),最流行的統一安全認證授權(OAuth2+JWT),好了玩笑開完了大家別當真,總之有興趣一起的小夥伴歡迎加入~

接下來說下這篇文章的原因,之前我是沒想過應用到專案中的OAuth2+JWT這套組合拳這麼受大家關注,期間一直有童鞋問怎麼自定義Spring Security OAuth2的異常處理、JWT怎麼續期、JWT退出等場景下如何失效等問題,所以最近有點時間想把這套統一認證授權完善掉,本篇就以如何自定義Spring Security OAuth2異常處理展開。

往期文章連結:

後端

  1. Spring Cloud實戰 | 第一篇:Windows搭建Nacos服務
  2. Spring Cloud實戰 | 第二篇:Spring Cloud整合Nacos實現註冊中心
  3. Spring Cloud實戰 | 第三篇:Spring Cloud整合Nacos實現配置中心
  4. Spring Cloud實戰 | 第四篇:Spring Cloud整合Gateway實現API閘道器
  5. Spring Cloud實戰 | 第五篇:Spring Cloud整合OpenFeign實現微服務之間的呼叫
  6. Spring Cloud實戰 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT實現微服務統一認證授權
  7. Spring Cloud實戰 | 最七篇:Spring Cloud Gateway+Spring Security OAuth2整合統一認證授權平臺下實現登出使JWT失效方案
  8. Spring Cloud實戰 | 最八篇:Spring Cloud +Spring Security OAuth2+ Vue前後端分離模式下無感知重新整理實現JWT續期
  9. Spring Cloud實戰 | 最九篇:Spring Security OAuth2認證伺服器統一認證自定義異常處理

管理前端

  1. vue-element-admin實戰 | 第一篇: 移除mock接入後臺,搭建有來商城youlai-mall前後端分離管理平臺
  2. vue-element-admin實戰 | 第二篇: 最小改動接入後臺實現根據許可權動態載入選單

微信小程式

  1. vue+uniapp商城實戰 | 第一篇:【有來小店】微信小程式快速開發接入Spring Cloud OAuth2認證中心完成授權登入

二. 自定義異常實現程式碼

直接需要答案的本節走起,新增和修改三個檔案即可,異常分析,點選下載完整工程程式碼

1. 在youlai-auth認證伺服器模組新增全域性異常處理器AuthExceptionHandler

package com.youlai.auth.exception;

import com.youlai.common.core.result.Result;
import com.youlai.common.core.result.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
@Slf4j
public class AuthExceptionHandler {

    /**
     * 使用者名稱和密碼錯誤
     *
     * @param e
     * @return
     */
    @ExceptionHandler(InvalidGrantException.class)
    public Result handleInvalidGrantException(InvalidGrantException e) {
        return Result.custom(ResultCode.USERNAME_OR_PASSWORD_ERROR);
    }

    /**
     * 賬戶異常(禁用、鎖定、過期)
     *
     * @param e
     * @return
     */
    @ExceptionHandler({InternalAuthenticationServiceException.class})
    public Result handleInternalAuthenticationServiceException(InternalAuthenticationServiceException e) {
        return Result.error(e.getMessage());
    }
}

2. 重寫ClientCredentialsTokenEndpointFilter實現客戶端自定義異常處理

package com.youlai.auth.filter;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter;
import org.springframework.security.web.AuthenticationEntryPoint;

/**
 * 重寫filter實現客戶端自定義異常處理
 */
public class CustomClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter {

    private AuthorizationServerSecurityConfigurer configurer;
    private AuthenticationEntryPoint authenticationEntryPoint;


    public CustomClientCredentialsTokenEndpointFilter(AuthorizationServerSecurityConfigurer configurer) {
        this.configurer = configurer;
    }

    @Override
    public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
        super.setAuthenticationEntryPoint(null);
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    @Override
    protected AuthenticationManager getAuthenticationManager() {
        return configurer.and().getSharedObject(AuthenticationManager.class);
    }

    @Override
    public void afterPropertiesSet() {
        setAuthenticationFailureHandler((request, response, e) -> authenticationEntryPoint.commence(request, response, e));
        setAuthenticationSuccessHandler((request, response, authentication) -> {
        });
    }
}

3. AuthorizationServerConfig認證伺服器配置修改

/**
 * 授權服務配置
 */
@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    ......

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        /*security.allowFormAuthenticationForClients();*/
        CustomClientCredentialsTokenEndpointFilter endpointFilter = new CustomClientCredentialsTokenEndpointFilter(security);
        endpointFilter.afterPropertiesSet();
        endpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint());
        security.addTokenEndpointAuthenticationFilter(endpointFilter);

        security.authenticationEntryPoint(authenticationEntryPoint())
                .tokenKeyAccess("isAuthenticated()")
                .checkTokenAccess("permitAll()");
    }

    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return (request, response, e) -> {
            response.setStatus(HttpStatus.HTTP_OK);
            response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Cache-Control", "no-cache");
            Result result = Result.custom(ResultCode.CLIENT_AUTHENTICATION_FAILED);
            response.getWriter().print(JSONUtil.toJsonStr(result));
            response.getWriter().flush();
        };
    }
    ......

}

三. 異常處理分析

其實你搜一下有關Spring Security OAuth2如何自定義異常處理,網上會很多差不多解決方案提供參考,但是照搬過來試用,一點效果沒有?!咋回事麼?其實不能武斷的說人家方案不行,最多的可能是Spring Security OAuth2版本不一致,本篇專案使用的是2.3.4版本,目前截止寫這篇文章最新一版的是2020.5.28釋出的2.5.0版本,後續專案會升級,如果有差異我會修改本篇文章,總之給大家提供一個解決思路,可行不可行我是不希望大家不能在我裡浪費時間。

好了正文開始了~ Spring Security OAuth2認證伺服器異常目前我知道的有3類:

  1. 使用者名稱或密碼錯誤
  2. 賬戶狀態異常
  3. 客戶端認證異常

有知道其他的歡迎留言補充~,以下就這3類異常逐一分析

在異常處理之前先看下UserDetailsServiceImpl#loadUserByUsername方法丟擲的異常資訊,如下圖:

1. 使用者名稱或密碼錯誤

  • 異常分析
org.springframework.security.oauth2.common.exceptions.InvalidGrantException: 使用者名稱或密碼錯誤

通過異常堆疊資訊定位到最終丟擲異常的方法是ResourceOwnerPasswordTokenGranter#getOAuth2Authentication,異常型別是InvalidGrantException,其實到這個異常型別中間經過幾道轉換UsernameNotFoundException->BadCredentialsException->InvalidGrantException

  • 處理方法

新增全域性異常處理器捕獲(定位標識:AuthExceptionHandler)


/**
 * 使用者名稱和密碼異常
 *
 * @param e
 * @return
 */
@ExceptionHandler(InvalidGrantException.class)
public Result handleInvalidGrantException(InvalidGrantException e) {
    return Result.error(ResultCode.USERNAME_OR_PASSWORD_ERROR);
}
  • 結果驗證

驗證成功,已按照自定義異常格式返回

2. 賬戶狀態異常

  • 異常分析

首先我們需要把資料庫youlai的表sys_user的欄位status設定為0,表示不可用狀態,然後輸入正確的使用者名稱和密碼,看看跑出來的原生異常資訊,可惜的是這個異常沒有列印堆疊資訊,不過沒關係,我們斷點除錯下,最終定位到ProviderManager#authenticate方法丟擲的異常,異常型別是InternalAuthenticationServiceException。

  • 處理方法

新增全域性異常處理器捕獲(定位標識:AuthExceptionHandler)

/**
 * 賬戶異常(禁用、鎖定、過期)
 *
 * @param e
 * @return
 */
@ExceptionHandler({InternalAuthenticationServiceException.class})
public Result handleInternalAuthenticationServiceException(InternalAuthenticationServiceException e) {
    return Result.error(e.getMessage());
}
  • 結果驗證

驗證成功,已按照自定義異常格式返回

3. 客戶端認證異常

  • 異常分析

之前兩種異常方式都可以通過全域性異常處理器捕獲,且@RestControllerAdvice只能捕獲Controller的異常。

客戶端認證的異常則是發生在過濾器filter上,此時還沒進入DispatcherServlet請求處理流程,便無法通過全域性異常處理器捕獲。

先看下客戶端認證異常出現的位置,首先把客戶端ID改成錯的。

然後執行“登入”操作,返回錯誤資訊如下:

{"error":"invalid_client","error_description":"Bad client credentials"}

一眼望去,這顯然不是我們想要的格式。

那怎麼做才能捕獲這個異常轉換成自定義資料格式返回呢?顯然全域性異常處理器無法實現,那必須轉換下思路了。

首先客戶端的認證是交由ClientCredentialsTokenEndpointFilter來完成的,其中有後置新增失敗處理方法,最後把異常交給OAuth2AuthenticationEntryPoint這個所謂認證入口處理。

認證入口OAuth2AuthenticationEntryPoint#commence方法中轉給父類AbstractOAuth2SecurityExceptionHandler#doHandle方法。

最後異常定格在AbstractOAuth2SecurityExceptionHandler#doHandle方法上,如下圖:

其中this.enhanceResponse是呼叫OAuth2AuthenticationEntryPoint#enhanceResponse方法得到響應結果資料。

  • 處理方法

上面我們得知客戶端的認證失敗異常是過濾器ClientCredentialsTokenEndpointFilter轉交給OAuth2AuthenticationEntryPoint得到響應結果的,既然這樣我們就可以重寫ClientCredentialsTokenEndpointFilter然後使用自定義的AuthenticationEntryPoint替換原生的OAuth2AuthenticationEntryPoint,在自定義AuthenticationEntryPoint處理得到我們想要的異常資料。

自定義AuthenticationEntryPoint設定異常響應資料格式

重寫ClientCredentialsTokenEndpointFilter替換AuthenticationEntryPoint

認證伺服器配置新增自定義過濾器

  • 結果驗證

驗證成功,已按照自定義異常格式返回

四. 總結

至此,認證伺服器的自定義異常處理已全部處理完畢,資源伺服器異常處理說明在這篇文章 Spring Cloud實戰 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT實現微服務統一認證授權,這就宣告 youlai-mall 的統一認證授權模組基本達到完善的一個標準, 後面繼續回到業務功能的開發,所以覺得對你有幫助的給個關注(持續更新)或者給個star,灰常感謝! 最重要的如果你真的對這個專案有興趣想一起開發學習的像文章開始說的那樣請聯絡我哈~

相關文章