如果還不了解JWT的,可以看
JWT實(shí)現(xiàn)單點(diǎn)登錄【JWT介紹】
下面以實(shí)際項(xiàng)目中的應(yīng)用分析
首先看一下大致的數(shù)據(jù)流圖:

主要有以下三步:
1、項(xiàng)目一開始我先封裝了一個(gè)JWTHelper工具包,主要提供了生成JWT、解析JWT以及校驗(yàn)JWT的方法,其他還有一些加密相關(guān)操作,后面我會(huì)以代碼的形式介紹下代碼。工具包寫好后我將打包上傳到私服,能夠隨時(shí)依賴下載使用;
2、接下來(lái),我在客戶端項(xiàng)目中依賴JWTHelper工具包,并添加Interceptor攔截器,攔截需要校驗(yàn)登錄的接口。攔截器中校驗(yàn)JWT有效性,并在response中重新設(shè)置JWT的新值;
3、最后在JWT服務(wù)端,依賴JWT工具包,在登錄方法中,需要在登錄校驗(yàn)成功后調(diào)用生成JWT方法,生成一個(gè)JWT令牌并且設(shè)置到response的header中。
下面部分代碼介紹
JwtHelper工具類:
package org.vic.common.util;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.vic.common.constant.SecretConstant;
import org.vic.common.web.ValidateLoginInterceptor;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
/**
* JWT工具類
*
* @author vic
*
*/
@Slf4j
@SuppressWarnings("restriction")
public class JwtHelper {
/**
* 生成JWT字符串 格式:A.B.C A-header頭信息 B-payload 有效負(fù)荷 C-signature 簽名信息
* 是將header和payload進(jìn)行加密生成的
*
* @param userId 用戶編號(hào)
* @param userName 用戶名
* @param identities 客戶端信息(變長(zhǎng)參數(shù)),目前包含瀏覽器信息,用于客戶端攔截器校驗(yàn),防止跨域非法訪問(wèn)
* @return
*/
public static String generateJWT(String userId, String userName, String... identities) {
// 簽名算法,選擇SHA-256
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 獲取當(dāng)前系統(tǒng)時(shí)間
long nowTimeMillis = System.currentTimeMillis();
Date now = new Date(nowTimeMillis);
// 將BASE64SECRET常量字符串使用base64解碼成字節(jié)數(shù)組
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET);
// 使用HmacSHA256簽名算法生成一個(gè)HS256的簽名秘鑰Key
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
// 添加構(gòu)成JWT的參數(shù)
Map<String, Object> headMap = new HashMap<String, Object>();
// Header { "alg": "HS256", "typ": "JWT" }
headMap.put("alg", SignatureAlgorithm.HS256.getValue());
headMap.put("typ", "JWT");
JwtBuilder builder = Jwts.builder().setHeader(headMap)
// Payload { "userId": "1234567890", "userName": "vic", }
// 加密后的客戶編號(hào)
.claim("userId", AESSecretUtil.encryptToStr(userId, SecretConstant.DATAKEY))
// 客戶名稱
.claim("userName", userName)
// 客戶端瀏覽器信息
.claim("userAgent", identities[0])
// Signature
.signWith(signatureAlgorithm, signingKey);
// 添加Token過(guò)期時(shí)間
if (SecretConstant.EXPIRESSECOND >= 0) {
long expMillis = nowTimeMillis + SecretConstant.EXPIRESSECOND;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate).setNotBefore(now);
}
return builder.compact();
}
/**
* 解析JWT 返回Claims對(duì)象
*
* @param jsonWebToken
* @return
*/
public static Claims parseJWT(String jsonWebToken) {
Claims claims = null;
try {
if (StringUtils.isNotBlank(jsonWebToken)) {
// 解析jwt
claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET))
.parseClaimsJws(jsonWebToken).getBody();
} else {
log.warn("[JWTHelper]-json web token 為空");
}
} catch (Exception e) {
log.error("[JWTHelper]-JWT解析異常:可能因?yàn)閠oken已經(jīng)超時(shí)或非法token");
}
return claims;
}
/**
* 校驗(yàn)JWT是否有效 返回json字符串的demo:
* {"freshToken":"A.B.C","userName":"vic","userId":"123", "userAgent":"xxxx"}
* freshToken-刷新后的jwt userName-客戶名稱 userId-客戶編號(hào) userAgent-客戶端瀏覽器信息
*
* @param jsonWebToken
* @return
*/
public static String validateLogin(String jsonWebToken) {
Map<String, Object> retMap = null;
Claims claims = parseJWT(jsonWebToken);
if (claims != null) {
// 解密客戶編號(hào)
String decryptUserId = AESSecretUtil.decryptToStr((String) claims.get("userId"), SecretConstant.DATAKEY);
retMap = new HashMap<String, Object>();
// 加密后的客戶編號(hào)
retMap.put("userId", decryptUserId);
// 客戶名稱
retMap.put("userName", claims.get("userName"));
// 客戶端瀏覽器信息
retMap.put("userAgent", claims.get("userAgent"));
// 刷新JWT
retMap.put("freshToken", generateJWT(decryptUserId, (String) claims.get("userName"),
(String) claims.get("userAgent"), (String) claims.get("domainName")));
} else {
log.warn("[JWTHelper]-JWT解析出claims為空");
}
return retMap != null ? JSONObject.toJSONString(retMap) : null;
}
}
AES加密工具類:
package org.vic.common.util;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.vic.common.constant.SecretConstant;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
/**
* AES加密工具類
*
* @author vic
*
*/
public class AESSecretUtil {
/**
* 秘鑰的大小
*/
private static final int KEYSIZE = 128;
/**
* AES加密
*
* @param data 待加密內(nèi)容
* @param key 加密秘鑰
* @return
*/
public static byte[] encrypt(String data, String key) {
if (StringUtils.isNotBlank(data)) {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
// 選擇一種固定算法,為了避免不同java實(shí)現(xiàn)的不同算法,生成不同的密鑰,而導(dǎo)致解密失敗
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(key.getBytes());
keyGenerator.init(KEYSIZE, random);
SecretKey secretKey = keyGenerator.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");// 創(chuàng)建密碼器
byte[] byteContent = data.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);// 初始化
byte[] result = cipher.doFinal(byteContent);
return result; // 加密
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* AES加密,返回String
*
* @param data 待加密內(nèi)容
* @param key 加密秘鑰
* @return
*/
public static String encryptToStr(String data, String key) {
return StringUtils.isNotBlank(data) ? parseByte2HexStr(encrypt(data, key)) : null;
}
/**
* AES解密
*
* @param data 待解密字節(jié)數(shù)組
* @param key 秘鑰
* @return
*/
public static byte[] decrypt(byte[] data, String key) {
if (ArrayUtils.isNotEmpty(data)) {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
// 選擇一種固定算法,為了避免不同java實(shí)現(xiàn)的不同算法,生成不同的密鑰,而導(dǎo)致解密失敗
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(key.getBytes());
keyGenerator.init(KEYSIZE, random);
SecretKey secretKey = keyGenerator.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");// 創(chuàng)建密碼器
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);// 初始化
byte[] result = cipher.doFinal(data);
return result; // 加密
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* AES解密,返回String
*
* @param enCryptdata 待解密字節(jié)數(shù)組
* @param key 秘鑰
* @return
*/
public static String decryptToStr(String enCryptdata, String key) {
return StringUtils.isNotBlank(enCryptdata) ? new String(decrypt(parseHexStr2Byte(enCryptdata), key)) : null;
}
/**
* 將二進(jìn)制轉(zhuǎn)換成16進(jìn)制
*
* @param buf 二進(jìn)制數(shù)組
*
*/
public static String parseByte2HexStr(byte buf[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**
* 將16進(jìn)制轉(zhuǎn)換為二進(jìn)制
*
* @param hexStr 16進(jìn)制字符串
* @return
*/
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte[] result = new byte[hexStr.length() / 2];
for (int i = 0; i < hexStr.length() / 2; i++) {
int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
}
所需的常量類:
package org.vic.common.constant;
/**
* JWT常量值
*
* @author vic
*
*/
public interface SecretConstant {
// 簽名秘鑰
public static final String BASE64SECRET = "ZW]4l5JH[m6Lm)LaQEjpb!4E0lRaG(";
// 超時(shí)毫秒數(shù)(默認(rèn)30分鐘)
public static final int EXPIRESSECOND = 1800000;
// 用于JWT加密的密匙
public static final String DATAKEY = "u^3y6SPER41jm*fn";
}
客戶端攔截器:
package org.vic.common.web;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.vic.common.constant.GlobalConstant;
import org.vic.common.util.JwtHelper;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
/**
* 校驗(yàn)是否登錄攔截器
* @author vic
*
*/
@Slf4j
public class ValidateLoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
//首先從請(qǐng)求頭中獲取jwt串,與頁(yè)面約定好存放jwt值的請(qǐng)求頭屬性名為User-Token
String jwt = httpServletRequest.getHeader("User-Token");
log.info("[登錄校驗(yàn)攔截器]-從header中獲取的jwt為:{}", jwt);
//判斷jwt是否有效
if(StringUtils.isNotBlank(jwt)){
//校驗(yàn)jwt是否有效,有效則返回json信息,無(wú)效則返回空
String retJson = JwtHelper.validateLogin(jwt);
log.info("[登錄校驗(yàn)攔截器]-校驗(yàn)JWT有效性返回結(jié)果:{}", retJson);
//retJSON為空則說(shuō)明jwt超時(shí)或非法
if(StringUtils.isNotBlank(retJson)){
JSONObject jsonObject = JSONObject.parseObject(retJson);
//校驗(yàn)客戶端信息
String userAgent = httpServletRequest.getHeader("User-Agent");
if (userAgent.equals(jsonObject.getString("userAgent"))) {
//獲取刷新后的jwt值,設(shè)置到響應(yīng)頭中
httpServletResponse.setHeader("User-Token", jsonObject.getString("freshToken"));
//將客戶編號(hào)設(shè)置到session中
httpServletRequest.getSession().setAttribute(GlobalConstant.SESSION_CUSTOMER_NO_KEY, jsonObject.getString("userId"));
return true;
}else{
log.warn("[登錄校驗(yàn)攔截器]-客戶端瀏覽器信息與JWT中存的瀏覽器信息不一致,重新登錄。當(dāng)前瀏覽器信息:{}", userAgent);
}
}else {
log.warn("[登錄校驗(yàn)攔截器]-JWT非法或已超時(shí),重新登錄");
}
}
//輸出響應(yīng)流
JSONObject jsonObject = new JSONObject();
jsonObject.put("hmac", "");
jsonObject.put("status", "");
jsonObject.put("code", "100");
jsonObject.put("msg", "未登錄");
jsonObject.put("data", "");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
httpServletResponse.getOutputStream().write(jsonObject.toJSONString().getBytes("UTF-8"));
return false;
}
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
到此,后臺(tái)服務(wù)的配置已經(jīng)完成
下一步就需要前端頁(yè)面將JWT令牌從response響應(yīng)頭中取出,然后存入Localstorage或Cookie中。但是遇到跨域場(chǎng)景,處理起來(lái)就會(huì)比較復(fù)雜,因?yàn)橐坏┰跒g覽器中跨域?qū)@取不到localstorage中的JWT令牌。例如www.a.com域下的JWT,在www.b.com域下是獲取不到的,所以我選擇了一種頁(yè)面跨域的方式進(jìn)行處理,使用iframe+H5的postMessage,具體我使用代碼分享的方式來(lái)分析。
/**CURD本地存儲(chǔ)信息 start**/
(function(doc,win){
var fn=function(){};
fn.prototype={
/*本地?cái)?shù)據(jù)存儲(chǔ) t:cookie有效時(shí)間,單位s; domain:cookie存儲(chǔ)所屬的domain域*/
setLocalCookie: function (k, v, t,domain) {
//如果當(dāng)前瀏覽器不支持localStorage將存儲(chǔ)在cookie中
typeof window.localStorage !== "undefined" ? localStorage.setItem(k, v) :
(function () {
t = t || 365 * 12 * 60 * 60;
domain=domain?domain:".jwtserver.com";
document.cookie = k + "=" + v + ";max-age=" + t+";domain="+domain+";path=/";
})()
},
/*獲取本地存儲(chǔ)數(shù)據(jù)*/
getLocalCookie: function (k) {
k = k || "localDataTemp";
return typeof window.localStorage !== "undefined" ? localStorage.getItem(k) :
(function () {
var all = document.cookie.split(";");
var cookieData = {};
for (var i = 0, l = all.length; i < l; i++) {
var p = all[i].indexOf("=");
var dataName = all[i].substring(0, p).replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");
cookieData[dataName] = all[i].substring(p + 1);
}
return cookieData[k]
})();
},
/*刪除本地存儲(chǔ)數(shù)據(jù)*/
clearLocalData: function (k) {
k = k || "localDataTemp";
typeof window.localStorage !== "undefined" ? localStorage.removeItem(k) :
(function () {
document.cookie = k + "=temp" + ";max-age=0";
})()
},
init:function(){
this.bindEvent();
},
//事件綁定
bindEvent:function(){
var _this=this;
win.addEventListener("message",function(evt){
if(win.parent!=evt.source){return}
var options=JSON.parse(evt.data);
if(options.type=="GET"){
var data=tools.getLocalCookie(options.key);
win.parent.postMessage(data, "*");
}
options.type=="SET"&&_this.setLocalCookie(options.key,options.value);
options.type=="REM"&&_this.clearLocalData(options.key);
},false)
}
};
var tools=new fn();
tools.init();
})(document,window);
/**CURD本地存儲(chǔ)信息 end**/
前端頁(yè)面js代碼(客戶端):
//頁(yè)面初始化向iframe域名發(fā)送消息
window.onload = function() {
console.log('get key value......................')
window.frames[0].postMessage(JSON.stringify({type:"GET",key:"User-Token"}),'*');
}
//監(jiān)聽(tīng)message信息,接收從iframe域下獲取到的token信息,然后存儲(chǔ)到localstorage或cookie中
window.addEventListener('message', function(e) {
console.log('listen.....');
var data = e.data;
console.log(data);
if(data != null){
localStorage.setItem("User-Token", data);
}
}, false);
總結(jié):
優(yōu)點(diǎn):在非跨域環(huán)境下使用JWT機(jī)制是一個(gè)非常不錯(cuò)的選擇,實(shí)現(xiàn)方式簡(jiǎn)單,操作方便,能夠快速實(shí)現(xiàn)。由于服務(wù)端不存儲(chǔ)用戶狀態(tài)信息,因此大用戶量,對(duì)后臺(tái)服務(wù)也不會(huì)造成壓力;
缺點(diǎn):跨域?qū)崿F(xiàn)相對(duì)比較麻煩,安全性也有待探討。因?yàn)镴WT令牌返回到頁(yè)面中,可以使用js獲取到,如果遇到XSS攻擊令牌可能會(huì)被盜取,在JWT還沒(méi)超時(shí)的情況下,就會(huì)被獲取到敏感數(shù)據(jù)信息。
注:
JWT 的自包含、防篡改的特點(diǎn)讓很多人喜歡,可以省掉最讓人煩的集中式的令牌,實(shí)現(xiàn)無(wú)狀態(tài)。
可是,這是有場(chǎng)景限制的。比如主動(dòng)吊銷 Token 要如何處理、有效時(shí)長(zhǎng)如何動(dòng)態(tài)控制、密鑰如何動(dòng)態(tài)切換。
如果沒(méi)有主動(dòng)吊銷 Token 的業(yè)務(wù)需求,那自包含的特點(diǎn)確實(shí)很有用,只是看大家的業(yè)務(wù)場(chǎng)景了。