JWT令牌

1.什么是 JWT

JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。

JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。

JWT 组成

JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分:

  • Header(头部) : 描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型。Header 被 Base64Url 编码后成为 JWT 的第一部分。
  • Payload(载荷) : 用来存放实际需要传递的数据,包含声明(Claims),如sub(subject,主题)、jti(JWT ID)。Payload 被 Base64Url 编码后成为 JWT 的第二部分。
  • Signature(签名):服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。生成的签名会成为 JWT 的第三部分。

JWT 通常是这样的:xxxxx.yyyyy.zzzzz

你可以在 jwt.io 这个网站上对其 JWT 进行解码,解码之后得到的就是 Header、Payload、Signature 这三部分。

Header 和 Payload 都是 JSON 格式的数据,Signature 由 Payload、Header 和 Secret(密钥)通过特定的计算公式和加密算法得到。

img

使用 JWT 进行身份验证不需要依赖 Cookie ,因此可以避免 CSRF 攻击。

2.简单案例

2.1 创建一个spring maven项目

引入以下依赖:

注意,我是用的是3.0.1的springframework版本,不同的版本之间javax.servlet-api可能会存在冲突。

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
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>

<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>

<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

2.2 项目结构

image-20241218224455440

2.3 数据库设计:

image-20241218224537632

2.4 spring数据库配置

1
2
3
4
5
6
7
8
9
10
11
12
13

spring:
datasource:
url: jdbc:mysql://localhost:3306/jwt?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: 数据库账号
password: 数据库密码
driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
configuration:
#这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
call-setters-on-nulls: true

3. 项目详细代码

3.1 users实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.kd13.jwt.entity;


import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("users") //数据库表
public class Users {
@TableField(value = "user_pid") //字段名
private Integer userPid;

@TableField(value = "user_name")
private String userName;

@TableField(value = "user_password")
private String userPassword;

}

3.2 数据库操作

1
2
3
4
5
6
7
8
9
10
11
package com.kd13.jwt.mapper;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kd13.jwt.entity.Users;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UsersMapper extends BaseMapper<Users> {

}

3.3 业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
//UserService
package com.kd13.jwt.service;


import com.baomidou.mybatisplus.extension.service.IService;
import com.kd13.jwt.entity.Users;

public interface UserService extends IService<Users> {
// 查询单个用户与账号密码
Integer SelectOneUser(String userName, String userPassword);

}
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
35
36
//UserServiceImpl
package com.kd13.jwt.service.impl;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kd13.jwt.entity.Users;
import com.kd13.jwt.mapper.UsersMapper;
import com.kd13.jwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UsersMapper, Users> implements UserService {

private final UsersMapper userMapper;

@Autowired
public UserServiceImpl(UsersMapper usersMapper) {
this.userMapper = usersMapper;
}


// 查询 用户账号密码 并返回id
@Override
public Integer SelectOneUser(String userName, String userPassword) {
QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
try {
queryWrapper.eq("user_name", userName).eq("user_password", userPassword);
return userMapper.selectOne(queryWrapper).getUserPid();
} catch (Exception e) {
return 0;
}
}

}

3.4 JWT工具

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.kd13.jwt.util;


import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.stereotype.Component;

import java.util.Calendar;

@Component
public class JwtTokenUtil {
private static final String TOKENKEY = "thisisacode"; // 随机盐
private static final long EXPIRATION = 86400000; // token有效时间(毫秒)

//生成Token
public String generateToken(Integer userId , String userName) {

Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, (int)EXPIRATION);

String token = JWT.create()//生成令牌
.withClaim("userId", userId)//payload
.withClaim("username", userName)//设置自定义用户名
.withExpiresAt(instance.getTime())//设置令牌的有效时间
.sign(Algorithm.HMAC256(TOKENKEY))//设置签名 保密 复杂
;
System.out.println(token);//输出令牌
return token;
}


// 验证token
public Boolean validateToken(String token) {
if (token == null || token.isEmpty()) {
System.out.println("Token is null or empty");
return false;
}
try {
// 检查token是否包含两个点号,这是JWT标准格式的一部分
if (token.split("\\.").length != 3) {
System.out.println("Invalid token format");
return false;
}
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(TOKENKEY)).build();
try {
DecodedJWT decodedJWT = jwtVerifier.verify(token);
System.out.println("用户Id:" + decodedJWT.getClaim("userId").asInt());
System.out.println("用户名:" + decodedJWT.getClaim("username").asString());
System.out.println("过期时间:" + decodedJWT.getExpiresAt());
return true;
}catch (Exception e){
if (e instanceof TokenExpiredException) {
System.out.println("Token已过期");
} else if (e instanceof SignatureVerificationException) {
System.out.println("签名验证失败");
} else if (e instanceof JWTVerificationException) {
System.out.println("JWT验证失败: " + e.getMessage());
} else {
System.out.print("未知错误: ");
e.printStackTrace();
}
}
return false;
} catch (Exception e) {
return false;
}
}
}

3.5 控制层

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.kd13.jwt.controller;

import com.kd13.jwt.service.impl.UserServiceImpl;
import com.kd13.jwt.util.JwtTokenUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@AllArgsConstructor
public class AuthController {
private JwtTokenUtil jwtTokenUtil;

private UserServiceImpl userService;


// 登录系统-获取token-并嵌入cookie中
@GetMapping("login")
public String webLogin(String username, String password, HttpServletRequest request, HttpServletResponse response){
// 先去查询数据库中是否存在该用户
Integer userId = userService.SelectOneUser(username, password);
if (userId > 0) {
// 登录成功后需要去获取token 同时嵌入再网站内的cookie中
String token = jwtTokenUtil.generateToken(userId,username);

// 将token放入请求头中
Cookie cookie = new Cookie("token", token);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(36000); // cookie有效时间
cookie.setSecure(true);

response.addCookie(cookie);

return "登录成功!\n"+token;
}else {
return "账号或密码错误!";
}
}


// 访问首页
@GetMapping("index")
public String onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response){
// 从cookie中获取token值
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("token".equals(cookie.getName())) {
String userToken = cookie.getValue();
if (jwtTokenUtil.validateToken(userToken)) {
return "登录成功!xxx";
}else {
return "账号或密码错误!";
}
}
}
}
return "cookie中找不到token";
}
}

4. 运行项目

通过对接口进行测试:

1
http://localhost:8080/login?username=admin&password=root

image-20241218225142878

验证是否登入成功:

1
http://localhost:8080/index

image-20241218225257485

image-20241218225305819

可以看到之前的token直接加入到了session中进行传递,并且重启服务器也不会失效,只有当设置的有效时长失效后才会失效,也可以再浏览器中以没登陆的方式访问,可以看到是没有携带我们需要的token值的。

image-20241218225426116

5. 扩展

5.1 token登出、改密后失效

使用jwt时,一般修改密码或退出登录时,需要把正在使用的token做失效处理,防止别的客户端使用失效token访问信息。

方案一:在每次修改密码或者退出登录后,修改一下自定义的盐值。当进行下次访问时,会根据自定义盐值验证token,修改了自定义盐值,自然访问不通过。
方案二:利用数据库,存放一个修改或者登出的时间,在创建token时,标注上创建时间。如果这个创建时间小于修改或登出的时间,就表示它是修改或者登出之前的token,为过期token

5.2 token的自动续期、一定时间内无操作掉线

场景:用户登陆后,token的过期时间为30分钟,如果在这30分钟内没有操作,则重新登录,如果30分钟内有操作,就给token自动续一个新的时间。避免用户正在操作时掉线重登!

实现①:在jwt生成token时先不设置过期时间,过期时间的操作放在redis中。

①:在登陆时,把用户信息(或者token)放进redis,并设置过期时间

②:如果30分钟内用户有操作,前端带着token来访问,过滤器解析token得到用户信息,去redis中验证用户信息,验证成功则在redis中增加过期时间,验证失败,返回token错误。实现了token时间的自动更新。

③:如果30分钟内用户无操作,redis中的用户信息已过期,此时再进行操作,token解析出的用户信息在redis中验证失败,则重新登录。实现了一定时间内无操作掉线!

实现②:使用access_token、refresh_token 解决

登录获取token(包括访问令牌access_token,刷新令牌refresh_token),其中access_token设置过期时间为5分钟,refresh_token设置过期时间为30分钟。不能同时过期
前端保存access_token和refresh_token,每次请求带着access_token去访问服务器资源
服务器校验access_token有效性,通过解析access_token看是否能解析出用户信息。如果用户信息为null,说明token无效,返回401,让用户重新登录
服务器端校验access_token是否过期
如果access_token没有过期,则token正常,继续执行业务逻辑
如果access_token过期,计算 过期后到当前的时间大小 是否在refresh_token过期时间之内(是否大于30 - 5 - 5 = 20分钟,为什么不是30 - 5 = 25分钟呢?主要是想对正在请求的用户token做一个缓存,保证在最后五分钟内,新、老token都有效!防止正在进行的请求token突然失效!),
如果大于refresh_token的过期时间,则表示用户长时间无操作,token真正过期了,返回401,让用户重新登录
如果小于refresh_token的过期时间,则继续让该access_token访问业务,但返回给前端标识,提示token已过期,让前端带着refresh_token去服务器获取新的access_token,并保存在前端,后续使用新的access_token去访问!
————————————————

原文链接:https://blog.csdn.net/qq_45076180/article/details/107243172