JWT是啥
我不会官方语言,我只会最简单的解释 json web token
为什么要用这个东西
简单点说 现在都流行前后端分离开发,如果需要一个前后端的通讯验证,最简单的cookie+session可以完成这个任务。但是会有问题昂,万一给你浏览cookie器禁了咋整,现在的用户才是老大哥不是,所以呢 就出现这个东西了
它能干点啥
网上一搜一大片最多的就是。单点登录 + jwt。但是这两个东西不是一定要混为一谈的。jwt是jwt 简单点 他就是提供了一点token验证的方式,而不是一定要去作单点登录。(一开始我也混为一谈了,俺们领导天天跟我强调他俩不是一回事)
正题开始—怎么玩
序篇:
最烦唠叨一大片没有实际应用的烦不烦,最开始还要强调一个问题,本文不是用的第三方 java-jwt 或者 jjwt 我们是完全自己实现的。和大众定义的jwt格式没啥太大关系。我们只是借鉴一下思想(因为领导说自己写加密方式比较安全)
正文开始:
我们查一下文档他说 jwt分为三段用 “.” 连接
第一段:
第一段是个json。很简单很简单,就是告诉大家,我们在用jwt,也可以告诉他你用什么方式加密的 但是我并不想告诉他 哈哈哈
第二段 payload
第二段是我们传输的实际业务内容,比如用户名呐,id呐。 所以可以定一个javaBean
第三段
这一段是作为安全性验证最关键的一步,将前两段内容拼接后拿过来,进行加密。
代码开始
我们先准备一点工具类,准备啥呢,我们准备用Aes进行第三种的加密,
当然还要准备base64工具类,不可能用字节数组来回的传输。这里用到的东西以jdk1.8为准。都是自带的 不需要导入任何第三方依赖
首先是AesUtil 这里有个坑要注意哦
网上搜的大多数aes加密工具类 会有问题,
第一个问题:详情请搜索Aes自动补全机制,如果你不改 在windows环境没问题,
linux环境加解密的补全机制不一样,解密时就会报错。报错是啥我没记 记住有这个问题就好 具体改动下面注释有
第二个问题:aes 处理前后是有byte数组存在的,正常情况下我们是直接
new String(byte[] b). String s.getBytes()这样的。但是这样会有问题,由于这个串长度 巴拉巴拉 的问题,前后会不一样,所以用base64进行处理。或者用 16--2 进制处理,我下面都提供了 自己看着来
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
/**
* @author chunying
* @Date 2020.12.02
* @Description 用于Aes加解密的工具类
*/
public class AesUtil {
private static final Logger LOG = LoggerFactory.getLogger(AesUtil.class);
/**
* AES加密
* @param value
* @param key
* @return
*/
public static byte[] encrypt(String value, String key) {
try {
//等等等 就是这里了 这里要告诉key生成器 我们用的是sun提供的加密。解密同理 如果不服气 你不用一个试试
Security.addProvider(new sun.security.provider.Sun());
KeyGenerator kgen = KeyGenerator.getInstance("AES");// 创建AES的Key生产者
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG",new sun.security.provider.Sun());
secureRandom.setSeed(key.getBytes());
kgen.init(128, secureRandom);
//加密没关系,SecureRandom是生成安全随机数序列,key.getBytes()是种子,只要种子相同,序列就一样,所以解密只要有password就行
SecretKey secretKey = kgen.generateKey();// 根据用户密码,生成一个密钥
byte[] enCodeFormat = secretKey.getEncoded();// 返回基本编码格式的密钥,如果此密钥不支持编码,则返回
SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");// 转换为AES专用密钥
Cipher cipher = Cipher.getInstance("AES");// 创建密码器
byte[] byteContent = value.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);// 初始化为加密模式的密码器
byte[] result = cipher.doFinal(byteContent);// 加密
return result;
} catch (NoSuchPaddingException e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
} catch (InvalidKeyException e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
} catch (BadPaddingException e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
}
return null;
}
/**
* AES解密 参数是二进制数组 + key
* @param value
* @param key
* @return
*/
public static byte[] desCrypt(byte[] value, String key) {
try {
Security.addProvider(new sun.security.provider.Sun());
KeyGenerator kgen = KeyGenerator.getInstance("AES");// 创建AES的Key生产者
// kgen.init(128, new SecureRandom(key.getBytes()));
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG", new sun.security.provider.Sun());
secureRandom.setSeed(key.getBytes());
kgen.init(128, secureRandom);
SecretKey secretKey = kgen.generateKey();// 根据用户密码,生成一个密钥
byte[] enCodeFormat = secretKey.getEncoded();// 返回基本编码格式的密钥
SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");// 转换为AES专用密钥
Cipher cipher = Cipher.getInstance("AES");// 创建密码器
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);// 初始化为解密模式的密码器
byte[] result = cipher.doFinal(value);
return result; // 明文
} catch (NoSuchAlgorithmException e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
} catch (NoSuchPaddingException e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
} catch (InvalidKeyException e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
} catch (BadPaddingException e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
}
return null;
}
/**
* 将二进制密文转再次换成可识别字符串 返回的是16进制
* @param value
* @return
*/
public static String parseEncrypt(String value, String key) {
byte[] encrypt = encrypt(value, key);
// String s = ParseSystemUtil.parseByte2HexStr(encrypt);
String s = Base64Util.byte2Base64StringFun(encrypt);
return s;
}
/**
* 返回解密后的内容
* @param value
* @param key
* @return
*/
public static String parseDesEncrypt(String value, String key) {
// byte[] bytes = ParseSystemUtil.parseHexStr2Byte(value);
byte[] bytes = Base64Util.base64String2ByteFun(value);
try {
return new String(desCrypt(bytes, key), "utf-8");
} catch (UnsupportedEncodingException e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
}
return null;
}
}
除了上面那个大哥 下面基本就没啥问题了、
import java.io.UnsupportedEncodingException;
import java.util.Base64;
/**
* @author wangchunying
* @Date 2020.12.06
* @Description base工具类
*/
public class Base64Util {
//转码 参数:值,编码格式
public static String getEncodeStr(String value, String format) throws UnsupportedEncodingException{
return Base64.getEncoder().encodeToString(value.getBytes(format));
}
//解码 参数:值,编码格式
public static String getDecodeStr(String value, String format) throws UnsupportedEncodingException {
return new String(Base64.getDecoder().decode(value), format);
}
//base64字符串转byte[]
public static byte[] base64String2ByteFun(String base64Str){
return Base64.getDecoder().decode(base64Str);
}
//byte[]转base64
public static String byte2Base64StringFun(byte[] b){
return Base64.getEncoder().encodeToString(b);
}
}
/**
* @author wangchunying
* @Date 2020.12.02
* @Description 目前有进制转换
*/
public class ParseSystemUtil {
/**将二进制转换成16进制
* @param buf
* @return
*/
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进制转换为二进制
* @param hexStr
* @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;
}
}
然后是最重要的部分 生成解密token部分
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpSession;
/**
* @author wangchunying
* @Date 2020.12.06
* @Description 关于jwt的操作
*/
public class JwtTokenUtil {
private static final Logger LOG = LoggerFactory.getLogger(JwtTokenUtil.class);
//token header
public static final String AUTH_HEADER_KEY = "Authorization";
//一周过期时间
private static final Long expireTime = 1000L*60*60*24*7;
private static final String TYPE_KEY = "typ";
private static final String TYPE_VALUE = "jwt";
private static final String SPOT = ".";
private static final String FORMAT = "utf-8";
private static final String SESSION_KEY = "token";
//加密key
private static final String KEY = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x";
//token前缀
private static final String TOKEN_PREFIX = "ssoToken";
/**
* 生成token 以用户基本信息为payload基础生成token 这样解析出来知道是哪个用户
*/
public static String createToken(UserToken userToken)throws Exception {
StringBuffer sbResult = new StringBuffer();
JSONObject jwtFirstJson = new JSONObject();
String jwtSecondStr = Base64Util.getEncodeStr(JSON.toJSONString(userToken), FORMAT);
//拼接jwt格式
jwtFirstJson.put(TYPE_KEY, TYPE_VALUE);
String jwtFirstStr = Base64Util.getEncodeStr(JSON.toJSONString(jwtFirstJson), FORMAT);
sbResult.append(jwtFirstStr).append(SPOT).append(jwtSecondStr);
String hexString = AesUtil.parseEncrypt(sbResult.toString(), KEY);
//设置过期时间
refreshExpireTime(userToken);
// String jwtString = TOKEN_PREFIX + JWT.create()
// .withSubject(jsonString)
// .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
// .sign(AesUtil.parseEncrypt(jsonString, KEY));
return sbResult.append(SPOT).append(hexString).toString();
}
//解析返回的token
public static UserToken verifyToken(String value) {
if (StringUtils.isBlank(value)) {
throw new JwtException(JwtException.JwtErrorCodeEnum.FORMAT, "token is null");
}
String[] valueArray = value.split("\\.");
if (valueArray.length < 3) {
throw new JwtException(JwtException.JwtErrorCodeEnum.FORMAT, "token des format is error", valueArray);
}
String thirdStr = AesUtil.parseDesEncrypt(valueArray[2], KEY);
//校验是否是真实用户?暂定先返回传过来的信息
if (StringUtils.isBlank(thirdStr)) {
throw new JwtException(JwtException.JwtErrorCodeEnum.FORMAT, "token des is error");
}
String[] thirdDesStr = thirdStr.split("\\.");
if (thirdDesStr == null || thirdDesStr.length < 2) {
throw new JwtException(JwtException.JwtErrorCodeEnum.CONTENT, "token des format is error", thirdDesStr);
}
//校验是否符合格式
try {
//检验payload 与 header 是否被动过
if (!valueArray[0].equals(thirdDesStr[0]) || !valueArray[1].equals(thirdDesStr[1])) {
throw new JwtException(JwtException.JwtErrorCodeEnum.MODIFIED, "token data is modified by smo");
}
//first str
String firstStr = Base64Util.getDecodeStr(thirdDesStr[0], FORMAT);
JSONObject jsonObject = JSON.parseObject(firstStr);
String typeValue = jsonObject.getString(TYPE_KEY);
if (!StringUtils.isNotBlank(typeValue) || !typeValue.equals(TYPE_VALUE)) {
throw new JwtException(JwtException.JwtErrorCodeEnum.CONTENT, "token first format is error", typeValue);
}
//second str
String secondStr = Base64Util.getDecodeStr(thirdDesStr[1], FORMAT);
UserToken userToken = JSON.parseObject(secondStr, UserToken.class);
if (isExpire(userToken)) {
throw new JwtException(JwtException.JwtErrorCodeEnum.EXPIRE, "token is expire");
}
return userToken;
}catch(Exception e) {
LOG.error(e.getMessage(), e);
e.printStackTrace();
}
return null;
}
/**
* 判断用户token是否过期
* @param userToken
* @return
*/
public static Boolean isExpire(UserToken userToken) {
Long currentTime = System.currentTimeMillis();
return userToken.getExpireTime() <= currentTime;
}
/**
* 刷新token生效时间
* @param usertoken
*/
public static void refreshExpireTime(UserToken usertoken) {
Long time = System.currentTimeMillis() + expireTime;
usertoken.setExpireTime(time);
}
/**
* 将token放入session 可以知道当前会话用户信息
* @param userToken
*/
public static void setLocalUser(HttpSession session, UserToken userToken) {
session.setAttribute(SESSION_KEY, userToken);
}
/**
* 获取当前会话的用户token信息
* @return
*/
public static UserToken getUserToken(HttpSession session) {
return (UserToken)session.getAttribute(SESSION_KEY);
}
}
下面是一些会用到的其他类 一起提供了 使用时请注意。。。
/**
* @author wangchunying
* @Date 2020.12.06
*/
public class JwtException extends RuntimeException{
private JwtErrorCodeEnum code;
private String message;
private Object data;
public JwtException(){
super();
}
public JwtException(JwtErrorCodeEnum code, String messgae) {
this.code = code;
this.message = messgae;
}
public JwtException(JwtErrorCodeEnum code, String message, Object data) {
this.code = code;
this.message = message;
this.data = data;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public JwtErrorCodeEnum getCode() {
return code;
}
public void setCode(JwtErrorCodeEnum code) {
this.code = code;
}
@Override
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
return "JwtException{" +
"code=" + code +
", message='" + message + '\'' +
", data=" + data +
'}';
}
/**
* sso code enum
*/
public enum JwtErrorCodeEnum {
//解析格式错误
FORMAT(30001),
//内容错误
CONTENT(30002),
//内容前后不一致 确认被修改过
MODIFIED(30003),
//过期
EXPIRE(30004),
//其他错误
OTHER(-10);
Integer code;
JwtErrorCodeEnum(Integer code){
this.code = code;
}
public Integer getCode() {
return code;
}
@Override
public String toString() {
return "JwtErrorCodeEnum{" +
"code=" + code +
'}';
}
}
}
public class SsoException extends RuntimeException{
private Integer code;
private String message;
public SsoException(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
@Override
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
return "SsoException{" +
"code=" + code +
", message='" + message + '\'' +
'}';
}
}
import java.io.Serializable;
/**
* @author wangchunying
* @Date 2020.12.02
* @Description
*/
public class UserToken implements Serializable {
private static final long serialVersionUID = 1619189980427628544L;
private Integer userID;
private String userName;
//过期时间
private Long expireTime;
public Long getExpireTime() {
return expireTime;
}
public void setExpireTime(Long expireTime) {
this.expireTime = expireTime;
}
public Integer getUserID() {
return userID;
}
public void setUserID(Integer userID) {
this.userID = userID;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
@Override
public String toString() {
return "UserToken{" +
"userID=" + userID +
", userName='" + userName + '\'' +
", expireTime='" + expireTime + '\'' +
'}';
}
}
到此为止 所谓的jwt部分已经结束了,下面是如果你想用jwt作为共有接口拦截应该怎么做。是基于springboot来说的
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
/**
* @author wangchunying
* @Date 2020.12.07
* @Descrip 全局方法拦截 若方法不想使用此拦截器 请看JwtIgnore
*/
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从http请求头中取出token
//如果本地测试嫌麻烦可以把下面三行注释打开 切记别提交
// if (1==1) {
// return true;
// }
final String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
//如果不是映射到方法,直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
//如果是方法探测,直接通过
if (HttpMethod.OPTIONS.equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
//如果方法有JwtIgnore注解,直接通过
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
if (method.isAnnotationPresent(JwtIgnore.class)) {
JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class);
if (jwtIgnore.value()) {
return true;
}
}
//若测试接口 可以注释 打开token
// if (StringUtils.isBlank(token)) {
// throw new SsoException(901, "token is null, please check again");
// }
//验证,并获取token内部信息
UserToken userToken = JwtTokenUtil.verifyToken(token);
//将token放入本地缓存
JwtTokenUtil.setLocalUser(request.getSession(), userToken);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//方法结束后,移除缓存的token
// JwtTokenUtil.removeUserToken();
}
}
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author wangchunying
* @Date 2020.12.07
* @Descrip 跨域配置
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
// /**
// * 重写父类提供的跨域请求处理的接口
// */
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// // 添加映射路径
// registry.addMapping("/**")
// // 放行哪些原始域
// .allowedOrigins("*")
// // 是否发送Cookie信息
// .allowCredentials(true)
// // 放行哪些原始域(请求方式)
// .allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS", "HEAD")
// // 放行哪些原始域(头部信息)
// .allowedHeaders("*")
// // 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
// .exposedHeaders("Server","Content-Length", "Authorization", "Access-Token", "Access-Control-Allow-Origin","Access-Control-Allow-Credentials");
// }
/**
* 添加拦截器。如果你想在拦截器里面注入其他类。请交给spring管理 如不会 可问我 或自行查询
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加权限拦截器
registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/**").excludePathPatterns("/static/**");
}
}
这里我不想启动跨域 所以注销掉了
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author wangchunying
* @Date 2020.12.07
* @Description: 如果你不想token 介入方法 请添加此注解
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtIgnore {
boolean value() default true;
}
到此为止 jwt已经写完了,后面我会陆续更新息息相关的登录~