感性認識JWT

很久沒寫博客了,由於最近公司要求我學spring cloud,早點將之前軟件遷移到新的架構上。因此我那個拼命的學吶,老是圖快,不少關鍵的筆記沒有作好記錄,如今又遺忘了不少關鍵的技術點,極其罪惡!javascript

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

想要解鎖更多新姿式?請訪問個人博客git

常見的認證機制

今天我麼聊一聊JWT。github

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

請聽我慢慢訴說這歷史!redis

最原始的辦法--HTTP BASIC AUTH

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

Session和cookie

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

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

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,掘金,簡書,開源中國同步發出。若有雷同,緣分呢兄弟。趕快加個好友,我們兩個想個號碼, 買個彩票,先掙他個幾百萬😝

相關文章
相關標籤/搜索