JWT(JSON Web Token), 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準((RFC 7519).該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登陸(SSO)場景。JWT的聲明通常被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也能夠增長一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。javascript
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,從而回避重放攻擊" }
定義一個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」不一樣。
HS256 使用同一個「secret_key」進行簽名與驗證。一旦 secret_key 泄漏,就毫無安全性可言了。
RS256 是使用 RSA 私鑰進行簽名,使用 RSA 公鑰進行驗證。公鑰即便泄漏也毫無影響,只要確保私鑰安全就行。
對於單體應用而言,HS256 和 RS256 的安全性沒有多大差異。
而對於須要進行多方驗證的微服務架構而言,顯然 RS256/ES256 安全性更高。
只有 user 微服務須要用 RSA 私鑰生成 JWT,其餘微服務使用公鑰便可進行簽名驗證,私鑰獲得了更好的保護。
無狀態登陸
微服務集羣中的每一個服務, 對外提供的都是Rest風格的接口, 而Rest風格的一個最重要的規範就是: 服務的無狀態性, 即:
優勢:
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>
客戶端發送 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, 只有私鑰能夠解密
客戶端向服務器請求,服務端讀取請求頭信息(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對象
解析過程:
載荷解析: 先對載荷進行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); }
簽名解析: 若是存在簽名部分,則對簽名進行解析。
可能的異常
建立簽名校驗器
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); }
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()); } }
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); } }
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/** - ...
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"; }
@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; } }
令牌的刷新要作到用戶無感知的效果, 推薦使用前端攔截器刷新令牌的方式
Web應用程序
一個好的模式是在它過時以前刷新令牌。
將令牌過時時間設置爲一週,並在每次用戶打開Web應用程序並每隔一小時刷新令牌。若是用戶超過一週沒有打開過應用程序,那他們就須要再次登陸,這是可接受的Web應用程序UX(用戶體驗)。
要刷新令牌,API須要一個新的端點,它接收一個有效的、沒有過時的JWT、並返回與新的到期字段相同的簽名的JWT。而後Web應用程序會將令牌存儲在某處。
移動/本地應用程序
大多數本地應用程序的登陸有且僅有一次。
這裏面的出發點是,刷新令牌永遠不會過時,而且能夠始終爲有效的JWT進行更換。
永遠不會過時的令牌的問題是它失去了令牌的意義。譬如,若是你電話丟了,你該怎麼辦?所以,它須要由用戶以某種方式進行識別,應用程序須要提供撤銷訪問的方法。咱們決定使用設備的名稱,例如「maryo的iPad」。而後用戶能夠去應用程序,並撤銷訪問「maryo的iPad」。
另外一種方法是撤銷特定事件的刷新令牌,其中一個有趣的事件是更改密碼。
咱們認爲JWT對於這些用例無效,所以咱們使用隨機生成的字符串,並將它們存儲在咱們這邊。
沒有辦法完美的將jwt失效
jwt 的目的原本就是爲了在服務器不存任何的東西, 用加解密 的 cpu 時間來換取之前要保存的空間 , 說白了就是用 cpu 時間換內存空間(這個內存能夠是 session, 也多是 redis 這種)
可能的解決方案:
參考:
https://www.jianshu.com/p/6bf...
https://blog.csdn.net/weixin_...
https://blog.csdn.net/github_...