SpringCloud(四) gateway + JWT進行權限管理

jwt相關知識可以查看

https://blog.csdn.net/sinat_29774479/article/details/89884500

 1.引入依賴 關於jwt的操作我們採用jjwt

jjwt是一個Java對jwt的支持庫,我們使用這個庫來創建、解碼token

<dependency>
     <groupId>io.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.0</version>
</dependency>

2.結合jjwt,封裝一個JWTUtil類

package com.wl.gateway.util.jwt;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author LWong
 * @date 2019/12/24/024
 */
@Slf4j
public class JwtUtil {

    private static final String KEY = "022bdc63c3c5a45879ee6581508b9d03adfec4a4658c0ab3d722e50c91a351c42c231cf43bb8f86998202bd301ec52239a74fc0c9a9aeccce604743367c9646b";

    public static SecretKey generateKey() {
        byte[] encodedKey = Base64.decodeBase64(KEY);
        SecretKeySpec key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");

        return key;
    }

    public static String createJWT(String id, String issuer, String subject, long ttlMillis) {
        // 指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的時間
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        // 創建payload的私有聲明(根據特定的業務需要添加,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的)
        Map<String, Object> claims = new HashMap<>();
        claims.put("uid", "123456");
        claims.put("user_name", "admin");
        claims.put("nick_name", "X-rapido");

        SecretKey key = generateKey();
        // 這裏其實就是new一個JwtBuilder,設置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標準的聲明賦值之後,就是覆蓋了那些標準的聲明的
                .setClaims(claims)
                // 設置jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置爲一個不重複的值,主要用來作爲一次性token,從而回避重放攻擊。
                .setId(id)
                // iat: jwt的簽發時間
                .setIssuedAt(now)
                // issuer:jwt簽發人
                .setIssuer(issuer)
                // sub(Subject):代表這個JWT的主體,即它的所有人,這個是一個json格式的字符串,可以存放什麼userid,roldid之類的,作爲什麼用戶的唯一標誌。
                .setSubject(subject)
                // 設置簽名使用的簽名算法和簽名使用的祕鑰
                .signWith(signatureAlgorithm, key);
        // 設置過期時間
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    /**
     * 解密jwt
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) {
        //簽名祕鑰,和生成的簽名的祕鑰一模一樣
        SecretKey key = generateKey();
        Claims claims = Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(jwt).getBody();
        return claims;
    }

    /**
     * 檢查token
     *
     * @return
     */
    public static boolean checkToken(String jwtToken, ObjectMapper objectMapper) throws Exception {
        //TODO 根據自己的業務修改
        Claims claims = JwtUtil.parseJWT(jwtToken);
        String subject = claims.getSubject();
        JwtModel jwtModel = objectMapper.readValue(subject, JwtModel.class);

        //TODO 對jwt裏面的用戶信息做判斷

        //獲取token的過期時間,和當前時間作比較,如果小於當前時間,則token過期
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        Date expiration = claims.getExpiration();
        log.info("======== token的過期時間:" + df.format(expiration));
        return true;
    }
}

3.添加過濾器JwtTokenFilter 結合gateway進行攔截

package com.wl.gateway.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wl.gateway.dto.Resp;
import com.wl.gateway.util.jwt.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;


/**
 * @author LWong
 * @date 2019/12/24/024
 */
@Component
@ConfigurationProperties("org.my.jwt")
@Setter
@Getter
@Slf4j
public class JwtTokenFilter implements GlobalFilter, Ordered {

    private String[] skipAuthUrls;

    private ObjectMapper objectMapper;

    public JwtTokenFilter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    /**
     * 過濾器
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();

        //跳過不需要驗證的路徑
        if (null != skipAuthUrls && Arrays.asList(skipAuthUrls).contains(url)) {
            return chain.filter(exchange);
        }

        //獲取token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        ServerHttpResponse resp = exchange.getResponse();
        if (StringUtils.isBlank(token)) {
            //沒有token
            return authErro(resp, "請登陸");
        } else {
            //有token
            try {
                JwtUtil.checkToken(token, objectMapper);
                return chain.filter(exchange);
            } catch (ExpiredJwtException e) {
                log.error(e.getMessage(), e);
                if (e.getMessage().contains("Allowed clock skew")) {
                    return authErro(resp, "認證過期");
                } else {
                    return authErro(resp, "認證失敗");
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                return authErro(resp, "認證失敗");
            }
        }
    }

    /**
     * 認證錯誤輸出
     *
     * @param resp 響應對象
     * @param mess 錯誤信息
     * @return
     */
    private Mono<Void> authErro(ServerHttpResponse resp, String mess) {
        resp.setStatusCode(HttpStatus.UNAUTHORIZED);
        resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        Resp<String> returnData = new Resp<>(org.apache.http.HttpStatus.SC_UNAUTHORIZED, mess, mess);
        String returnStr = "";
        try {
            returnStr = objectMapper.writeValueAsString(returnData);
        } catch (JsonProcessingException e) {
            log.error(e.getMessage(), e);
        }
        DataBuffer buffer = resp.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
        return resp.writeWith(Flux.just(buffer));
    }

    @Override
    public int getOrder() {
        return -100;
    }
}


4.在配置文件中配置gateway的路由規則

###################################
#服務啓動端口的配置
###################################
server:
  port: 8899


spring:
  cloud:
#################################
#   gateway相關配置
#################################
    gateway:
      ################################
      # 配置允許跨域請求
      ################################
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "*"
            allowedMethods:
            - GET
            - POST
#    路由定義
      routes:

      - id: baidu
        uri: https://www.baidu.com
        predicates:
        - Path=/baidu/**
        filters:
        - StripPrefix=1



      - id: sina
        uri: https://www.sina.com.cn/
        predicates:
        - Path=/sina/**
        filters:
        - StripPrefix=1

org:
  my:
    jwt:
      #跳過認證的路由
      skip-auth-urls:
      - /baidu
      ############################################
      #   有效時長
      #     單位:d:天、h:小時、m:分鐘、s:秒
      ###########################################
      effective-time: 3m

5.編寫測試接口

這裏我自己封裝了一個註解用於解析jwt  要看源碼可以從文末的gittee地址上拉代碼

package com.wl.gateway.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wl.gateway.annotation.JWTCheck;
import com.wl.gateway.dto.Resp;
import com.wl.gateway.dto.UserDTO;
import com.wl.gateway.util.jwt.JwtModel;
import com.wl.gateway.util.jwt.JwtUtil;
import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.ArrayList;

/**
 * @author LWong
 * @date 2019/12/24/024
 */
@RestController
public class PersonController {
    private ObjectMapper objectMapper;

    @Value("${org.my.jwt.effective-time}")
    private String effectiveTime;

    public PersonController(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    /**
     * 登陸認證接口
     *
     * @param userDTO
     * @return
     */
    @PostMapping("/login")
    public Resp<String> login(@RequestBody UserDTO userDTO) throws Exception {
        ArrayList<String> roleIdList = new ArrayList<>(1);
        roleIdList.add("role_test_1");
        JwtModel jwtModel = new JwtModel("test", roleIdList);
        int effectivTimeInt = Integer.valueOf(effectiveTime.substring(0, effectiveTime.length() - 1));
        String effectivTimeUnit = effectiveTime.substring(effectiveTime.length() - 1, effectiveTime.length());
        String jwt = null;
        switch (effectivTimeUnit) {
            case "s": {
                //秒
                jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 1000L);
                break;
            }
            case "m": {
                //分鐘
                jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 60L * 1000L);
                break;
            }
            case "h": {
                //小時
                jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 60L * 60L * 1000L);
                break;
            }
            case "d": {
                //小時
                jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 24L * 60L * 60L * 1000L);
                break;
            }
        }
        return new Resp<String>(HttpStatus.SC_OK, "認證成功", jwt);
    }

    /**
     * 爲授權提示
     */
    @GetMapping("/unauthorized")
    public Resp<String> unauthorized() {
        return new Resp<String>(HttpStatus.SC_UNAUTHORIZED, "未認證,請重新登陸", null);
    }

    /**
     * jwt 檢查註解測試 測試
     *
     * @return
     */
    @GetMapping("/testJwtCheck")
    @JWTCheck
    public Resp<String> testJwtCheck(@RequestHeader("Authorization") String token, @RequestParam("name") @Valid String name) {

        return new Resp<String>(HttpStatus.SC_OK, "請求成功咯", "請求成功咯" + name);

    }

}

6.測試

1.未登錄跳轉sina 顯示需要登錄

2.登陸獲取token

3.登陸後成功跳轉sina

代碼地址:https://gitee.com/wishlucky/gateway.git

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