一、jwt簡介
JWT(Json Web Token)是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于 Json 的開放標(biāo)準(zhǔn)。JWT 的聲明一般被用來在身份提供者和服務(wù)提供者間傳遞被認(rèn)證的用戶身份信息,以便于從資源服務(wù)器獲取資源。
JWT 由三部分構(gòu)成,第一部分稱為頭部(Header),第二部分稱為消息體(Payload),第三部分是簽名(Signature)。
JWT 生成的 Token 格式為:
token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)
頭部的信息通常由兩部分內(nèi)容組成,令牌的類型和使用的簽名算法,比如下面的代碼:
{"alg": "HS256", "typ": "JWT"}
消息體中可以攜帶一些你需要的信息,比如用戶 ID。因?yàn)槟愕弥肋@個 Token 是哪個用戶的,比如下面的代碼: { "userId": "1", "admin": true,"userAgent":"PostmanRuntime/7.1.1"}
簽名是用來判斷消息在傳遞的路上是否被篡改,從而保證數(shù)據(jù)的安全性,格式如下:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
通過這三部分就組成了我們的 Json Web Token。
更多介紹可以查看 JWT 官網(wǎng) https://jwt.io/introduction/。
二、使用場景
在前后分離場景下,越來越多的項(xiàng)目使用token作為接口的安全機(jī)制,APP端或者WEB端(使用VUE、REACTJS等構(gòu)建)使用token與后端接口交互,以達(dá)到安全的目的。
三、思路整理

四、springCloudAlibaba引入JWT的依賴
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
五、代碼
工具類:
@Slf4j
public class JwtUtils {
/**
* JWT_WEB_TTL:WEBAPP應(yīng)用中token的有效時間,默認(rèn)60分鐘
*/
public static final long JWT_WEB_TTL = 60* 60 * 1000;
/**
* 將jwt令牌保存到header中的key
*/
public static final String JWT_HEADER_KEY = "auth_token";
// 指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經(jīng)將這部分內(nèi)容封裝好了。
private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
private static final String JWT_SECRET = "xxxxx";// JWT密匙 c
private static final SecretKey JWT_KEY;// 使用JWT密匙生成的加密key
static {
byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);
JWT_KEY = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
}
/**
* 檢查Token是否合法
* @param token
* @return JWTResult
*/
public static JWTResult checkToken(String token) {
try {
Claims claims = Jwts.parser().setSigningKey(JWT_KEY).parseClaimsJws(token).getBody();
String sub = claims.get("sub", String.class);
return new JWTResult(true, sub, "合法請求", ResponseCode.SUCCESS_CODE.getCode());
} catch (ExpiredJwtException e) {
// 在解析JWT字符串時,如果‘過期時間字段’已經(jīng)早于當(dāng)前時間,將會拋出ExpiredJwtException異常,說明本次請求已經(jīng)失效
return new JWTResult(false, null, "token已過期", ResponseCode.TOKEN_TIMEOUT_CODE.getCode());
}catch (Exception e) {
return new JWTResult(false, null, "非法請求", ResponseCode.NO_AUTH_CODE.getCode());
}
}
/**
* 刷新令牌
* @param token
* @return
*/
public static ResponseData refreshToken(String token){
log.info("刷新令牌token:{}",token);
JSONObject resultMap = new JSONObject();
try {
byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);
SecretKeySpec JWT_KEY = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
Claims claims = Jwts.parser().setSigningKey(JWT_KEY).parseClaimsJws(token).getBody();
//Claims claims = Jwts.parserBuilder().setSigningKey(JWT_KEY).build().parseClaimsJws(token).getBody();
Date expiration = claims.getExpiration(); //過期時間
Date issuedAt = claims.getIssuedAt(); //token簽發(fā)時間
resultMap.put("userAgent",claims.get("userAgent"));
Object userId = claims.get("userId");
long nowTime = System.currentTimeMillis();
//如果當(dāng)前的token已經(jīng)過期則需要重新登錄
if(nowTime > expiration.getTime()){
log.info("用戶userId:{}令牌token:{}已經(jīng)過期,需要重新登錄",userId,token);
return ResponseData.fail("token過期",ResponseCode.TOKEN_TIMEOUT_CODE.getCode());
}
long startTime = issuedAt.getTime();
long pastTime = nowTime - startTime;
if(pastTime > 30*60*1000){
log.info("用戶userId:{}令牌token:{}已經(jīng)存在超過30分鐘,每30分鐘刷新一次令牌",userId,token);
//獲取新的token ,將原先的token進(jìn)行失效
String newToken = createToken(claims, JwtUtils.JWT_WEB_TTL);
resultMap.put("token",newToken);
log.info("用戶userId:{}令牌token:{}已經(jīng)存在超過30分鐘,每30分鐘刷新一次令牌,新令牌:{}",userId,token,newToken);
return ResponseData.ok(resultMap);
}
String sub = claims.get("sub", String.class);
resultMap.put("token",token);
log.info("用戶userId:{}令牌token:{}",userId,token);
return ResponseData.ok(resultMap);
} catch (ExpiredJwtException e) {
log.info("令牌token:{},過期了需要重新登錄",token);
return ResponseData.fail("token過期",ResponseCode.TOKEN_TIMEOUT_CODE.getCode());
} catch (Exception e) {
log.error("刷新令牌出現(xiàn)異常:{}",e);
}
return null;
}
/**
* 創(chuàng)建JWT令牌,簽發(fā)時間為當(dāng)前時間
*
* @param claims
* 創(chuàng)建payload的私有聲明(根據(jù)特定的業(yè)務(wù)需要添加,如果要拿這個做驗(yàn)證,一般是需要和jwt的接收方提前溝通好驗(yàn)證方式的)
* @param ttlMillis
* JWT的有效時間(單位毫秒),當(dāng)前時間+有效時間=過期時間
* @return jwt令牌
*/
public static String createToken(Map<String, Object> claims, long ttlMillis) {
byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);
SecretKeySpec JWT_KEY = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
// 生成JWT的時間,即簽發(fā)時間
long nowMillis = System.currentTimeMillis();
JwtBuilder builder = Jwts.builder()
.setClaims(claims) //如果有私有聲明,一定要先設(shè)置這個自己創(chuàng)建的私有的聲明,這個是給builder的claim賦值,一旦寫在標(biāo)準(zhǔn)的聲明賦值之后,就是覆蓋了那些標(biāo)準(zhǔn)的聲明的
.setId(UUID.randomUUID().toString().replace("-", ""))// 設(shè)置jti(JWT ID):是JWT的唯一標(biāo)識,根據(jù)業(yè)務(wù)需要,這個可以設(shè)置為一個不重復(fù)的值,主要用來作為一次性token,從而回避重放攻擊。
.setIssuedAt(new Date(nowMillis))//jwt的簽發(fā)時間
.signWith(SIGNATURE_ALGORITHM,JWT_KEY)//設(shè)置簽名使用的簽名算法和簽名使用的秘鑰
.setExpiration(new Date(nowMillis + ttlMillis));//設(shè)置JWT的過期時間
return builder.compact();
}
}
public class RSAUtils {
/**
* * 生成公鑰和私鑰
* * @throws NoSuchAlgorithmException *
*/
public static HashMap<String, Object> getKeys()
throws NoSuchAlgorithmException {
HashMap<String, Object> map = new HashMap<String, Object>();
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(1024);
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
map.put("public", publicKey);
map.put("private", privateKey);
return map;
}
/**
* * 使用模和指數(shù)生成RSA公鑰
* * @param modulus 模
* * @param exponent 指數(shù) *
*
* @return
*/
public static RSAPublicKey getPublicKey(String modulus, String exponent) {
try {
BigInteger b1 = new BigInteger(modulus);
BigInteger b2 = new BigInteger(exponent);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(b1, b2);
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* * 使用模和指數(shù)生成RSA私鑰
* * /None/NoPadding
* * @param modulus
* 模 * @param
* exponent指數(shù) * @return
*/
public static RSAPrivateKey getPrivateKey(String modulus, String exponent) {
try {
BigInteger b1 = new BigInteger(modulus);
BigInteger b2 = new BigInteger(exponent);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(b1, b2);
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* * 公鑰加密 *
* * @param data
* * @param publicKey
* * @return
* * @throws
* Exception
*/
public static String encryptByPublicKey(String data, RSAPublicKey publicKey)
throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 模長
int key_len = publicKey.getModulus().bitLength() / 8;
// 加密數(shù)據(jù)長度 <= 模長-11
String[] datas = splitString(data, key_len - 11);
String mi = "";
// 如果明文長度大于模長-11則要分組加密
for (String s : datas) {
mi += bcd2Str(cipher.doFinal(s.getBytes()));
}
return mi;
}
/**
* * 私鑰解密 *
* * @param data
* * @param privateKey
* * @return
* * @throws
* Exception
*/
public static String decryptByPrivateKey(String data,
RSAPrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// 模長
int key_len = privateKey.getModulus().bitLength() / 8;
byte[] bytes = data.getBytes();
byte[] bcd = ASCII_To_BCD(bytes, bytes.length);
// System.err.println(bcd.length);
// 如果密文長度大于模長則要分組解密
String ming = "";
byte[][] arrays = splitArray(bcd, key_len);
for (byte[] arr : arrays) {
ming += new String(cipher.doFinal(arr));
}
return ming;
}
/**
* * ASCII碼轉(zhuǎn)BCD碼 *
*/
public static byte[] ASCII_To_BCD(byte[] ascii, int asc_len) {
byte[] bcd = new byte[asc_len / 2];
int j = 0;
for (int i = 0; i < (asc_len + 1) / 2; i++) {
bcd[i] = asc_to_bcd(ascii[j++]);
bcd[i] = (byte) (((j >= asc_len) ? 0x00 : asc_to_bcd(ascii[j++])) + (bcd[i] << 4));
}
return bcd;
}
public static byte asc_to_bcd(byte asc) {
byte bcd;
if ((asc >= '0') && (asc <= '9'))
bcd = (byte) (asc - '0');
else if ((asc >= 'A') && (asc <= 'F'))
bcd = (byte) (asc - 'A' + 10);
else if ((asc >= 'a') && (asc <= 'f'))
bcd = (byte) (asc - 'a' + 10);
else
bcd = (byte) (asc - 48);
return bcd;
}
/**
* * BCD轉(zhuǎn)字符串
*/
public static String bcd2Str(byte[] bytes) {
char temp[] = new char[bytes.length * 2], val;
for (int i = 0; i < bytes.length; i++) {
val = (char) (((bytes[i] & 0xf0) >> 4) & 0x0f);
temp[i * 2] = (char) (val > 9 ? val + 'A' - 10 : val + '0');
val = (char) (bytes[i] & 0x0f);
temp[i * 2 + 1] = (char) (val > 9 ? val + 'A' - 10 : val + '0');
}
return new String(temp);
}
/**
* * 拆分字符串
*/
public static String[] splitString(String string, int len) {
int x = string.length() / len;
int y = string.length() % len;
int z = 0;
if (y != 0) {
z = 1;
}
String[] strings = new String[x + z];
String str = "";
for (int i = 0; i < x + z; i++) {
if (i == x + z - 1 && y != 0) {
str = string.substring(i * len, i * len + y);
} else {
str = string.substring(i * len, i * len + len);
}
strings[i] = str;
}
return strings;
}
/**
* *拆分?jǐn)?shù)組
*/
public static byte[][] splitArray(byte[] data, int len) {
int x = data.length / len;
int y = data.length % len;
int z = 0;
if (y != 0) {
z = 1;
}
byte[][] arrays = new byte[x + z][];
byte[] arr;
for (int i = 0; i < x + z; i++) {
arr = new byte[len];
if (i == x + z - 1 && y != 0) {
System.arraycopy(data, i * len, arr, 0, y);
} else {
System.arraycopy(data, i * len, arr, 0, len);
}
arrays[i] = arr;
}
return arrays;
}
}
攔截器:
@Configuration
public class LoginConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注冊TestInterceptor攔截器
InterceptorRegistration registration = registry.addInterceptor(new ValidateLoginInterceptor());
registration.addPathPatterns("/**"); //所有路徑都被攔截
registration.excludePathPatterns( //添加不攔截路徑
"/user/login", //登錄
);
}
}
@Slf4j
public class ValidateLoginInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
//首先從請求頭中獲取jwt串,與頁面約定好存放jwt值的請求頭屬性名為author_token
String jwt = httpServletRequest.getHeader(JwtUtils.JWT_HEADER_KEY);
log.info("[登錄攔截器]-從header中獲取的jwt為:{}", jwt);
//判斷jwt是否有效
if(StringUtils.isNotBlank(jwt)){
//校驗(yàn)jwt是否有效,有效則返回json信息,無效則返回空
// JWTResult jwtResult = JwtUtils.checkToken(jwt);
ResponseData responseData = JwtUtils.refreshToken(jwt);
log.info("[登錄攔截器]-校驗(yàn)JWT有效性返回結(jié)果:{}", responseData);
//retJSON為空則說明jwt超時或非法
if(responseData != null && responseData.getCode() == 200 ){
Object data = responseData.getData();
String s = JSONObject.toJSONString(data);
JSONObject jsonObject = JSONObject.parseObject(s);
log.info("[登錄攔截器]-校驗(yàn)JWT有效性,解析返回data:{}", jsonObject);
//校驗(yàn)客戶端信息
String userAgent = httpServletRequest.getHeader("User-Agent");
if (userAgent.equals(jsonObject.getString("userAgent"))) {
//獲取刷新后的jwt值,設(shè)置到響應(yīng)頭中
httpServletResponse.setHeader(JwtUtils.JWT_HEADER_KEY, jsonObject.getString("token"));
//將客戶編號設(shè)置到session中
httpServletRequest.getSession().setAttribute("user_id", jsonObject.getString("userId"));
return true;
}else{
log.warn("[登錄攔截器]-客戶端瀏覽器信息與JWT中存的瀏覽器信息不一致,重新登錄。當(dāng)前瀏覽器信息:{}", userAgent);
}
}else {
log.warn("[登錄攔截器]-JWT非法或已超時,重新登錄");
}
}
//輸出響應(yīng)流
JSONObject jsonObject = new JSONObject();
jsonObject.put("hmac", "");
jsonObject.put("status", "");
jsonObject.put("code", "4007");
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;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
登錄接口
@RequestMapping("/login")
public Result login(String userName ,String password,HttpServletResponse response,HttpServletRequest request){
//todo--根據(jù)userName,parssword進(jìn)行登錄驗(yàn)證
Integer userId = 1381 ;
Map<String,Object> claims=new HashMap<String, Object>();
claims.put("userId",userId);
String userAgent = request.getHeader("User-Agent");
claims.put("userAgent",userAgent);
// JwtUtils jwtUtils = new JwtUtils();
String jwt = JwtUtils.createToken(claims, JwtUtils.JWT_WEB_TTL);
response.setHeader(JwtUtils.JWT_HEADER_KEY, jwt);
return Result.success(jwt);
}
總結(jié):
此案例通過jwt獲取token,通過攔截器校驗(yàn),生成token的userAgent和當(dāng)前訪問的userAgent是否一致,以及token是否合法,實(shí)現(xiàn)登錄邏輯