0 JWT是什麼
JWT(JSON Web Token)是一種開放標準,它以一種緊湊且獨立的方式,可以在各方之間作爲JSON對象安全地傳輸信息。
其認證原理是,客戶端向服務器申請授權,服務器認證以後,生成一個token字符串並返回給客戶端,此後客戶端在請求受保護的資源時攜帶這個token,服務端進行驗證再從這個token中解析出用戶的身份信息。
0.1 JWT的結構
一個JWT是一個字符串,其由Header(頭部)、Payload(負載)和Signature(簽名)三個部分組成,中間以.
號分隔,其格式爲Header.Payload.Signature
,如下所示:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJNSU5HIiwiZXhwIjoxNTQ3MzQ1MjgxLCJ1c2VyTmFtZSI6ImFkbWluIiwiaWF0IjoxNTQ3MzQ1MjIxfQ.
NfsnnoORftR4oP7hFHjmDgj5HYxKd-RXjEW9upn9Tgk
Header
Header本質是一個JSON對象,由包含了兩個屬性:
{
"alg": "HS256",
"typ": "JWT"
}
alg
表示簽名的算法,typ
則表示token的類型。然後,將Header進行Base64URL 編碼轉成字符串作爲JWT的第一組成部分。
Payload
JWT的第二組成部分被稱作載荷,其本質也是一個JSON對象,用來存放認證所需的一些額外聲明,其包含有一種類型:標準中註冊的聲明(registered),公共的聲明(public), 和私有的聲明(private claims),其中公有聲明和私有聲明可自定義字段。
標準中註冊的聲明 (建議但不強制使用) :
iss (issuer):簽發人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受衆
nbf (Not Before):生效時間
iat (Issued At):簽發時間
jti (JWT ID):編號
僅有的聲明和私有的聲明可以添加一些額外的用戶屬性,例如:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
由於JWT 是不加密的,所以不應該把用戶敏感類信息放在這個部分。
將Payload
部分進行Base64URL 編碼轉成字符串,即爲JWT的第二組成部分。
Signature
JWT的第三組成部分是簽名,它是對前面兩個部分的簽名。比如,如果JWT中Header指定的算法是 MAC SHA256,那麼Signature爲,其中secret加解密使用的密鑰:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
關於JWT更詳細的介紹,可到官網查看:https://jwt.io/introduction/
0.2 JWT的使用場景
由於JWT傳輸過程中可攜帶額外的信息(如用戶信息),使用JWT後,服務端不再需要保存用戶的狀態信息,服務端需要保存的只有加解密使用的密鑰。由於JWT的無狀態特性(不需存儲),在一個分佈式的面向服務的框架中,使用JWT做接口鑑權是再適合不過的了。
1 JWT用作接口鑑權
由於JWT的天然無狀態,在JWT生成的一刻,其過期時間就已經註定了,並且不可更改,所以,如果要對JWT實現token續簽的話,一般的做法是額外生成一個refreshToken用於獲取新token,refreshToken需存儲於服務端,其過期時間可以比JWT的過期時間要稍長。
在Spring Cloud微服務框架下,要實現接口的鑑權,最好的方式就是通過Spring Cloud Gateway網關服務來完成。網關提供了統一服務的訪問接口,客戶端在調用服務時,須先經過網關,網關進行權限的校驗,對非法請求進行過濾後再進行請求的轉發,以此來完成鑑權。
接下來,我們通過在網關中使用JWT實現一個簡單的鑑權服務。
其授權基本流程:
1 用戶提交賬號密碼,服務生成token和refreshToken並返回,其中refreshToken設置過期時間,並關聯用戶身份和當前的token;
2 用戶請求服務時,前置網關對請求進行過濾,從請求中取出token,在驗證通過後將token中包含的身份信息作爲參數所加到當前請求;
3 用戶請求到達具體的服務後,取出包含的身份信息,進行具體的業務處理;
token的刷新流程:
1 用戶攜帶refreshToken參數請求token刷新接口,服務端在判斷refreshToken未過期後,取出關聯的用戶信息和當前token,使用當前用戶信息重新生成token,並將舊的token置於黑名單中,返回新的token;
2 用戶攜帶新token重新進行請求;
該示例中JWT的實現使用了開源的Java JWT,地址爲:https://github.com/auth0/java-jwt
1.1 代碼實現
1.1.1 登錄授權:token的生成
public Map<String,Object> login(@RequestParam String userName,
@RequestParam String password){
Map<String,Object> resultMap = new HashMap<>();
//賬號密碼校驗
if(StringUtils.equals(userName, "admin")&&
StringUtils.equals(password, "admin")){
//生成JWT
String token = buildJWT(userName);
//生成refreshToken
String refreshToken = UUID.randomUUID().toString().replaceAll("-","");
//保存refreshToken至redis,使用hash結構保存使用中的token以及用戶標識
String refreshTokenKey = String.format(jwtRefreshTokenKeyFormat, refreshToken);
stringRedisTemplate.opsForHash().put(refreshTokenKey,
"token", token);
stringRedisTemplate.opsForHash().put(refreshTokenKey,
"userName", userName);
//refreshToken設置過期時間
stringRedisTemplate.expire(refreshTokenKey,
refreshTokenExpireTime, TimeUnit.MILLISECONDS);
//返回結果
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("token", token);
dataMap.put("refreshToken", refreshToken);
resultMap.put("code", "10000");
resultMap.put("data", dataMap);
return resultMap;
}
resultMap.put("isSuccess", false);
return resultMap;
}
private String buildJWT(String userName){
//生成jwt
Date now = new Date();
Algorithm algo = Algorithm.HMAC256(secretKey);
String token = JWT.create()
.withIssuer("MING")
.withIssuedAt(now)
.withExpiresAt(new Date(now.getTime() + tokenExpireTime))
.withClaim("userName", userName)//保存身份標識
.sign(algo);
return token;
}
1.1.2 token的刷新
public Map<String,Object> refreshToken(@RequestParam String refreshToken){
Map<String,Object> resultMap = new HashMap<>();
String refreshTokenKey = String.format(jwtRefreshTokenKeyFormat, refreshToken);
String userName = (String)stringRedisTemplate.opsForHash().get(refreshTokenKey,
"userName");
if(StringUtils.isBlank(userName)){
resultMap.put("code", "10001");
resultMap.put("msg", "refreshToken過期");
return resultMap;
}
String newToken = buildJWT(userName);
//替換當前token,並將舊token添加到黑名單
String oldToken = (String)stringRedisTemplate.opsForHash().get(refreshTokenKey,
"token");
stringRedisTemplate.opsForHash().put(refreshTokenKey, "token", newToken);
stringRedisTemplate.opsForValue().set(String.format(jwtBlacklistKeyFormat, oldToken), "",
tokenExpireTime, TimeUnit.MILLISECONDS);
resultMap.put("code", "10000");
resultMap.put("data", newToken);
return resultMap;
}
1.1.3 網關鑑權:token的校驗
@Component
public class AuthFilter implements GlobalFilter, Ordered {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthFilter.class);
@Value("${jwt.secret.key}")
private String secretKey;
@Value("${auth.skip.urls}")
private String[] skipAuthUrls;
@Value("${jwt.blacklist.key.format}")
private String jwtBlacklistKeyFormat;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public int getOrder() {
return -100;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String url = exchange.getRequest().getURI().getPath();
//跳過不需要驗證的路徑
if(Arrays.asList(skipAuthUrls).contains(url)){
return chain.filter(exchange);
}
//從請求頭中取出token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
//未攜帶token或token在黑名單內
if (token == null ||
token.isEmpty() ||
isBlackToken(token)) {
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.setStatusCode(HttpStatus.OK);
originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] response = "{\"code\": \"401\",\"msg\": \"401 Unauthorized.\"}"
.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
return originalResponse.writeWith(Flux.just(buffer));
}
//取出token包含的身份,用於業務處理
String userName = verifyJWT(token);
if(userName.isEmpty()){
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.setStatusCode(HttpStatus.OK);
originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] response = "{\"code\": \"10002\",\"msg\": \"invalid token.\"}"
.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
return originalResponse.writeWith(Flux.just(buffer));
}
//將現在的request,添加當前身份
ServerHttpRequest mutableReq = exchange.getRequest().mutate().header("Authorization-UserName", userName).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
return chain.filter(mutableExchange);
}
/**
* JWT驗證
* @param token
* @return userName
*/
private String verifyJWT(String token){
String userName = "";
try {
Algorithm algorithm = Algorithm.HMAC256(secretKey);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("MING")
.build();
DecodedJWT jwt = verifier.verify(token);
userName = jwt.getClaim("userName").asString();
} catch (JWTVerificationException e){
LOGGER.error(e.getMessage(), e);
return "";
}
return userName;
}
/**
* 判斷token是否在黑名單內
* @param token
* @return
*/
private boolean isBlackToken(String token){
assert token != null;
return stringRedisTemplate.hasKey(String.format(jwtBlacklistKeyFormat, token));
}
}
完整代碼見:GitHub
參考鏈接:
http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html