分佈式系統--感性認識JWT

好久沒寫博客了,因爲最近公司要求我學spring cloud ,早點將以前軟件遷移到新的架構上。所以我那個拼命的學吶,總是圖快,很多關鍵的筆記沒有做好記錄,現在又遺忘了很多關鍵的技術點,極其罪惡!

現在想一想,還是踏踏實實的走比較好。這不,今天我冒了個泡,來補一補前面我所學所忘的知識點。

想要解鎖更多新姿勢?請訪問我的博客

常見的認證機制

今天我麼聊一聊JWT。

關於JWT,相信很多人都已經看過用過,他是基於json數據結構的認證規範,簡單的說就是驗證用戶登沒登陸的玩意。這時候你可能回想,哎喲,不是又那個session麼,分佈式系統用redis做分佈式session,那這個jwt有什麼好處呢?

請聽我慢慢訴說這歷史!

最原始的辦法--HTTP BASIC AUTH

HTTP BASIC auth,別看它名字那麼長那麼生,你就認爲這個玩意很高大上。其實原理很簡單,簡單的說就是每次請求API的時候,都會把用戶名和密碼通過restful API傳給服務端。這樣就可以實現一個無狀態思想,即每次HTTP請求和以前都沒有啥關係,只是獲取目標URI,得到目標內容之後,這次連接就被殺死,沒有任何痕跡。你可別一聽無狀態,正是現在的熱門思想,就覺得很厲害。其實他的缺點還是又的,我們通過http請求發送給服務端的時候,很有可能將我們的用戶名密碼直接暴漏給第三方客戶端,風險特別大,因此生產環境下用這個方法很少。

Session和cookie

session和cookie老生常談了。開始時,都會在服務端全局創建session對象,session對象保存着各種關鍵信息,同時向客戶端發送一組sessionId,成爲一個cookie對象保存在瀏覽器中。

當認證時,cookie的數據會傳入服務端與session進行匹配,進而進行數據認證。

how session work

此時,實現的是一個有狀態的思想,即該服務的實例可以將一部分數據隨時進行備份,並且在創建一個新的有狀態服務時,可以通過備份恢復這些數據,以達到數據持久化的目的。

缺點

這種認證方法基本是現在軟件最常用的方法了,它有一些自己的缺點:

  • 安全性。cookies的安全性不好,攻擊者可以通過獲取本地cookies進行欺騙或者利用cookies進行CSRF攻擊。
  • 跨域問題。使用cookies時,在多個域名下,會存在跨域問題。
  • 有狀態。session在一定的時間裏,需要存放在服務端,因此當擁有大量用戶時,也會大幅度降低服務端的性能。
  • 狀態問題。當有多臺機器時,如何共享session也會是一個問題,也就是說,用戶第一個訪問的時候是服務器A,而第二個請求被轉發給了服務器B,那服務器B如何得知其狀態。
  • 移動手機問題。現在的智能手機,包括安卓,原生不支持cookie,要使用cookie挺麻煩。

Token認證(使用jwt規範)

token 即使是在計算機領域中也有不同的定義,這裏我們說的token,是指 訪問資源的憑據 。使用基於 Token 的身份驗證方法,在服務端不需要存儲用戶的登錄記錄。大概的流程是 這樣的:

  1. 客戶端使用用戶名跟密碼請求登錄
  2. 服務端收到請求,去驗證用戶名與密碼
  3. 驗證成功後,服務端會簽發一個 Token,再把這個 Token 發送給客戶端
  4. 客戶端收到 Token 以後可以把它存儲起來,比如放在 Cookie 裏
  5. 客戶端每次向服務端請求資源的時候需要帶着服務端簽發的 Token
  6. 服務端收到請求,然後去驗證客戶端請求裏面帶着的 Token,如果驗證成功,就向客戶端返回請求的數據

Token機制,我認爲其本質思想就是將session中的信息簡化很多,當作cookie用,也就是客戶端的“session”。

好處

那Token機制相對於Cookie機制又有什麼好處呢?

  • 支持跨域訪問: Cookie是不允許垮域訪問的,這一點對Token機制是不存在的,前提 是傳輸的用戶認證信息通過HTTP頭傳輸.
  • 無狀態:Token機制本質是校驗, 他得到的會話狀態完全來自於客戶端, Token機制在服務端不需要存儲session信息,因爲 Token 自身包含了所有登錄用戶的信息,只需要在客戶端的cookie或本地介質存儲狀態信息.
  • 更適用CDN: 可以通過內容分發網絡請求你服務端的所有資料(如:javascript, HTML,圖片等),而你的服務端只要提供API即可.
  • 去耦: 不需要綁定到一個特定的身份驗證方案。Token可以在任何地方生成,只要在 你的API被調用的時候,你可以進行Token生成調用即可.
  • 更適用於移動應用: 當你的客戶端是一個原生平臺(iOS, Android,Windows 8等) 時,Cookie是不被支持的(你需要通過Cookie容器進行處理),這時採用Token認 證機制就會簡單得多。 CSRF:因爲不再依賴於Cookie,所以你就不需要考慮對CSRF(跨站請求僞造)的防 範。
  • 性能: 一次網絡往返時間(通過數據庫查詢session信息)總比做一次HMACSHA256 計算 的Token驗證和解析要費時得多. 不需要爲登錄頁面做特殊處理: 如果你使用Protractor 做功能測試的時候,不再需要 爲登錄頁面做特殊處理.
  • 基於標準化:你的API可以採用標準化的 JSON Web Token (JWT). 這個標準已經存在 多個後端庫(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如: Firebase,Google, Microsoft)

缺陷在哪?

說了那麼多token認證的好處,但他其實並沒有想象的那麼神,token 也並不是沒有問題。

  1. 佔帶寬

    正常情況下要比 session_id 更大,需要消耗更多流量,擠佔更多帶寬,假如你的網站每月有 10 萬次的瀏覽器,就意味着要多開銷幾十兆的流量。聽起來並不多,但日積月累也是不小一筆開銷。實際上,許多人會在 JWT 中存儲的信息會更多。

  2. 無論如何你需要操作數據庫

    在網站上使用 JWT,對於用戶加載的幾乎所有頁面,都需要從緩存/數據庫中加載用戶信息,如果對於高流量的服務,你確定這個操作合適麼?如果使用redis進行緩存,那麼效率上也並不能比 session 更高效

  3. 無法在服務端註銷,那麼久很難解決劫持問題
  4. 性能問題

    JWT 的賣點之一就是加密簽名,由於這個特性,接收方得以驗證 JWT 是否有效且被信任。但是大多數 Web 身份認證應用中,JWT 都會被存儲到 Cookie 中,這就是說你有了兩個層面的簽名。聽着似乎很牛逼,但是沒有任何優勢,爲此,你需要花費兩倍的 CPU 開銷來驗證簽名。對於有着嚴格性能要求的 Web 應用,這並不理想,尤其對於單線程環境。

JWT

現在我們來說說今天的主角,JWT

JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在用 戶和服務器之間傳遞安全可靠的信息

1543760350545

組成

一個JWT實際上就是一個字符串,它由三部分組成,頭部載荷簽名

頭部(header)

頭部用於描述關於該JWT的最基本的信息,例如其類型以及簽名所用的算法等。這也可以 被表示成一個JSON對象。

{
    "typ":"JWT",
    "alg":"HS256"
}

這就是頭部的明文內容,第一部分說明他是一個jwt,第二部分則指出簽名算法用的是HS256算法

然後將這個頭部進行BASE64編碼,編碼後形成頭部:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

載荷(payload)

載荷就是存放有效信息的地方,有效信息包含三個部分:

(1)標準中註冊的聲明(建議但不強制使用)

  • iss: jwt簽發者
  • sub: jwt所面向的用戶
  • aud: 接收jwt的一方
  • exp: jwt的過期時間,這個過期時間必須要大於簽發時間
  • nbf: 定義在什麼時間之前,該jwt都是不可用的.
  • iat: jwt的簽發時間
  • jti: jwt的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊。

(2)公共的聲明
公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息. 但不建議添加敏感信息,因爲該部分在客戶端可解密.

(3)私有的聲明

私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因爲base64 是對稱解密的,意味着該部分信息可以歸類爲明文信息。

{
    "sub":"1234567890",
    "name":"tengshe789",
    "admin": true
}

上面就是一個簡單的載荷的明文,接下來使用base64加密:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

簽證(signature)

jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:

  1. header (base64後的)
  2. payload (base64後的)
  3. secret

這個部分需要base64加密後的header和base64加密後的payload使用.連接組成的字符串,然後通過header中聲明的加密方式進行加鹽secret組合加密,然後就構成了jwt的第 三部分。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

合成

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7Hg Q

實現JWT

現在一般實現jwt,都使用Apache 的開源項目JJWT(一個提供端到端的JWT創建和驗證的Java庫)。

依賴

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>

創建token的demo

public class CreateJWT {
    public static void main(String[] args) throws Exception{
        JwtBuilder builder = Jwts.builder().setId("123")
                .setSubject("jwt所面向的用戶")
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256,"tengshe789");
        String s = builder.compact();
        System.out.println(s);
        //eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiJqd3TmiYDpnaLlkJHnmoTnlKjmiLciLCJpYXQiOjE1NDM3NTk0MjJ9.1sIlEynqqZmA4PbKI6GgiP3ljk_aiypcsUxSN6-ATIA
    }
}

結果如圖:

1543759471279

(注意,jjwt不支持jdk11,0.9.1以後的jjwt必須實現signWith()方法才能實現)

解析Token的demo

public class ParseJWT {
    public static void main(String[] args) {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiJqd3TmiYDpnaLlkJHnmoTnlKjmiLciLCJpYXQiOjE1NDM3NTk0MjJ9.1sIlEynqqZmA4PbKI6GgiP3ljk_aiypcsUxSN6-ATIA";

        Claims claims =
                Jwts.parser().setSigningKey("tengshe789").parseClaimsJws(token).getBody();
        
        System.out.println("id"+claims.getId());
        System.out.println("Subject"+claims.getSubject());
        System.out.println("IssuedAt"+claims.getIssuedAt());
    }
}

結果如圖:

1543759769057

生產中的JWT

在企業級系統中,通常內部會有非常多的工具平臺供大家使用,比如人力資源,代碼管理,日誌監控,預算申請等等。如果每一個平臺都實現自己的用戶體系的話無疑是巨大的浪費,所以公司內部會有一套公用的用戶體系,用戶只要登陸之後,就能夠訪問所有的系統。

這就是 單點登錄(SSO: Single Sign-On)

SSO 是一類解決方案的統稱,而在具體的實施方面,一般有兩種策略可供選擇:

  1. SAML 2.0
  2. OAuth 2.0

欲揚先抑,先說說幾個重要的知識點。

Authentication VS Authorisation

  • Authentication: 身份鑑別,鑑權,以下簡稱認證

    認證 的作用在於認可你有權限訪問系統,用於鑑別訪問者是否是合法用戶。負責認證的服務通常稱爲 Authorization Server 或者 Identity Provider,以下簡稱 IdP

  • Authorisation: 授權

    授權 用於決定你有訪問哪些資源的權限。大多數人不會區分這兩者的區別,因爲站在用戶的立場上。而作爲系統的設計者來說,這兩者是有差別的,這是不同的兩個工作職責,我們可以只需要認證功能,而不需要授權功能,甚至不需要自己實現認證功能,而藉助 Google 的認證系統,即用戶可以用 Google 的賬號進行登陸。負責提供資源(API調用)的服務稱爲 Resource Server 或者 Service Provider,以下簡稱 SP

SMAL 2.0

smal flow

OAuth(JWT)

OAuth(開放授權)是一個開放的授權標準,允許用戶讓第三方應用訪問該用戶在 某一web服務上存儲的私密的資源(如照片,視頻,聯繫人列表),而無需將用戶名和密碼提供給第三方應用。

流程可以參考如下:

oauth

簡單的來說,就是你要訪問一個應用服務,先找它要一個request token(請求令牌),再把這個request token發到第三方認證服務器,此時第三方認證服務器會給你一個aceess token(通行令牌), 有了aceess token你就可以使用你的應用服務了。

注意圖中第4步兌換 access token 的過程中,很多第三方系統,如Google ,並不會僅僅返回 access token,還會返回額外的信息,這其中和之後更新相關的就是 refresh token。一旦 access token 過期,你就可以通過 refresh token 再次請求 access token

refresh token

當然了,流程是根據你的請求方式和訪問的資源類型而定的,業務很多也是不一樣的,我這是簡單的聊聊。

現在這種方法比較常見,常見的譬如使用QQ快速登陸,用的基本的都是這種方法。

開源項目

我們用一個很火的開源項目Cloud-Admin爲栗子,來分析一下jwt的應用。

Cloud-Admin是基於Spring Cloud微服務化開發平臺,具有統一授權、認證後臺管理系統,其中包含具備用戶管理、資源權限管理、網關API管理等多個模塊,支持多業務系統並行開發。

目錄結構

1543763543823

鑑權中心功能在ace-authace-gate下。

模型

下面是官方提供的架構模型。

image.png

可以看到,AuthServer在架構的中心環節,要訪問服務,必須需要鑑權中心的JWT鑑權。

鑑權中心服務端代碼解讀

實體類

先看實體類,這裏鑑權中心定義了一組客戶端實體,如下:

@Table(name = "auth_client")
@Getter
@Setter
public class Client {
    @Id
    private Integer id;

    private String code;

    private String secret;

    private String name;

    private String locked = "0";

    private String description;

    @Column(name = "crt_time")
    private Date crtTime;

    @Column(name = "crt_user")
    private String crtUser;

    @Column(name = "crt_name")
    private String crtName;

    @Column(name = "crt_host")
    private String crtHost;

    @Column(name = "upd_time")
    private Date updTime;

    @Column(name = "upd_user")
    private String updUser;

    @Column(name = "upd_name")
    private String updName;

    @Column(name = "upd_host")
    private String updHost;
    
    private String attr1;
    private String attr2;
    private String attr3;
    private String attr4;
    private String attr5;
    private String attr6;
    private String attr7;
    private String attr8;

對應數據庫:

CREATE TABLE `auth_client` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(255) DEFAULT NULL COMMENT '服務編碼',
  `secret` varchar(255) DEFAULT NULL COMMENT '服務密鑰',
  `name` varchar(255) DEFAULT NULL COMMENT '服務名',
  `locked` char(1) DEFAULT NULL COMMENT '是否鎖定',
  `description` varchar(255) DEFAULT NULL COMMENT '描述',
  `crt_time` datetime DEFAULT NULL COMMENT '創建時間',
  `crt_user` varchar(255) DEFAULT NULL COMMENT '創建人',
  `crt_name` varchar(255) DEFAULT NULL COMMENT '創建人姓名',
  `crt_host` varchar(255) DEFAULT NULL COMMENT '創建主機',
  `upd_time` datetime DEFAULT NULL COMMENT '更新時間',
  `upd_user` varchar(255) DEFAULT NULL COMMENT '更新人',
  `upd_name` varchar(255) DEFAULT NULL COMMENT '更新姓名',
  `upd_host` varchar(255) DEFAULT NULL COMMENT '更新主機',
  `attr1` varchar(255) DEFAULT NULL,
  `attr2` varchar(255) DEFAULT NULL,
  `attr3` varchar(255) DEFAULT NULL,
  `attr4` varchar(255) DEFAULT NULL,
  `attr5` varchar(255) DEFAULT NULL,
  `attr6` varchar(255) DEFAULT NULL,
  `attr7` varchar(255) DEFAULT NULL,
  `attr8` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;

這些是每組微服務客戶端的信息

第二個實體類,就是客戶端_服務的實體,也就是對應着那些微服務客戶端能調用哪些微服務客戶端:

大概對應的就是微服務間調用權限關係。

@Table(name = "auth_client_service")
public class ClientService {
    @Id
    private Integer id;

    @Column(name = "service_id")
    private String serviceId;

    @Column(name = "client_id")
    private String clientId;

    private String description;

    @Column(name = "crt_time")
    private Date crtTime;

    @Column(name = "crt_user")
    private String crtUser;

    @Column(name = "crt_name")
    private String crtName;

    @Column(name = "crt_host")
    private String crtHost;}

接口層

我們跳着看,先看接口層

@RestController
@RequestMapping("jwt")
@Slf4j
public class AuthController {
    @Value("${jwt.token-header}")
    private String tokenHeader;

    @Autowired
    private AuthService authService;

    @RequestMapping(value = "token", method = RequestMethod.POST)
    public ObjectRestResponse<String> createAuthenticationToken(
            @RequestBody JwtAuthenticationRequest authenticationRequest) throws Exception {
        log.info(authenticationRequest.getUsername()+" require logging...");
        final String token = authService.login(authenticationRequest);
        return new ObjectRestResponse<>().data(token);
    }

    @RequestMapping(value = "refresh", method = RequestMethod.GET)
    public ObjectRestResponse<String> refreshAndGetAuthenticationToken(
            HttpServletRequest request) throws Exception {
        String token = request.getHeader(tokenHeader);
        String refreshedToken = authService.refresh(token);
        return new ObjectRestResponse<>().data(refreshedToken);
    }

    @RequestMapping(value = "verify", method = RequestMethod.GET)
    public ObjectRestResponse<?> verify(String token) throws Exception {
        authService.validate(token);
        return new ObjectRestResponse<>();
    }
}

這裏放出了三個接口

先說第一個接口,創建token

具體邏輯如下:
每一個用戶登陸進來時,都會進入這個環節。根據request中用戶的用戶名和密碼,利用feign客戶端的攔截器攔截request,然後使用作者寫的JwtTokenUtil裏面的各種方法取出token中的key和密鑰,驗證token是否正確,正確則用authService.login(authenticationRequest);的方法返回出去一個新的token。

public String login(JwtAuthenticationRequest authenticationRequest) throws Exception {
        UserInfo info = userService.validate(authenticationRequest);
        if (!StringUtils.isEmpty(info.getId())) {
            return jwtTokenUtil.generateToken(new JWTInfo(info.getUsername(), info.getId() + "", info.getName()));
        }
        throw new UserInvalidException("用戶不存在或賬戶密碼錯誤!");
    }

下圖是詳細邏輯圖:

model

鑑權中心客戶端代碼

入口

作者寫了個註解的入口,使用@EnableAceAuthClient即自動開啓微服務(客戶端)的鑑權管理

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AutoConfiguration.class)
@Documented
@Inherited
public @interface EnableAceAuthClient {
}

配置

接着沿着註解的入口看

@Configuration
@ComponentScan({"com.github.wxiaoqi.security.auth.client","com.github.wxiaoqi.security.auth.common.event"})
public class AutoConfiguration {
    @Bean
    ServiceAuthConfig getServiceAuthConfig(){
        return new ServiceAuthConfig();
    }
    @Bean
    UserAuthConfig getUserAuthConfig(){
        return new UserAuthConfig();
    }
}

註解會自動的將客戶端的用戶token和服務token的關鍵信息加載到bean中

feigin攔截器

作者重寫了okhttp3攔截器的方法,每一次微服務客戶端請求的token都會被攔截下來,驗證服務調用服務的token和用戶調用服務的token是否過期,過期則返回新的token

@Override
    public Response intercept(Chain chain) throws IOException {
        Request newRequest = null;
        if (chain.request().url().toString().contains("client/token")) {
            newRequest = chain.request()
                    .newBuilder()
                    .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken())
                    .build();
        } else {
            newRequest = chain.request()
                    .newBuilder()
                    .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken())
                    .header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken())
                    .build();
        }
        Response response = chain.proceed(newRequest);
        if (HttpStatus.FORBIDDEN.value() == response.code()) {
            if (response.body().string().contains(String.valueOf(CommonConstants.EX_CLIENT_INVALID_CODE))) {
                log.info("Client Token Expire,Retry to request...");
                serviceAuthUtil.refreshClientToken();
                newRequest = chain.request()
                        .newBuilder()
                        .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken())
                        .header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken())
                        .build();
                response = chain.proceed(newRequest);
            }
        }
        return response;
    }

spring容器的攔截器

第二道攔截器是來自spring容器的,第一道feign攔截器只是驗證了兩個token是否過期,但token真實的權限卻沒驗證。接下來就要驗證兩個token的權限問題了。

服務調用權限代碼如下:

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        // 配置該註解,說明不進行服務攔截
        IgnoreClientToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreClientToken.class);
        if (annotation == null) {
            annotation = handlerMethod.getMethodAnnotation(IgnoreClientToken.class);
        }
        if(annotation!=null) {
            return super.preHandle(request, response, handler);
        }

        String token = request.getHeader(serviceAuthConfig.getTokenHeader());
        IJWTInfo infoFromToken = serviceAuthUtil.getInfoFromToken(token);
        String uniqueName = infoFromToken.getUniqueName();
        for(String client:serviceAuthUtil.getAllowedClient()){
            if(client.equals(uniqueName)){
                return super.preHandle(request, response, handler);
            }
        }
        throw new ClientForbiddenException("Client is Forbidden!");
    }

用戶權限:

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        // 配置該註解,說明不進行用戶攔截
        IgnoreUserToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreUserToken.class);
        if (annotation == null) {
            annotation = handlerMethod.getMethodAnnotation(IgnoreUserToken.class);
        }
        if (annotation != null) {
            return super.preHandle(request, response, handler);
        }
        String token = request.getHeader(userAuthConfig.getTokenHeader());
        if (StringUtils.isEmpty(token)) {
            if (request.getCookies() != null) {
                for (Cookie cookie : request.getCookies()) {
                    if (cookie.getName().equals(userAuthConfig.getTokenHeader())) {
                        token = cookie.getValue();
                    }
                }
            }
        }
        IJWTInfo infoFromToken = userAuthUtil.getInfoFromToken(token);
        BaseContextHandler.setUsername(infoFromToken.getUniqueName());
        BaseContextHandler.setName(infoFromToken.getName());
        BaseContextHandler.setUserID(infoFromToken.getId());
        return super.preHandle(request, response, handler);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        BaseContextHandler.remove();
        super.afterCompletion(request, response, handler, ex);
    }

spring cloud gateway網關代碼

該框架中所有的請求都會走網關服務(ace-gatev2),通過網關,來驗證token是否過期異常,驗證token是否不存在,驗證token是否有權限進行服務。

下面是核心代碼:

@Override
    public Mono<Void> filter(ServerWebExchange serverWebExchange, GatewayFilterChain gatewayFilterChain) {
        log.info("check token and user permission....");
        LinkedHashSet requiredAttribute = serverWebExchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
        ServerHttpRequest request = serverWebExchange.getRequest();
        String requestUri = request.getPath().pathWithinApplication().value();
        if (requiredAttribute != null) {
            Iterator<URI> iterator = requiredAttribute.iterator();
            while (iterator.hasNext()){
                URI next = iterator.next();
                if(next.getPath().startsWith(GATE_WAY_PREFIX)){
                    requestUri = next.getPath().substring(GATE_WAY_PREFIX.length());
                }
            }
        }
        final String method = request.getMethod().toString();
        BaseContextHandler.setToken(null);
        ServerHttpRequest.Builder mutate = request.mutate();
        // 不進行攔截的地址
        if (isStartWith(requestUri)) {
            ServerHttpRequest build = mutate.build();
            return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build());
        }
        IJWTInfo user = null;
        try {
            user = getJWTUser(request, mutate);
        } catch (Exception e) {
            log.error("用戶Token過期異常", e);
            return getVoidMono(serverWebExchange, new TokenForbiddenResponse("User Token Forbidden or Expired!"));
        }
        List<PermissionInfo> permissionIfs = userService.getAllPermissionInfo();
        // 判斷資源是否啓用權限約束
        Stream<PermissionInfo> stream = getPermissionIfs(requestUri, method, permissionIfs);
        List<PermissionInfo> result = stream.collect(Collectors.toList());
        PermissionInfo[] permissions = result.toArray(new PermissionInfo[]{});
        if (permissions.length > 0) {
            if (checkUserPermission(permissions, serverWebExchange, user)) {
                return getVoidMono(serverWebExchange, new TokenForbiddenResponse("User Forbidden!Does not has Permission!"));
            }
        }
        // 申請客戶端密鑰頭
        mutate.header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken());
        ServerHttpRequest build = mutate.build();
        return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build());

    }

1543848104059

cloud admin總結

總的來說,鑑權和網關模塊就說完了。作者代碼構思極其精妙,使用在大型的權限系統中,可以巧妙的減少耦合性,讓服務鑑權粒度細化,方便管理。

結束

此片完了~ 想要了解更多精彩新姿勢?
請訪問我的個人博客

本篇爲原創內容,已在個人博客率先發表,隨後看心情可能會在CSDN,segmentfault,掘金,簡書,開源中國同步發出。如有雷同,緣分呢兄弟。趕快加個好友,咱們兩個想個號碼, 買個彩票,先掙他個幾百萬😝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章