分布式系統--感性認識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("用戶不存在或賬戶密碼錯誤!");
    }

下圖是詳細邏輯圖:

[圖片上傳失敗...(image-153ab0-1543986254420)]

鑒權中心客戶端代碼

入口

作者寫了個注解的入口,使用@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,掘金,簡書,開源中國同步發出。如有雷同,緣分呢兄弟。趕快加個好友,咱們兩個想個號碼, 買個彩票,先掙他個幾百萬??

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容