背景
这两天在整理之前研发的单体服务时,偶然发现了一个好玩的东西:JWT。该服务中集成了用户登陆和权鉴功能,用户登陆后,通过JWT将用户的基本信息进行签名,存储到cookie里,后面的请求自动带上cookie,通过过滤器检查cookie里的用户信息是否存在,是否过期,是否被篡改,从而达到校验用户身份的效果。而这些信息的签名,解析,校验,仅通过JWT用几行代码就可以实现。我当时看到这个只觉眼前一亮:嘿,这小东西还挺别致哈,必须得拿来给宝子们分享下!
JWT介绍
JWT是JSON Web Token的缩写。它是一种用于在网络应用之间安全传递信息的方式。JWT由三部分组成,分别是Header(头部),Payload(负载)和Signature(签名)。
Header(头部)用于描述关于该JWT的最基本的信息,比如JWT的类型(typ)和签名算法(alg)等。JWT的头部信息和负载信息都是经过Base64编码的,并通过"."连接起来形成最终的JWT。设置头部信息的作用有以下几点:
1) 指定JWT的类型:JWT可以分为JWS(JSON Web Signature)和JWE(JSON Web Encryption),JWT三种类型。JWS用于签名,JWE用于加密。在头部信息中,可以通过设置typ字段来指定JWT的类型,方便客户端进行相应的处理。
2) 指定JWT的签名算法:JWT的签名算法决定了JWT的安全性和可靠性。在头部信息中,可以通过设置alg字段来指定JWT的签名算法。常见的签名算法包括HS256、RS256等。(一般不在header里设置,而是用.sign(Algorithm)来设置签名算法)。
3) 添加自定义头部信息:在头部信息中,还可以添加自定义的字段,用于传递额外的信息。比如,可以添加kid字段来指定签名密钥的ID,方便客户端进行密钥管理。
Payload(负载)包含了需要传递的用户信息,如用户ID、角色、权限等。Signature(签名)则是对Header和Payload进行签名,防止信息被篡改。
JWT的优点是易于使用和传递,由于信息已经被签名,可以防止信息篡改,因此可以实现无状态的分布式系统。但是,由于JWT中包含了用户信息,因此一旦被窃取,可能会导致安全问题。因此在使用JWT时需要注意安全性,并采取相应的措施,如使用HTTPS协议等。
在使用JWT时,服务端会生成一个JWT并将其返回给客户端。客户端之后每次请求时都会将该JWT加入到请求头部中,服务端会验证该JWT的合法性,并根据其中的信息进行相应的操作。由于JWT中已包含了用户信息,因此无需再次查询数据库,可以提高系统的性能。
JWT实践
闲话不多说,咱们这就来实践JWT的数据签名和信息获取。
1、引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
2、编写JWT工具类
// 用户类
@Data
public class UserVO {
private String userAccount;
private String password;
private String userName;
}
// JWT工具类
public class JwtUtils {
// token密钥
private static final String TOKEN_SECRET = "leixiyueqi";
public static final String USER_INFO_KEY = "userInfo";
/**
* 将数据转换成JWT
*
* @param subject 主题
* @param obj 数据
* @return 转换后的token值
*/
public static String token(String subject, Object obj) {
String token = "";
try {
//秘钥及加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
//设置头部信息
Map<String, Object> header = new HashMap<>();
header.put("typ", "JWT");
token = JWT.create()
// 设置主题
.withSubject(subject)
// 设置头
.withHeader(header)
// 设置数据
.withClaim("data", JSON.toJSONString(obj))
// 设置签发时间
.withIssuedAt(new Date())
// 设置过期时间(expiration) 为1小时
.withExpiresAt(new Date(System.currentTimeMillis() + 3600000))
// 设置签名算法
.sign(algorithm);
} catch (Exception e) {
e.printStackTrace();
return null;
}
return token;
}
/**
* @desc 验证token,通过返回true
**/
public static void verify(String token) {
try {
JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).build().verify(token);
} catch (Exception e) {
throw e;
}
}
public static String getTokenStr(String token, String data) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(data).asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 从token中获取信息,并进行转码
*
* @param token
* @param clazz
* @return
* @param <T>
*/
public static <T> T getTokenData(String token, Class<T> clazz) {
try {
DecodedJWT jwt = JWT.decode(token);
Map<String, Claim> claims = jwt.getClaims();
String data = claims.get("data").asString();
return JSON.parseObject(data, clazz);
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 从cookie中获取信息并解码
*
* @param request
* @return
*/
public static <T> T getDataFromCookie(HttpServletRequest request, String cookieName, Class<T> clazz) {
String cookieLoginValue = getCookieFromRequest(request, cookieName);
try {
verify(cookieLoginValue);
return getTokenData(cookieLoginValue, clazz);
} catch (Exception e) {
return null;
}
}
public static String getCookieFromRequest(HttpServletRequest request, String cookieName) {
String cookieLoginValue = Strings.EMPTY;
Cookie[] cookies = request.getCookies();
if (cookies == null || cookies.length == 0) {
return cookieLoginValue;
}
for (Cookie cookie : cookies) {
if (cookieName.equals(cookie.getName())) {
cookieLoginValue = cookie.getValue();
break;
}
}
return cookieLoginValue;
}
}
3、添加controller
@RestController
public class UserController {
@PostMapping("/user/login")
public Object login(@RequestBody UserVO user, HttpServletResponse response) throws Exception {
if (!user.getPassword().equals("123456")) {
throw new Exception("用户密码错误,登陆失败");
}
// 将用户信息转成token,存到key为userInfo的cookie中
Cookie cookie = new Cookie(JwtUtils.USER_INFO_KEY, JwtUtils.token("LOGIN", user));
cookie.setPath("127.0.0.1");
response.addCookie(cookie);
return "登陆成功";
}
@GetMapping("/getUserInfo")
public Object getUserInfo(HttpServletRequest request){
return JwtUtils.getDataFromCookie(request, JwtUtils.USER_INFO_KEY, UserVO.class);
}
}
4、postman执行测试
用postman执行了login之后,自动添加了cookie到工具上,直接运行查询接口,就可以查询到用户信息。
5、添加拦截器:如果想要每次发送请求时,都对cookie进行校验,检查cookie中的数据是否准确,有没有过期,则可以通过添加一个拦截器来实现。如下:
// 配置类
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor()).
excludePathPatterns("/user/login") // 放行
.addPathPatterns("/**"); // 拦截除了"/user/**的所有请求路径
}
}
//拦截器类
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = JwtUtils.getCookieFromRequest(request, JwtUtils.USER_INFO_KEY);
Map<String,Object> map = new HashMap<>();
try {
JwtUtils.verify(token);
return true;
} catch (TokenExpiredException e) {
map.put("state", false);
map.put("msg", "Token已经过期!!!");
} catch (SignatureVerificationException e){
map.put("state", false);
map.put("msg", "签名错误!!!");
} catch (AlgorithmMismatchException e){
map.put("state", false);
map.put("msg", "加密算法不匹配!!!");
} catch (Exception e) {
e.printStackTrace();
map.put("state", false);
map.put("msg", "无效token~~");
}
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
我们把JwtUtils里面的过期时间设置为8000ms,重新启动服务,先调/user/login刷新cookie,等8秒后再调/getUserInfo,结果如下图,说明拦截器是成功的。
扩展测试
在实践JWT的功能时,我突发其想,既然token是密文,我能不能用什么办法将密文给破解了呢?于是我从postman中拿到response里返回的cookie,直接对token进行解析,然后再进行验证,结果如下:
事实证明,JWT加密后的数据可以直接通过JWT.decode()方法进行解密,根本不需要知道加密时用的什么签名key和算法,所以JWT加密的数据是非常不安全的,千万别像雷袭那样把用户密码都放到token里。但是,在verify时,会用生成token的算法和secretKey对token进行检查,如果发现token时间过期,或者算法、secretKey不符合预设,内容不一致,均会报错。所以verify()方法防止了信息被篡改,实现了数据的安全传输。在进行token解析之前,一定要先对token执行verify()方法。
最后,在进行JWT实践时,我参考了这篇文章jwt原理_bigrobin的技术博客_51CTO博客,
感谢大佬引路之恩。