JWT !
前记:
- 官网:https://jwt.io/
- jwt有人说是用计算力换空间(相对于session)
- 小程序后台要求全部用springboot实现.。登录状态的管理:本来想用自己随便生成UUID作为token使用,然后token可以配合cookie进行存储与传送。但是微信小程序发起的请求中默认不带有cookie(可以通过设置来发出cookie,但麻烦),同时为了更方便与更安全,则学习JWT。
- 学习视频:https://www.bilibili.com/video/BV1i54y1m7cP?from=search&seid=14084425364989447981
一、概述
-
JWT:json web token :JSON Web令牌
-
作用:
- 授权
- 信息交换(签名处理)
-
传统的session认证
- 认证方式与流程
- 客户端发来请求
- 服务端生成session对象**(保存在服务器内存中)**,保存用户一些信息
- 服务端生成sessionid放在cookie的 Jsession 字段中返回给用户
- 用户下次请求的时候,在cookie中带有 Jsession,作为认证
- 暴露的问题
- session保存在服务器内存中,如果用户量大,服务端的开销大
- 会话如果保存在一个服务器上,在分布式系统中,用户必须访问同一个服务器,限制了负载均衡器的能力,限制了扩展能力
- 基于cookie认证,如果cookie被截获,容易受到跨站请求伪造攻击
- 在前后端分离的系统中,增加了部署的复杂性。通常用户一次请求就要转发多次,如果用session,每次携带sessionid到服务器,服务器还要查询用户信息,增加负担 (??看不懂)
- 认证方式与流程
-
JWT认证
认证流程
前端发来账号密码认证请求
后端核对成功后,将用户id等其他信息作为 JWT Payload,将其与头部分别进行Base64编码拼接后签名,形成一个JWT。
前端收到响应后,将jwt保存在本地
以后:
前端的每次请求,在HTTP Header中的Authorization字段中放入 JWT(解决XSS和XSRF)
后端检查JWT合法性,包括是否存在,有效性,签名合法性,接收方是否自己等
后端验证成功后,用JWT包含的用户信息进行其他操作,返回结果
优势
- 简洁:可以通过URL、post参数在HTTP header中改善,数据量小,传输速度快
- 自包含self-contaioned:负载中包含了所有用户所需要的信息,避免多次查询数据库
- Token是以json加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持
- 不需要在服务商保存会话信息,节约服务端内存开销,且特别适用于分布式系统的开发
- 服务器断电后,只要token不过期,那么token还有效!
二、结构
JWT的结构由三部分组成:
- 标头Header
- 有效载荷Payload
- 签名Signature
格式: xxxx(header 的base64编码) . yyyy(payoad的base64编码) . zzzz(签名)
标头Header
-
标头由两个部分组成: 令牌类型(JWT) + 所使用的签名算法
-
用Base64进行编码(所以可被解码)
-
格式:
{ "alg": "HS256", #表示所使用的签名算法 "typ": "JWT" #表示令牌类型 }
有效载荷
-
包含声明。声明是有关实体(如用户)等其他数据的声明
-
用Base64进行编码(所以可被解码)
-
一般情况下不能放如密码等敏感的信息
-
其实可以把实体信息加密码后,才放入Payload中
-
格式:
{ "userID": "1234543536", #后台自定义的一些消息 "name": "admin", "sex": "男" }
签名
- 使用编码后的header和Payload以及私钥,再使用header中指定的编码进行签名
- 目的:保证 JWT 在传输过程中没有被篡改过
- 验证流程:对前端送来的JWT,取前面两个字段(header和payload),再用私钥,再用header中指定的编码进行签名的生成,将生成的签名与前端送来的JWT的第三个字段进行对比,相同则证明没有被篡改过
三、JWT在java使用
依赖:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
编码形成token
设置的过期时间会在payload中增加一个exp字段(第二个java程序中输出可以看出)。
所以这就是为什么xxx.yyy.zzz中签名字段加签的时候没有用到时间,但结果还是体现出了和时间相关
@Test
void contextLoads() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, 6000);//设置6000s
String token = JWT.create()
// .withHeader(new HashMap<>()) //添加标头,不写默认 jwt 和 HS256
.withExpiresAt(calendar.getTime())//设置过期时间,注意:签名的值与过期时间相关!!!
.withClaim("userName", "hao")//设置payload中的声明块
.withClaim("userID", 123)
.sign(Algorithm.HMAC256("我是私钥"));//设置签名的算法和私钥
System.out.println(token);
}
/**输出:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTc3MTg5MjAsInVzZXJOYW1lIjoiaGFvIiwidXNlcklEIjoxMjN9.CCrsWH0XWTDqUVxE94sL4ABBuBGmRuco5AUPxSurQAY
**/
对已有的token进行解码
@Test
void test() throws IOException {
Verification verification = JWT.require(Algorithm.HMAC256("我是私钥"));//指定验证签名时的算法及私钥
DecodedJWT decodedJWT = verification
.build()
.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJleHAiOjE2MTc3MTg5MjAsInVzZXJOYW1lIjoiaGFvIiwidXNlcklEIjoxMjN9." +
"CCrsWH0XWTDqUVxE94sL4ABBuBGmRuco5AUPxSurQAY");//对指定的token进行验证
System.out.println("token = " + decodedJWT.getToken());//拿到一整个token
System.out.println("header = " + decodedJWT.getHeader());//拿到base64编码的标头
System.out.println("解码后:" + new String(new BASE64Decoder().decodeBuffer(decodedJWT.getHeader())));
System.out.println("payload = " + decodedJWT.getPayload());//拿到base64编码的payload
System.out.println("解码后:" + new String(new BASE64Decoder().decodeBuffer(decodedJWT.getPayload())));
System.out.println("signature = " + decodedJWT.getSignature());//拿到签名
System.out.println("userName = " + decodedJWT.getClaim("userName").asString());//拿到payload中的声明块字段的值
System.out.println("userID = " + decodedJWT.getClaim("userID").asInt());
}
/**输出:
token = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTc3MTg5MjAsInVzZXJOYW1lIjoiaGFvIiwidXNlcklEIjoxMjN9.CCrsWH0XWTDqUVxE94sL4ABBuBGmRuco5AUPxSurQAY
header = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
解码后:{"typ":"JWT","alg":"HS256"}
payload = eyJleHAiOjE2MTc3MTg5MjAsInVzZXJOYW1lIjoiaGFvIiwidXNlcklEIjoxMjN9
解码后:{"exp":1617718920,"userName":"hao","userID":123}
signature = CCrsWH0XWTDqUVxE94sL4ABBuBGmRuco5AUPxSurQAY
userName = hao
userID = 123
**/
四、工具类的封装
/**
对第三大点进行简单的封装
**/
public class JWTUtil {
private final String KEY = "我是私钥";
public static String getToken(Map<String, String> claimMap) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, 6000);//设置6000s
JWTCreator.Builder jwtBuilder = JWT.create();
// claimMap.forEach(jwtBuilder::withClaim); //两种lambda表达式都可以
//
// claimMap.forEach((k,v)->{
// jwtBuilder.withClaim(k,v):
// });
for (Map.Entry<String, String> entry : claimMap.entrySet()) {
jwtBuilder.withClaim(entry.getKey(), entry.getValue());
}
String token = jwtBuilder.withExpiresAt(calendar.getTime())
.sign(Algorithm.HMAC256(KEY));
return token;
}
//调用的方法需要在外部try catch, 拿到异常。无异常则正常
public static void verify(String token) {
JWT.require(Algorithm.HMAC256(KEY)).build().verify(token);
}
}
五、配合拦截器
关于token在项目中的理解:
@Component
public class CommonInterceptor implements HandlerInterceptor {
@Autowired
private RedisService redisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");//token要放在header中
if (token == null) {
return false;
}
if (redisService.get(token) == null) { //如果redis没有这个token
return false;
}
try {
JWTUtil.verify(token);
} catch (Exception e) { //接住JWT工具封装的异常
return false;
}
return true;
}
}
@PostMapping("/login")
public String login(@RequestBody Map<Object, Object> data,
HttpServletResponse response) {
System.out.println("执行了登录接口");
//从云函数返回的resultBean中拿到data
JSONObject resultBeanJsonObject = JSON.parseObject(cloudFunction.execute("admin", JSON.toJSONString(data)));
JSONObject dataJsonObject = resultBeanJsonObject.getJSONObject("data");
String id = dataJsonObject.getString("_id");
String admin = dataJsonObject.getString("admin");
Map<String, String> claimMap = new HashMap<>();
claimMap.put("id", "id");
claimMap.put("admin", admin);
String token = JWTUtil.getToken(claimMap);
dataJsonObject.put("token", token);//放入返回数据data中
resultBeanJsonObject.put("data", dataJsonObject);//把data更新到resultBean中
String oldToken = redisService.get(id);
if(oldToken != null) {//如果上次登录时的token还在,则移除,防止用户多次登录爆redis内存
redisService.remove(oldToken);
redisService.remove(id);
}
//反向两个key其实有两个功能:
//1:可以防用户重复登录时,redis爆内存。因为上一次的旧token(上一次登录时的token),会被清除。
//2:可以让用户只能在一个设备上登录。因为在另一个设备上登录时,旧的token(另一台设备上存的)会被从redis中清除
redisService.setWithExpire(token, id, RedisConstant.TOKEN_EXPIRE);//加入redis
redisService.setWithExpire(id, token, RedisConstant.TOKEN_EXPIRE);//加入反向key,防止用户多次登录爆redis内存
return resultBeanJsonObject.toJSONString();
}