感性認識JWT

tengshe789發表於2018-12-05

好久沒寫部落格了,因為最近公司要求我學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,掘金,簡書,開源中國同步發出。如有雷同,緣分呢兄弟。趕快加個好友,我們們兩個想個號碼, 買個彩票,先掙他個幾百萬?

相關文章