一、jwt简介
JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于 Json 的开放标准。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。
JWT 由三部分构成,第一部分称为头部(Header),第二部分称为消息体(Payload),第三部分是签名(Signature)。
JWT 生成的 Token 格式为:
token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)
头部的信息通常由两部分内容组成,令牌的类型和使用的签名算法,比如下面的代码:
{"alg": "HS256", "typ": "JWT"}
消息体中可以携带一些你需要的信息,比如用户 ID。因为你得知道这个 Token 是哪个用户的,比如下面的代码: { "userId": "1", "admin": true,"userAgent":"PostmanRuntime/7.1.1"}
签名是用来判断消息在传递的路上是否被篡改,从而保证数据的安全性,格式如下:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
通过这三部分就组成了我们的 Json Web Token。
更多介绍可以查看 JWT 官网 https://jwt.io/introduction/。
二、使用场景
在前后分离场景下,越来越多的项目使用token作为接口的安全机制,APP端或者WEB端(使用VUE、REACTJS等构建)使用token与后端接口交互,以达到安全的目的。
三、思路整理
四、springCloudAlibaba引入JWT的依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
五、代码
工具类:
@Slf4j
public class JwtUtils {
/**
* JWT_WEB_TTL:WEBAPP应用中token的有效时间,默认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已经将这部分内容封装好了。
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字符串时,如果‘过期时间字段’已经早于当前时间,将会抛出ExpiredJwtException异常,说明本次请求已经失效
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签发时间
resultMap.put("userAgent",claims.get("userAgent"));
Object userId = claims.get("userId");
long nowTime = System.currentTimeMillis();
//如果当前的token已经过期则需要重新登录
if(nowTime > expiration.getTime()){
log.info("用户userId:{}令牌token:{}已经过期,需要重新登录",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:{}已经存在超过30分钟,每30分钟刷新一次令牌",userId,token);
//获取新的token ,将原先的token进行失效
String newToken = createToken(claims, JwtUtils.JWT_WEB_TTL);
resultMap.put("token",newToken);
log.info("用户userId:{}令牌token:{}已经存在超过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("刷新令牌出现异常:{}",e);
}
return null;
}
/**
* 创建JWT令牌,签发时间为当前时间
*
* @param claims
* 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
* @param ttlMillis
* JWT的有效时间(单位毫秒),当前时间+有效时间=过期时间
* @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的时间,即签发时间
long nowMillis = System.currentTimeMillis();
JwtBuilder builder = Jwts.builder()
.setClaims(claims) //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setId(UUID.randomUUID().toString().replace("-", ""))// 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setIssuedAt(new Date(nowMillis))//jwt的签发时间
.signWith(SIGNATURE_ALGORITHM,JWT_KEY)//设置签名使用的签名算法和签名使用的秘钥
.setExpiration(new Date(nowMillis + ttlMillis));//设置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;
}
/**
* * 使用模和指数生成RSA公钥
* * @param modulus 模
* * @param exponent 指数 *
*
* @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;
}
}
/**
* * 使用模和指数生成RSA私钥
* * /None/NoPadding
* * @param modulus
* 模 * @param
* exponent指数 * @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;
// 加密数据长度 <= 模长-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码转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转字符串
*/
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;
}
/**
* *拆分数组
*/
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)){
//校验jwt是否有效,有效则返回json信息,无效则返回空
// JWTResult jwtResult = JwtUtils.checkToken(jwt);
ResponseData responseData = JwtUtils.refreshToken(jwt);
log.info("[登录拦截器]-校验JWT有效性返回结果:{}", 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("[登录拦截器]-校验JWT有效性,解析返回data:{}", jsonObject);
//校验客户端信息
String userAgent = httpServletRequest.getHeader("User-Agent");
if (userAgent.equals(jsonObject.getString("userAgent"))) {
//获取刷新后的jwt值,设置到响应头中
httpServletResponse.setHeader(JwtUtils.JWT_HEADER_KEY, jsonObject.getString("token"));
//将客户编号设置到session中
httpServletRequest.getSession().setAttribute("user_id", jsonObject.getString("userId"));
return true;
}else{
log.warn("[登录拦截器]-客户端浏览器信息与JWT中存的浏览器信息不一致,重新登录。当前浏览器信息:{}", userAgent);
}
}else {
log.warn("[登录拦截器]-JWT非法或已超时,重新登录");
}
}
//输出响应流
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--根据userName,parssword进行登录验证
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);
}
总结:
此案例通过jwt获取token,通过拦截器校验,生成token的userAgent和当前访问的userAgent是否一致,以及token是否合法,实现登录逻辑