使用Gateway+JWT實現網關鑑權

JWT

JWT(JSON Web Token), 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準((RFC 7519).該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登陸(SSO)場景。JWT的聲明通常被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也能夠增長一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。javascript

JWT的組成

  • Header(頭部) —— base64編碼的Json字符串
  • Payload(載荷) —— base64編碼的Json字符串
  • Signature(簽名)—— 使用指定算法,經過Header和Payload加鹽計算的字符串

header前端

jwt的頭部承載兩部分信息:java

{
  'typ': 'JWT', //聲明類型
  'alg': 'RS256' //簽名加密的算法
}

而後將頭部進行base64加密(該加密是能夠對稱解密的),構成了第一部分.git

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

playloadgithub

載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分:web

  • 標準中註冊的聲明 (==建議但不強制使用==) :
{ "iss": "JWT Builder", //jwt簽發者
  "iat": 1416797419, // jwt的簽發時間
  "exp": 1448333419,  //jwt的過時時間,這個過時時間必需要大於簽發時間
  "aud": "www.bilibili.com", //接收jwt的一方
  "sub": "1837307557@qq.com",  //jwt所面向的用戶
  "GivenName": "Levin", 
  "Surname": "Levin", 
  "Email": "1837307557@qq.com", 
  "Role": [ "ADMIN", "MEMBER" ],
  "nbf" : 1416797420 //定義在什麼時間以前,該jwt都是不可用的,
  "jti" : "jwt的惟一身份標識,主要用來做爲一次性token,從而回避重放攻擊"
}
  • ==公共==的聲明 :
    公共的聲明能夠添加任何的信息,通常添加用戶的相關信息或其餘業務須要的必要信息.但不建議添加敏感信息,由於該部分在客戶端可解密.
  • ==私有==的聲明 :
    私有聲明是提供者和消費者所共同定義的聲明,通常不建議存放敏感信息,由於base64是對稱解密的,意味着該部分信息能夠歸類爲明文信息。

定義一個payload:redis

// 包括須要傳遞的用戶信息;
{ "iss": "Online JWT Builder", 
  "iat": 1416797419, 
  "exp": 1448333419, 
  "aud": "www.gusibi.com", 
  "sub": "uid", 
  "nickname": "goodspeed", 
  "username": "goodspeed", 
  "scopes": [ "admin", "user" ] 
}

而後將其進行base64加密,獲得Jwt的第二部分。算法

eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVk

signaturespring

jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:數據庫

// 根據頭部alg算法與私有祕鑰進行加密獲得的簽名字符串;
// 這一段是最重要的敏感信息,只能在服務端解密;
HMACSHA256(  
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    SECREATE_KEY
)

這個部分須要base64加密後的header和base64加密後的payload使用 "." 鏈接組成的字符串,而後經過header中聲明的加密方式進行加鹽secret組合加密,而後就構成了jwt的第三部分。

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

將這三部分用"."鏈接成一個完整的字符串,構成了最終的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,因此,它就是你服務端的私鑰,在任何場景都不該該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是能夠自我簽發jwt了。

加密及驗證過程

加密:

生成頭JSON,荷載(playload) JSON

將頭JSON Base64編碼 + 荷載JSON Base64編碼 +secret 三者拼接進行加密獲得簽名

JSON Base64編碼 + 荷載JSON Base64編碼 + 簽名 三者經過 "." 相鏈接

一條 hhh.ppp.sss 格式的JWT 即生成

解密:

取得Jwt hhh.ppp.sss 格式字符,經過 "." 將字符分爲三段

對第一段進行Base64解析獲得header json,獲取加密算法類型

將第一段Header JSON Base64編碼 + 第二段 荷載JSON Base64編碼 + secret採用相應的加密算法加密獲得簽名

將步驟三獲得的簽名與步驟一分紅的第三段也就是客戶端傳入的簽名進行匹配,匹配成功說明該jwt爲server自身產出;

獲取playload內信息,經過信息能夠作鑑權操做;

成功訪問;

經過這些步驟,保證了第三方沒法修改jwt,jwt只能自產自銷,在分佈式環境下服務接收到合法的jwt即可知是本系統內自身或其餘服務發出的jwt,該用戶是合法的;

X509

X.509是常見通用的證書格式。全部的證書都符合爲Public Key Infrastructure (PKI) 制定的 ITU-T X509 國際標準。X.509是國際電信聯盟-電信(ITU-T)部分標準和國際標準化組織(ISO)的證書格式標準。做爲ITU-ISO目錄服務系列標準的一部分,X.509是定義了公鑰證書結構的基本標準。1988年首次發佈,1993年和1996年兩次修訂。當前使用的版本是X.509 V3,它加入了擴展字段支持,這極大地增進了證書的靈活性。X.509 V3證書包括一組按預約義順序排列的強制字段,還有可選擴展字段,即便在強制字段中,X.509證書也容許很大的靈活性,由於它爲大多數字段提供了多種編碼方案.

JWT 最多見的幾種簽名算法HS256(HMAC-SHA256) 、RS256(RSA-SHA256) 還有 ES256(ECDSA-SHA256)。

這三種算法都是一種消息簽名算法,獲得的都只是一段沒法還原的簽名。區別在於消息簽名簽名驗證須要的 「key」不一樣。

  1. HS256 使用同一個「secret_key」進行簽名與驗證。一旦 secret_key 泄漏,就毫無安全性可言了。

    • 所以 HS256 只適合集中式認證,簽名和驗證都必須由可信方進行。
  2. RS256 是使用 RSA 私鑰進行簽名,使用 RSA 公鑰進行驗證。公鑰即便泄漏也毫無影響,只要確保私鑰安全就行。

    • RS256 能夠將驗證委託給其餘應用,只要將公鑰給他們就行。
  3. ES256 和 RS256 同樣,都使用私鑰簽名,公鑰驗證。算法速度上差距也不大,可是它的簽名長度相對短不少(省流量),而且算法強度和 RS256 差很少。

對於單體應用而言,HS256 和 RS256 的安全性沒有多大差異。
而對於須要進行多方驗證的微服務架構而言,顯然 RS256/ES256 安全性更高。
只有 user 微服務須要用 RSA 私鑰生成 JWT,其餘微服務使用公鑰便可進行簽名驗證,私鑰獲得了更好的保護。

無狀態登陸

微服務集羣中的每一個服務, 對外提供的都是Rest風格的接口, 而Rest風格的一個最重要的規範就是: 服務的無狀態性, 即:

  • 服務端不保存任何客戶端請求者狀態信息
  • 客戶端的每次請求必須具有自描述信息, 經過這些信息識別客戶端身份

優勢:

  • 客戶端請求不依賴服務端的信息, 任何屢次請求不須要必須訪問到同一臺服務
  • 服務端的集羣和狀態對客戶端透明
  • 服務端能夠任意的遷移和伸縮
  • 減少服務端存儲壓力

JJWT

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

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

配合joda-time處理過時時間

<dependency>
     <groupId>joda-time</groupId>
     <artifactId>joda-time</artifactId>
     <version>2.9.6</version>
 </dependency>

生成JWT

客戶端發送 POST 請求到服務器,提交登陸處理的Controller層
調用認證服務進行用戶名密碼認證,若是認證經過,返回完整的用戶信息及對應權限信息
利用 JJWT 對用戶、權限信息、祕鑰構建Token
返回構建好的Token
下面是關鍵代碼, 文章後面有所有的工具類

/**
     * 私鑰加密生成token
     * @param user 載荷數據
     * @param privateKey 私鑰字節數組
     * @param expireMinutes 過時時間,單位分鐘
     * @return
     */
    public static String generateToken(ShopUser user, byte[] privateKey, Integer expireMinutes) throws Exception{

        return Jwts.builder()
                .claim(JWTConstants.JWT_KEY_ID, user.getId())
                .claim(JWTConstants.JWT_KEY_USER_NAME, user.getUserName())
                .claim(JWTConstants.JWT_KEY_ROLE, user.getRole())
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey))
                .compact();
    }

Jwts.builder() 返回了一個 DefaultJwtBuilder()

DefaultJwtBuilder屬性

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private Header header; //頭部
    private Claims claims; //聲明
    private String payload; //載荷
    private SignatureAlgorithm algorithm; //簽名算法
    private Key key; //簽名key
    private byte[] keyBytes; //簽名key的字節數組
    private CompressionCodec compressionCodec; //壓縮算法

DefaultJwtBuilder包含了一些Header和Payload的一些經常使用設置方法

解析&驗證JWT

使用私鑰加密的jwt, 公鑰和私鑰均可以解密
使用公鑰加密的jwt, 只有私鑰能夠解密
客戶端向服務器請求,服務端讀取請求頭信息(request.header)獲取Token
若是找到Token信息,則根據配置文件中的簽名加密祕鑰,調用JJWT Lib對Token信息進行解密和解碼;
完成解碼並驗證簽名經過後,對Token中的exp、nbf、aud等信息進行驗證;
所有經過後,根據獲取的用戶的角色權限信息,進行對請求的資源的權限邏輯判斷;
若是權限邏輯判斷經過則經過Response對象返回;不然則返回HTTP 401;

/**
 * 公鑰解析token
 * @param token 用戶請求中的token
 * @param publicKey 公鑰字節數組
 * @return
 * @throws Exception
 */
private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception {
    return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey))
            .parseClaimsJws(token);
}

Jwts.parser() 返回了DefaultJwtParser 對象

DefaultJwtParser() 屬性

//don't need millis since JWT date fields are only second granularity:
private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private static final int MILLISECONDS_PER_SECOND = 1000;

private ObjectMapper objectMapper = new ObjectMapper();

private byte[] keyBytes; //簽名key字節數組
private Key key; //簽名key
private SigningKeyResolver signingKeyResolver; //簽名Key解析器
private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); //壓縮解析器
Claims expectedClaims = new DefaultClaims(); //指望Claims
private Clock clock = DefaultClock.INSTANCE; //時間工具實例
private long allowedClockSkewMillis = 0;  //容許的時間偏移量

parse() 方法傳入一個JWT字符串,返回一個JWT對象

解析過程

  1. 檢查: 以分隔符" . "切分JWT的三個部分。若是分隔符數量錯誤或者載荷爲空,將拋出 MalformedJwtException 異常。
  2. 頭部解析: 將頭部原始Json鍵值存入map。根據是否加密建立不一樣的頭部對象。jjwt的DefaultCompressionCodecResolver根據頭部信息的壓縮算法信息,添加不一樣的壓縮解碼器。
  3. 載荷解析: 先對載荷進行Base64解碼,若是有通過壓縮,那麼在解碼後再進行解壓縮。此時將值賦予payload。若是載荷是json形式,將json鍵值讀入map,將值賦予claims 。

    if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { 
        //likely to be json, parse it:
        Map<String, Object> claimsMap = readValue(payload);
        claims = new DefaultClaims(claimsMap);
    }
  4. 簽名解析: 若是存在簽名部分,則對簽名進行解析。

    • 首先根據頭部的簽名算法信息,獲取對應的算法。
      若是簽名部分不爲空,可是簽名算法爲null或者'none',將拋出MalformedJwtException異常。
    • 獲取簽名key
    • 可能的異常

      • 若是同時設置了key屬性和keyBytes屬性,parser不知道該使用哪一個值去做爲簽名key解析,將拋出異常。
      • 若是key屬性和keyBytes屬性只存在一個,可是設置了signingKeyResolver,也不知道該去解析前者仍是使用後者,將拋出異常。
      • 若是設置了key(setSigningKey() 方法)則直接使用生成Key對象。若是兩種形式( key和keyBytes )都沒有設置,則使用SigningKeyResolver(經過setSigningKeyResolver()方法設置)獲取key, 固然,獲取key爲null會拋出異常
    • 建立簽名校驗器
      JJWT實現了一個默認的簽名校驗器DefaultJwtSignatureValidator。該類提供了兩個構造方法,外部調用的構造方法傳入算法和簽名key,再加上一個DefaultSignatureValidatorFactory工廠實例傳遞調用另外一個構造函數,以便工廠根據不一樣算法建立不一樣類型的Validator。

      public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) {
          this(DefaultSignatureValidatorFactory.INSTANCE, alg, key);
      }
      
      public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key) {
          Assert.notNull(factory, "SignerFactory argument cannot be null.");
          this.signatureValidator = factory.createSignatureValidator(alg, key);
      }
    • 比對驗證
      根據頭部和載荷從新計算簽名並比對。
      若是不匹配,拋出SignatureException異常
    • 時間校驗
      根據當前時間和時間偏移判斷是否過時。
      根據當前時間和時間偏移判斷是夠未到可接收時間
    • Claims參數校驗
      即校驗parser前面設置的因此require部分。校驗完成後,以header,claims或者payload建立DefaultJwt對象返回
    • 至此,已經完成JWT Token的校驗過程。校驗經過後返回JWT對象。

工具類

JWTUtils

import com.uni.entity.ShopUser;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.*;
import java.security.PrivateKey;
import java.security.PublicKey;


/**
 *  JWT 的工具類:包含了建立和解碼的工具
 */
@Slf4j
public class JWTUtils {

    /**
     * 私鑰加密token
     * @param user 載荷數據
     * @param privateKey 私鑰
     * @param expireMinutes 過時時間,單位分鐘
     * @return
     */
    public static String generateToken(ShopUser user, PrivateKey privateKey, Integer expireMinutes) throws Exception{

        return Jwts.builder()
                .claim(JWTConstants.JWT_KEY_ID, user.getId())
                .claim(JWTConstants.JWT_KEY_USER_NAME, user.getUserName())
                .claim(JWTConstants.JWT_KEY_ROLE, user.getRole())
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();
    }

    /**
     * 私鑰加密token
     * @param user 載荷數據
     * @param privateKey 私鑰字節數組
     * @param expireMinutes 過時時間,單位分鐘
     * @return
     */
    public static String generateToken(ShopUser user, byte[] privateKey, Integer expireMinutes) throws Exception{

        return Jwts.builder()
                .claim(JWTConstants.JWT_KEY_ID, user.getId())
                .claim(JWTConstants.JWT_KEY_USER_NAME, user.getUserName())
                .claim(JWTConstants.JWT_KEY_ROLE, user.getRole())
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey))
                .compact();
    }

    /**
     * 使用公鑰解析token
     * @param token 用戶請求中的token
     * @param publicKey 公鑰對象
     * @return
     */
    public static Jws<Claims> parserToken(String token, PublicKey publicKey){
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }

    /**
     * 公鑰解析token
     * @param token 用戶請求中的token
     * @param publicKey 公鑰字節數組
     * @return
     * @throws Exception
     */
    private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception {
        return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey))
                .parseClaimsJws(token);
    }

    /**
     * 獲取token中的用戶信息
     * @param token 用戶請求中的令牌
     * @param publicKey 公鑰
     * @return 用戶信息
     * @throws Exception
     */
    public static ShopUser getInfoFromToken(String token, PublicKey publicKey) throws Exception {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();

        Long user_id = (Long) body.get(JWTConstants.JWT_KEY_ID);
        String user_name = (String) body.get(JWTConstants.JWT_KEY_USER_NAME);
        Integer user_role = (Integer) body.get(JWTConstants.JWT_KEY_ROLE);
        return new ShopUser(user_id, user_name, user_role);
    }

    /**
     * 獲取token中的用戶信息
     * @param token 用戶請求中的token
     * @param publicKey 公鑰字節數組
     * @return 用戶信息
     * @throws Exception
     */
    public static ShopUser getInfoFromToken(String token, byte[] publicKey) throws Exception {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();

        Long user_id = (Long) body.get(JWTConstants.JWT_KEY_ID);
        String user_name = (String) body.get(JWTConstants.JWT_KEY_USER_NAME);
        Integer user_role = (Integer) body.get(JWTConstants.JWT_KEY_ROLE);
        return new ShopUser(user_id, user_name, user_role);
    }

    /* 測試解析token */
    public static void main(String[] args) throws Exception {
        PublicKey publicKey = RsaUtils.getPublicKey("D://rsa//rsa.pub");
        Jws<Claims> claimsJws = parserToken("eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoxMjczOTEyMTE1MDI3MTE2MDMyLCJ1c2VyX3JvbGUiOjAsImV4cCI6MTU5MzMxODM2OH0.FqXgDP6b3qoTrAXteCHxQ2IUnryh_7XfeUHPTW8bXiLpXVDn1zigBJTGcxFhivcy0aIACBs32i0ynbBc5DUli6chesvIE7HfbAl9IiBj0D6Ujde-HnQdHcrzjPt783fy-5Voj4HJZWHrAH9SCPkKqs6VUUR6Ba8QHJeoJtkmUXg", publicKey);
        System.out.println(claimsJws.getSignature());
        System.out.println(claimsJws.toString());
    }

}

RsaUtils

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * rsa非對稱加密
 * 私鑰加密,解密須要公鑰
 */
public class RsaUtils {

    /**
     * 從文件中讀取公鑰
     * @param filename 公鑰保存路徑,相對於classpath
     * @return 公鑰對象
     * @throws Exception
     */
    public static PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     *  獲取公鑰
     * X.509是定義了公鑰證書結構的基本標準
     * @param bytes 公鑰的字節形式
     * @return 公鑰對象
     * @throws Exception
     */
    public static PublicKey getPublicKey(byte[] bytes) throws Exception {
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 從文件中讀取私鑰
     * @param filename 私鑰保存路徑,相對於classpath
     * @return 私鑰對象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 獲取私鑰
     * @param bytes 私鑰的字節形式
     * @return 私鑰對象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根據密文,生成rsa公鑰和私鑰,並寫入指定文件
     * @param publicKeyFilename 公鑰文件路徑
     * @param privateKeyFilename 私鑰文件路徑
     * @param secret 生成密鑰的密文
     * @throws Exception
     */
    public static void generateKey(String publicKeyFilename,
                                   String privateKeyFilename, String secret) throws Exception {

        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(1024, secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        //獲取公鑰並寫出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        writeFile(publicKeyFilename, publicKeyBytes);
        //獲取私鑰並寫出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String filename) throws Exception {
        return Files.readAllBytes(new File(filename).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException{
        File dest = new File(destPath);
        if (!dest.exists()){
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }

    /* 測試公私鑰獲取 */
    public static void main(String[] args) throws Exception {
        //公私鑰路徑
        String pubKeyPath = "D:\\rsa\\rsa.pub";
        String priKeyPath = "D:\\rsa\\rsa.pri";

        //明文
        String secret = "sc@Login(Auth}*^31)&czxy%";
        //RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);

        /* 解密 */
        PublicKey publicKey = RsaUtils.getPublicKey(pubKeyPath);
        System.out.println("公鑰: " + publicKey);
        PrivateKey privateKey = RsaUtils.getPrivateKey(priKeyPath);
        System.out.println("私鑰: " + privateKey);
        //簽名驗證器
        DefaultJwtSignatureValidator validator = new DefaultJwtSignatureValidator(SignatureAlgorithm.RS256, publicKey);
        boolean valid = validator.isValid("eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoxMjczOTEyMTE1MDI3MTE2MDMyLCJ1c2VyX3JvbGUiOjAsImV4cCI6MTU5MzMxODM2OH0", "FqXgDP6b3qoTrAXteCHxQ2IUnryh_7XfeUHPTW8bXiLpXVDn1zigBJTGcxFhivcy0aIACBs32i0ynbBc5DUli6chesvIE7HfbAl9IiBj0D6Ujde-HnQdHcrzjPt783fy-5Voj4HJZWHrAH9SCPkKqs6VUUR6Ba8QHJeoJtkmUXg");
        System.out.println(valid);
    }
}

JWTProperties

package com.uni.config;

import com.uni.util.RsaUtils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import javax.annotation.PostConstruct;
import java.io.File;
import java.security.PrivateKey;
import java.security.PublicKey;

/**
 * 初始化公鑰和私鑰
 */
@Slf4j
@Data
@PropertySource("classpath:application.yml")
@ConfigurationProperties(prefix = "jwt")
@Configuration
public class JWTProperties {

    private String secret; // 密文

    private String pubKeyPath;// 公鑰

    private String priKeyPath;// 私鑰

    private Integer expire;// token過時時間

    private String[] skipAuthUrls; //跳過的url

    private PublicKey publicKey; // 公鑰

    private PrivateKey privateKey; // 私鑰

    //被@PostConstruct修飾的方法會在服務器加載Servlet的時候運行,而且只會被服務器調用一次
    @PostConstruct
    public void init() {
        try {
            log.info("公鑰地址: " + pubKeyPath);
            log.info("私鑰地址: " + priKeyPath);
            File pubKey = new File(pubKeyPath);
            File priKey = new File(priKeyPath);

            if (!pubKey.exists() || !priKey.exists()) {
                // 生成公鑰和私鑰並寫入文件
                RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);
            }
            // 獲取公鑰和私鑰
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
            this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
        } catch (Exception e) {
            log.error("初始化公鑰和私鑰失敗! " + e);
            throw new RuntimeException();
        }
    }
}

配置以下:

jwt:
  secret: sc@Login(Auth}*^31)&czxy% # 登陸校驗的明文
  pubKeyPath: D://rsa//rsa.pub # 公鑰地址
  priKeyPath: D://rsa//rsa.pri # 私 鑰地址
  expire: 30 # 過時時間,單位分鐘
  skipAuthUrls:
    - /auth/**
    - ...

JWTConstants

public class JWTConstants {

    public static final String JWT_HEADER_KEY = "Authorization";

    public static final String JWT_KEY_ID = "user_id";

    public static final String JWT_KEY_USER_NAME = "user_name";

    public static final String JWT_KEY_ROLE = "user_role";
}

JWTModel

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class JWTModel {

    private Long userId;

    private String userName;

    private String jwt;
}

用戶登陸

import com.uni.config.JWTProperties;
import com.uni.entity.Dto;
import com.uni.entity.ShopUser;
import com.uni.service.ShopUserService;
import com.uni.util.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;


@Slf4j
@RestController
@RequestMapping("/auth")
public class AuthAPI {

    @Autowired
    private ShopUserService shopUserService;

    @Autowired
    private JWTProperties jwtProperties;

    @PostMapping("/login")
    public Dto doLogin(@RequestBody ShopUser user){
        ShopUser result = null;
        // 驗證用戶明和密碼
        if (ObjectUtils.isNotEmpty(user)) {
             result = shopUserService.login(user);
        }
        if (ObjectUtils.isEmpty(result)){
            return DtoUtil.returnFail("用戶名或密碼錯誤", "401");
        }
        try {
            //生成token
            String token = JWTUtils.generateToken(
                    result, jwtProperties.getPrivateKey(), 30);
            return DtoUtil.returnSuccess("登陸成功",
                    new JWTModel(result.getId(), result.getUserName(), token));
        } catch (Exception e) {
            log.error("生成token失敗! ", e);
            return DtoUtil.returnFail("登陸失敗", "500");
        }
    }
}

網關鑑權

import com.uni.config.JWTProperties;
import com.uni.util.JWTConstants;
import com.uni.util.JWTUtils;
import com.uni.util.RsaUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.Minutes;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.Ordered;

/**
 * 請求鑑權過濾器
 */
@Slf4j
@Component
public class AccessGateWayFilter implements GlobalFilter, Ordered {

    private ObjectMapper objectMapper;

    @Autowired
    private JWTProperties jwtProperties;

    @Autowired
    private AntPathMatcher antPathMatcher; //路徑匹配器

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

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();

        //跳過不須要驗證的url
        for (String skip : jwtProperties.getSkipAuthUrls()) {
            if (antPathMatcher.match(skip, url))
                return chain.filter(exchange);
        }

        //獲取token
        String token = exchange.getRequest().getHeaders().getFirst(JWTConstants.JWT_HEADER_KEY);
        ServerHttpResponse response = exchange.getResponse();

        if (StringUtils.isBlank(token)){
            //沒有token
            return authError(response, "請登陸");
        } else {
            try {
                //解析token
                Jws<Claims> claims = JWTUtils.parserToken(token, jwtProperties.getPublicKey());
                DateTime now = DateTime.now();
                DateTime exp = new DateTime(claims.getBody().getExpiration());
                
                log.debug(claims.getBody().getExpiration().toString());

                /* 
                    根據具體業務
                    用戶信息&權限驗證 
                */
                //claims.getBody()獲取載荷
                //JWTUtils.getInfoFromToken()獲取token中的用戶信息

                if (valid){ //簽名驗證經過
                    return chain.filter(exchange);
                }else {
                    return authError(response, "認證無效");
                }
            } catch (Exception e) {
                log.error("檢查token時異常: " + e);
                if (e.getMessage().contains("JWT expired"))
                    return authError(response, "認證過時");
                else
                    return authError(response, "認證失敗");
            }
        }
    }

    /**
     * 認證錯誤輸出
     * @param response 響應對象
     * @param msg 錯誤信息
     * @return 響應信息
     */
    private Mono<Void> authError(ServerHttpResponse response, String msg) {
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
        Dto returnFail = DtoUtil.returnFail(msg, HttpStatus.UNAUTHORIZED.toString());
        String returnStr = "";
        try {
            returnStr = objectMapper.writeValueAsString(returnFail);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        DataBuffer buffer = response.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Flux.just(buffer));
    }


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

刷新JWT

令牌的刷新要作到用戶無感知的效果, 推薦使用前端攔截器刷新令牌的方式

Web應用程序

一個好的模式是在它過時以前刷新令牌。

將令牌過時時間設置爲一週,並在每次用戶打開Web應用程序並每隔一小時刷新令牌。若是用戶超過一週沒有打開過應用程序,那他們就須要再次登陸,這是可接受的Web應用程序UX(用戶體驗)。

要刷新令牌,API須要一個新的端點,它接收一個有效的、沒有過時的JWT、並返回與新的到期字段相同的簽名的JWT。而後Web應用程序會將令牌存儲在某處。

移動/本地應用程序

大多數本地應用程序的登陸有且僅有一次。

這裏面的出發點是,刷新令牌永遠不會過時,而且能夠始終爲有效的JWT進行更換。

永遠不會過時的令牌的問題是它失去了令牌的意義。譬如,若是你電話丟了,你該怎麼辦?所以,它須要由用戶以某種方式進行識別,應用程序須要提供撤銷訪問的方法。咱們決定使用設備的名稱,例如「maryo的iPad」。而後用戶能夠去應用程序,並撤銷訪問「maryo的iPad」。

另外一種方法是撤銷特定事件的刷新令牌,其中一個有趣的事件是更改密碼。

咱們認爲JWT對於這些用例無效,所以咱們使用隨機生成的字符串,並將它們存儲在咱們這邊。

註銷

沒有辦法完美的將jwt失效

jwt 的目的原本就是爲了在服務器不存任何的東西, 用加解密 的 cpu 時間來換取之前要保存的空間 , 說白了就是用 cpu 時間換內存空間(這個內存能夠是 session, 也多是 redis 這種)

可能的解決方案:

  • 將JWT存儲在數據庫中。您能夠檢查哪些令牌有效以及哪些令牌已被撤銷,但這在我看來徹底違背了使用JWT的目的。
  • 從客戶端刪除令牌。這將阻止客戶端進行通過身份驗證的請求,但若是令牌仍然有效且其餘人能夠訪問它,則仍可使用該令牌。這引出了個人下一點。
  • 令牌生命週期短。讓令牌快速到期。根據應用,多是幾分鐘或半小時。當客戶端刪除其令牌時,會有一個很短的時間窗口仍然可使用它。從客戶端刪除令牌並具備短令牌生存期不須要對後端進行重大修改。可是令牌生命週期短意味着用戶因令牌已過時而不斷被註銷。
  • 旋轉代幣。也許引入刷新令牌的概念。當用戶登陸時,爲他們提供JWT和刷新令牌。將刷新令牌存儲在數據庫中。對於通過身份驗證的請求,客戶端可使用JWT,可是當令牌過時(或即將過時)時,讓客戶端使用刷新令牌發出請求以換取新的JWT。這樣,您只需在用戶登陸或要求新的JWT時訪問數據庫。當用戶註銷時,您須要使存儲的刷新令牌無效。不然,即便用戶已經註銷,有人在監聽鏈接時仍然能夠得到新的JWT。
  • 建立JWT黑名單。根據過時時間,當客戶端刪除其令牌時,它可能仍然有效一段時間。若是令牌生存期很短,則可能不是問題,但若是您仍但願令牌當即失效,則能夠建立令牌黑名單。當後端收到註銷請求時,從請求中獲取JWT並將其存儲在內存數據庫中。對於每一個通過身份驗證的請求,您須要檢查內存數據庫以查看令牌是否已失效。爲了保持較小的搜索空間,您能夠從黑名單中刪除已通過期的令牌。
參考:
https://www.jianshu.com/p/6bf...
https://blog.csdn.net/weixin_...
https://blog.csdn.net/github_...
相關文章
相關標籤/搜索