暢購商城(八):微服務網關和JWT令牌


好好學習,天天向上


本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star,更多文章請前往:目錄導航

微服務網關

介紹

網關是介於用戶和微服務之前的中間層。說白了,網關就像是小區的保安,無論你想到小區的哪一戶人家去,你都得先通過小區的大門。所以,小區的保安可以做人員統計,還可以控制某個時間段進去小區的人數,限制進入小區的資格等。保證了小區業主們的安全。微服務網關同樣起着這些作用。

為什么要有微服務網關

不同的微服務一般會有不同的網絡地址,而外部客戶端可能需要調用多個服務的接口才能完成一個業務需求,如果讓客戶端直接與各個微服務通信,會有以下的問題:

  • 客戶端會多次請求不同的微服務,增加了客戶端的復雜性
  • 存在跨域請求,在一定場景下處理相對復雜
  • 認證復雜,每個服務都需要獨立認證
  • 難以重構,隨着項目的迭代,可能需要重新划分微服務。例如,可能將多個服務合並成一個或者將一個服務拆分成多個。如果客戶端直接與微服務通信,那么重構將會很難實施
  • 某些微服務可能使用了防火牆 / 瀏覽器不友好的協議,直接訪問會有一定的困難

那么有了微服務網關之后,這些問題就可以得到解決。它有着以下優點。

  • 安全 ,只有網關系統對外進行暴露,微服務可以隱藏在內網,通過防火牆保護。
  • 易於監控。可以在網關收集監控數據並將其推送到外部系統進行分析。
  • 易於認證。可以在網關上進行認證,然后再將請求轉發到后端的微服務,而無須在每個微服務中進行認證。
  • 減少了客戶端與各個微服務之間的交互次數
  • 易於統一授權。

總結:微服務網關就是一個系統,通過暴露該微服務網關系統,方便我們進行相關的鑒權,安全控制,日志統一處理,易於監控的相關功能

網關微服務

微服務搭建

一個項目中可能會用到不止一個網關,所以我們將網關微服務放在changgou-gateway父工程下。現在我們創建一個名為changou-gateway-web的微服務。有些依賴是所有網關微服務都要用到的,所以將這些依賴放在父工程下:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

啟動類和配置文件不能少,啟動類就不貼了,配置文件如下👇

spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有請求
            allowedOrigins: "*" #跨域處理 允許所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
server:
  port: 8001
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
management:
  endpoint:
    gateway:
      enabled: true
    web:
      exposure:
        include: true

網關過濾配置

  • Host 路由
# 用戶請求的域名規格配置,所有以robod.changgou.com開頭的請求都將被路由到http://localhost:18081微服務
# 例如  http://robod.changgou.com:8001/brand ——> http://localhost:18081/brand
# 但是首先得在hosts文件中配置一下:   127.0.0.1  robod.changgou.com
spring:
  cloud:
    gateway:
      routes:
        - id: changgou_goods_route   # 唯一標識符
          uri: http://localhost:18081
          predicates:
            - Host=robod.changgou.com**
  • - Path 路徑匹配過濾配置
# 所有以/brand開頭的請求都將路由到http://localhost:18081
# 例如  localhost:8001/brand  ——>  localhost:18081/brand
spring:
  cloud:
    gateway:
      routes:
        - id: changgou_goods_route
          uri: http://localhost:18081
          predicates:
            - Path=/brand/**
  • PrefixPath 過濾配置
# 自動加上某個前綴,用戶請求/** ——>/brand/**
# 例如  localhost:8001/111  ——>  localhost:8001/brand/111  ——>  localhost:18081/brand/111
spring:
  cloud:
    gateway:
      routes:
        - id: changgou_goods_route
          uri: http://localhost:18081
          predicates:
            - Path=/**
          filters:
            - PrefixPath=/brand
  • StripPrefix 過濾配置
# 將請求路徑中的前n個路徑去掉,請求路徑以/區分,一個/代表一個路徑
# 例如  localhost:8001/api/brand/111  ——>  localhost:8001/brand/111  ——>  localhost:18081/brand/111
spring:
  cloud:
    gateway:
      routes:
        - id: changgou_goods_route
          uri: http://localhost:18081
          predicates:
            - Path=/**
          filters:
            - StripPrefix=1
  • LoadBalancerClient 路由過濾器(客戶端負載均衡)
# 使用LoadBalancerClient實現負載均衡,后面的goods是微服務的名稱,主要應用於集群環境
# 比如現在有5台服務器都是goods微服務,網關就會自動將請求發送給不同的服務器達到負載均衡的目的
spring:
  cloud:
    gateway:
      routes:
        - id: changgou_goods_route
          uri: lb://goods

網關限流

當訪問量多大的時候,我們的服務就可能會掛掉,所以我們需要對每個微服務進行限流,但是這樣比較麻煩。有了網關之后,我們可以對網關進行限流,因為所有的請求必須通過網關才能到達微服務,這樣比較方便。

令牌桶算法

常見的限流算法有計數器,漏斗,令牌桶算法。令牌桶算法有以下幾個特點:

  • 所有的請求在處理之前都需要拿到一個可用的令牌才會被處理;
  • 根據限流大小,設置按照一定的速率往桶里添加令牌;
  • 桶設置最大的放置令牌限制,當桶滿時、新添加的令牌就被丟棄或者拒絕;
  • 請求達到后首先要獲取令牌桶中的令牌,拿着令牌才可以進行其他的業務邏輯,處理完業務邏輯之后,將令牌直接刪除;
  • 令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之后將不會刪除令牌,以此保證足夠的限流

使用令牌桶進行請求次數限流

spring cloud gateway 默認使用redis的RateLimter限流算法來實現。首先在changgou-gateway-web中添加Redis的依賴:

<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

然后我們需要有限流的Key,這里用IP來當作限流的Key,限制某一個IP在一定時間段的訪問次數,在啟動類中定義一個Bean用於獲取key

@Bean(name = "ipKeyResolver")
public KeyResolver userKeyResolver() {
    return exchange -> {
        String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName();
        return Mono.just(ip);
    };
}

我這里使用了Lamda去簡化書寫。接下來還得在配置文件中配置一下

spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      routes:
          filters:
            - name: RequestRateLimiter #請求數限流 名字不能隨便寫 ,使用默認的factory
              args:
                # 用戶身份唯一標識符
                key-resolver: "#{@ipKeyResolver}"
                # 允許用戶每秒執行多少請求,而不會丟棄任何請求。這是令牌桶填充的速率
                redis-rate-limiter.replenishRate: 1
                # 令牌桶的容量,允許在一秒鍾內完成的最大請求數
                redis-rate-limiter.burstCapacity: 1

既然是使用redis的RateLimter限流算法,那么Redis的配置自然不能少。

#Redis配置
spring:
  application:
  redis:
    host: 192.168.31.200
    port: 6379

限流的配置就配置好了,現在如果在1秒內請求超過1次的話就會被拒絕。

JWT

在實現用戶登錄功能之前,我們先來介紹一下JWT(JSON Web Token)。是一種用於通信雙方之間傳遞安全信息的簡潔的、URL安全的表述性聲明規范。

JWT的構成

一個JWT實際上就是一個字符串,它由三部分組成,頭部、載荷與簽名。為了能夠直觀的看到JWT的結構,我畫了一張思維導圖:

最終生成的JWT令牌就是下面這樣,有三部分,用 . 分隔。

base64UrlEncode(JWT 頭)+"."+base64UrlEncode(載荷)+"."+HMACSHA256(base64UrlEncode(JWT 頭) + "." + base64UrlEncode(有效載荷),密鑰)

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT的使用

  • 導入依賴:
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
  • 創建Token
public String createToken() {
    JwtBuilder builder = Jwts.builder()
            .setId("test1")
            .setSubject("Robod")
            .setAudience("馬化騰")
            .setIssuedAt(new Date());
            .signWith(SignatureAlgorithm.HS256,"robod666");
    Map<String,Object> map = new HashMap<>();
    map.put("ha","哈哈哈");
    builder.addClaims(map);
    return builder.compact();
}
  • 解析Token
public String parseToken() {
    String compactJwt="eyJhbGciOiJIUzI1NiJ9" +
            ".eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9" +
            ".RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4";
    Claims claims = Jwts.parser().
            setSigningKey("robod666").
            parseClaimsJws(compactJwt).
            getBody();
    return claims.toString();
}

用戶登錄與鑒權

介紹了JWT之后,我們就來用JWT實現用戶登錄與鑒權。流程如下:

首先我們需要准備一個JWT的工具類,JWTUtil,放在changgou-common下:

public class JwtUtil {
    //默認有效期,一個小時
    public static final Long JWT_TTL = 3600000L;

    //Jwt令牌信息
    public static final String JWT_KEY = "RobodLee";

    //密鑰
    public static SecretKey secretKey = generalKey();

    //生成令牌
    public static String createJWT(String id, String subject, Long ttlMillis) {
        //指定算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        //當前系統時間
        long nowMillis = System.currentTimeMillis();
        //令牌簽發時間
        Date now = new Date(nowMillis);

        //如果令牌有效期為null,則默認設置有效期1小時
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }

        //令牌過期時間設置
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);

        //封裝Jwt令牌信息
        JwtBuilder builder = Jwts.builder()
                .setId(id)                    //唯一的ID
                .setSubject(subject)          // 主題  可以是JSON數據
                .setIssuer("robod")          // 簽發者
                .setIssuedAt(now)             // 簽發時間
                .signWith(signatureAlgorithm,secretKey) // 簽名算法以及密匙
                .setExpiration(expDate);      // 設置過期時間
        return builder.compact();
    }

   	//生成加密 secretKey
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes());
        return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    }

	//解析令牌
    public static Claims parseJWT(String jwt) throws Exception {
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}

我發現資料提供的代碼中每次調用generalKey()、parseJWT()方法的時候都去調用generalKey()方法去生成SecretKey,但是generalKey()方法內容是不變的,所以可以將SecretKey單獨提取出來,這樣就不用每次都調用generalKey()去生成了。

然后創建一個用戶微服務changou-service-user在UserController中編寫登錄邏輯👇

@RequestMapping("/login")
public Result<String> login(String username, String password, HttpServletResponse response) {
    User user = userService.findById(username);
    if (BCrypt.checkpw(password,user.getPassword())){
        Map<String,Object> tokenInfo = new HashMap<>(4);
        tokenInfo.put("role","USER");
        tokenInfo.put("success","SUCCESS");
        tokenInfo.put("username",username);
        String token = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(tokenInfo), null);
        Cookie cookie = new Cookie("Authorization",token);
        cookie.setDomain("localhost");
        cookie.setPath("/");
        response.addCookie(cookie);
        return new Result<>(true,StatusCode.OK,"登錄成功",token);
    }
    return new Result<>(false,StatusCode.LOGIN_ERROR,"登錄失敗");
}

在這段代碼中,調用Service層從數據庫中查出對應的User,然后比對password,看密碼是否正確。如果正確,就調用JwtUtil創建一個JWT令牌,並放入一些簡單的信息。然后將JWT令牌存入Cookie中,並返回給前端。如果登錄失敗就返回登錄失敗的信息。

然后就是在網關微服務中添加相應的邏輯了,在changgou-gateway-web中配置一下,配置一下User微服務的路由。

spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      routes:
        - id: changgou_user_route   # 唯一標識符
          uri: http://localhost:18088
          predicates:
            - Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**
          filters:
            - StripPrefix=1

再添加一個過濾器:

@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {

    private static final String AUTHORIZE_TOKEN = "Authorization";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String token;
        //從頭中獲取Token
        token = request.getHeaders().getFirst(AUTHORIZE_TOKEN);
        //請求頭中沒有Token就從參數中獲取
        if (StringUtils.isEmpty(token)){
            token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
        }
        //參數中再沒有Token就從Cookie中獲取
        if (StringUtils.isEmpty(token)){
            HttpCookie cookie = request.getCookies().getFirst(AUTHORIZE_TOKEN);
            if (cookie!=null){
                token = cookie.getValue();
            }
        }
        //還是沒有Token就攔截
        if (StringUtils.isEmpty(token)){
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        //Token不為空就校驗Token
        try {
            JwtUtil.parseJWT(token);
        } catch (Exception e) {
            //報異常說明Token是錯誤的,攔截
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

這段代碼就是分別從Header,參數,Cookie中看有沒有Token信息,沒有的話就說明用戶沒有權限,攔截下來。有Token的話就解析一下Token有沒有錯,錯誤就攔截下來。如果都沒有問題的話就放行,將請求路由到用戶微服務中。

這是沒有Token的情況下👆

當我們登陸后就會獲取到Token👇

當我們攜帶着token去訪問就沒有問題了👇

小結

這篇文章中,首先介紹了微服務網關及網關的搭建及過濾配置和限流配置。然后介紹了JWT,最后使用了JWT去實現了用戶登錄與鑒權的操作。

如果我的文章對你有些幫助,不要忘了點贊收藏轉發關注。要是有什么好的意見歡迎在下方留言。讓我們下期再見!
微信公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM