介绍

JWT全称JSON WEB TOKENS,是当下非常流行的跨域身份验证方法。传统的身份验证需要服务端将用户信息,比如登录信息存入session,然后在用户想要访问后端服务时,通过比对用户的登录状态和session中的数据来进行身份验证。但这种方式在分布式中就显得很麻烦了,JWT 可以跨域传输,适用于微服务、API网关等场景,因为它在客户端存储用户信息,减轻了服务器的内存压力

JWT的组成

JWT整体由三部分组成:Header、Payload、Signature

Header通常由令牌的类型和加密的算法组成,例如:

json
1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

Payload

这部分主要是记录我们所存储的简单且不重要的信息。例如:用户名,过期时间,用户id等等,注意payload中的数据为公开的,不能在里面存放敏感数据,例如password

json
1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

Signature

签名里是由三部分组成,Header的Base64编码,Payload的Base64编码,还有secret,然后通过指定的加密方式,例如HS256,进行加密后得出的字符串

json
1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

这部分是jwt中最重要的一部分,有两个功能:

1.验证该Token在发送过程中是否被修改

因为token中存在secret,是我们指定的密钥,该密钥保存在服务端且不会向用户公开,这样根据请求token中的secret就能判断是否有被修改

2.验证签发人的身份

第二个功能是是从第一个功能中体现出来的,因为只有自己才知道自己的token

在计算出签名哈希后,JWT头(Header),有效载荷(Payload)和签名哈希(Signature)的三个部分组合成一个字符串,每个部分用”.”分隔,就构成整个JWT对象。

JWT使用逻辑

  1. 用户发送登录请求,将用户名和密码传入后端
  2. 后端接收后核对账号密码无误,则根据规则生成jwt token
  3. 后端将生成的token返回给前端,前端保存在本地如localStorage或sessionStorage,退出登录时删除即可
  4. 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
  5. 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
  6. 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

具体使用示例

导入依赖

xml
1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>

创建TokenUtils类用来放置常用方法

获取token

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Token一天后过期
public static final long EXPIRE_TIME = 1000 * 60 * 60 * 24;

public static String genToken(String username,String secret){
//现在系统的时间 + 一天
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
//对密码进行加密
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);

}

创建拦截器

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Slf4j
public class JwtFilter implements HandlerInterceptor {

@Autowired
private UserService userService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

// 从 http 请求头中取出 token
String token = request.getHeader("token");
// 如果请求不是映射到方法直接通过
if(!(handler instanceof HandlerMethod)){
return true;
}
if (token != null){
String username = TokenUtils.getUserNameByToken(request);
// 这边拿到的 用户名 应该去数据库查询获得密码,简略,步骤在service直接获取密码
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StringUtils.isNotEmpty(username),User::getName,username);
User user = userService.getOne(queryWrapper);

boolean result = TokenUtils.verify(token,username,user.getPassword());
if(result){
log.info("通过拦截器");
return true;
}
}else{
throw new ServiceException(Constants.CODE_401,"token认证失败,请重新登录");
}
return false;

}
}

配置拦截器

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) { //一定要通过bean注入,不能new
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/user/login", "/user/register", "**/");
}
@Bean
public JwtFilter authenticationInterceptor() {
return new JwtFilter();
}

}

这样当前端的请求发送过来时首先会被拦截,从中取出token,如果没有token则说明未登录,返回未登录的错误信息。有token则验证token中的信息是否正确,是否被篡改过,验证通过后,拦截器放行,前端能正常收到后端返回的数据。

jwt的一大特点就是服务端不保存用户的身份信息,而是在每次请求时验证用户发送的token,token首次交给用户,用户可以将其保存在localstorage中,然后在每次请求头中添加token,后端收到并验证正确后即可开始正常的业务逻辑,其中返回给用户的token可以在用户登录成功时一并返回

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    @PostMapping("/login")
public Res<User> Login(@RequestBody UserDto user, HttpServletRequest request){

String userName = user.getUsername();
log.info("用户{}正在登录",userName);
String password = DigestUtils.md5DigestAsHex(user.getPassword().getBytes());
// HttpSession session = request.getSession();
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getName,userName);

User one = userService.getOne(queryWrapper);

if(one == null){
log.error("登录失败,用户名不存在");
return Res.error("用户名不存在,请先注册!");
}

if(password.equals(one.getPassword())){
// session.setAttribute("user",one.getId());
log.info("登录成功");

}else{
log.error("密码错误!");
log.info("传入:{},正确:{}",password,one.getPassword());
return Res.error("密码错误!");
}
String token = TokenUtils.genToken(userName, password);
Res<User> res = Res.success(one);
res.setToken(token);

return res;
}

前端在请求头中添加token

js
1
2
3
4
5
6
7
8
9
10
11
12
13
// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=utf-8';
let token = localStorage.getItem("token");
if(token != null){
config.headers['token'] = token; // 设置请求头
}
return config
}, error => {
return Promise.reject(error)
});

image-20240225171711158