持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情
JWT学习
JWT官网: jwt.io/ JWT(Java版)的github地址:github.com/jwtk/jjwt
1. 概述
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).**定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。**因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
2. JWT工作流程
- 用户使用账号和面发出post请求;
- 服务器使用
私钥创建一个jwt;- 服务器返回这个jwt给浏览器;
- 浏览器将该jwt串在请求头中像服务器发送请求;
- 服务器验证该jwt;
- 返回响应的资源给浏览器。
3. 优点
- **1.简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快 **
- **2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库 **
- 3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
- 4.不需要在服务端保存会话信息,特别适用于分布式微服务
4. 为什么Token可以防止伪造
问题
csrf中伪造的请求可以在不得到cookie的情况下在请求头中带上cookie,为什么token不能在未知的情况下在伪造请求中发送,发送请求时不是都是浏览器提供吗?
解答
这么来说吧,Cookies是浏览器自动添加的。这是CSRF攻击的原理。
但是token是无法由浏览器添加的,无法被跨站得到的,这是CSRF的前提,CSRF中的CS就是Cross Site,跨站。 token存在的意义就是,获取token时,将token放在跨站无法得到的数据之中,而验证token时,token必须出现在指定位置。
所以token在验证时就肯定不存在cookies之中,也就不会被浏览器自动带上
5. JWT的三部分
| 三部分 | 说明 |
|---|---|
| Header 头部 | 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型) |
| Payload 负载 | 负载 (类似于飞机上承载的物品) |
| Signature 签证 | 签名/签证 |
5.1 结构 A . B . C
JWT是由三段信息构成的,将这三段信息文本用.连接一起就构成了JWT字符串。
就像这样: A . B . C
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
5.2 第一部分——头部header
完整的头部就像下面这样的JSON:
{
'typ': 'JWT', 'alg': 'HS256'
}
jwt的头部承载两部分信息:
声明类型,这里是jwt 声明加密的算法 通常直接使用
HMACSHA256
A部分
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分(也就是A部分)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
5.3 第二部分——负载payload
负载包含三部分
标准中注册的声明 公共的声明 私有的声明
标准声明
iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的.iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。公共声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
B部分
定义一个payload:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
然后将其进行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
5.4 第三部分——签证signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的) payload (base64后的) secret (秘钥) 秘钥仅服务器知道
C部分
这个部分需要base64加密后的header和base64加密后的payload使用 . 连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret');
// TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
得到的signature就是C部分
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
解析JWT
6. JWT的Java使用
可参考文章
6.1 依赖包JJWT
<!-- jwt依赖包-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
秘钥
// 秘钥
private static final String jwtToken = "Ysl Love";
6.2 创建Token
/***
* 生成Token 利用userId生成唯一的Token
* @param userId
* @return
*/
public static String createToken(Integer userId){
// B部分
Map<String,Object> claims = new HashMap<>();
claims.put("userId",userId);
// claims.put("password","1203456");
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
.setClaims(claims) // body数据,要唯一,自行设置
//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000)); // 设置一天的有效时间
String token = jwtBuilder.compact(); //压缩为xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxx这样的jwt
return token;
}
6.3 解析Token
/**
* 解析token
* @param token
* @return
*/
public static Map<String, Object> checkToken(String token){
try {
// jwtToken是签名秘钥 和生成的签名的秘钥一模一样
Jwt parse = Jwts.parser() //得到DefaultJwtParser
.setSigningKey(jwtToken)//设置签名的秘钥
.parse(token);
return (Map<String, Object>) parse.getBody();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
main测试
public static void main(String[] args) {
String token = createToken(123);
System.out.println(token);
Map<String, Object> stringObjectMap = checkToken(token);
System.out.println(stringObjectMap.toString());
Integer userId = (Integer)stringObjectMap.get("userId");
System.out.println(userId);
}
2022.3.29遇到疑惑——关于JWT合法性检测问题
6.4 检测JWT合法性
接受到JWT后,会⾸先对头部和载荷的内容⽤同⼀算法再次签名。
那么服务器应⽤是怎么知道我们⽤的是哪⼀种算法呢?别忘了,我们在JWT的头部中已经⽤alg字段指明了我们的加密算法了。
如果服务器应⽤对头部和载荷再次以同样方法签名之后发现,⾃⼰计算出来的签名和接受到的签名不⼀样,那么就说明这个Token的内容被别⼈动过的,我们应该拒绝这个Token,返回⼀个HTTP 401 Unauthorized响应。
测试代码
/**
* 校验token是否正确
* @param token 密钥
* @param SECRET 秘钥
* @return 是否正确
*/
public static boolean verify(String token) {
/**
* 根据头部的算法再次对头部和载荷进行同样的签名
*/
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.build();
DecodedJWT jwt = verifier.verify(token);
log.info("jwt的verify校验通过");
return true;
} catch (Exception exception) {
return false;
}
}
文章👇
JWTUtil
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import edu.zhku.zkdcms.shiro.cache.PropertiesUtil;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
/**
* @date: 2022/3/30
* @FileName: JWTUtil
* @author: Yan
* @Des:
*/
@Slf4j
public class JWTUtil {
// 过期时间30分钟
public static final long EXPIRE_TIME = 24*60*60;
private final static String SECRET = "***#&*dcms^@(";
/**
* 校验token是否正确
* @param token 密钥
* @param SECRET 秘钥
* @return 是否正确
*/
public static boolean verify(String token, String username,String roleId,String currentTimeMillis) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.withClaim("roleId",roleId.toString())
.withClaim("currentTimeMillis",currentTimeMillis)
.build();
DecodedJWT jwt = verifier.verify(token);
log.info("jwt的verify校验通过");
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 校验token是否正确
* @param token 密钥
* @param SECRET 秘钥
* @return 是否正确
*/
public static boolean verify(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.build();
DecodedJWT jwt = verifier.verify(token);
log.info("jwt的verify校验通过");
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 获得token中的信息无需secret解密也能获得
* @return token中包含的用户名
*/
public static String getClaim(String token,String claim) {
try {
DecodedJWT jwt = JWT.decode(token);
log.info("在jwt解析后,拿到的数据{}",jwt.getClaim(claim).asString());
return jwt.getClaim(claim).asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 生成签名,5min后过期
* @param username 用户名
* @param currentTimeMillis 当前时间戳
* @param SECRET 秘钥
* @return 加密的token
*/
public static String sign(String username,String roleId,String currentTimeMillis) {
Date date = new Date(System.currentTimeMillis()+ EXPIRE_TIME*1000);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
// 附带username信息
return JWT.create()
.withClaim("username", username)
.withClaim("roleId",roleId.toString())
.withClaim("currentTimeMillis",currentTimeMillis)
.withExpiresAt(date)
.sign(algorithm);
}
}
使用拦截器校验Token
/**
* 添加拦截器,拦截请求,校验token
*/
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HashMap<String, Object> map = new HashMap<>();
String token = request.getHeader("token"); //从request中获取到请求头中的token,进行解析校验
try {
jwtUtil.verifyToken(token);//调用token解析的工具类进行解析
return true; //请求放行
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg", "签名不一致异常");
} catch (TokenExpiredException e) {
e.printStackTrace();
map.put("msg", "令牌过期异常");
} catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("msg", "算法不匹配异常");
} catch (InvalidClaimException e) {
e.printStackTrace();
map.put("msg", "失效的payload异常");
} catch (Exception e) {
e.printStackTrace();
map.put("msg", "token无效");
}
//map异常的数据要返回给客户端需要转换成json格式 @ResponseBody 内置了jackson
String resultJson = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(resultJson);
return false; //异常不放行
}
}