前言

通过JWT令牌Interceptor拦截器实现登录校验功能(以一个简单的Demo实例演示)。

版本:

  • Maven:3.6.1
  • JDK:17
  • SpringBoot:3.3.2
  • API测试工具:Postman

项目实例构建

新建一个名为springboot_demo的项目,并勾选添加如下依赖:

  • Developer Tools
    • Lombok
  • Web
    • Spring Web
  • SQL
    • MyBatis Framework
    • MySQL Driver

项目目录如下:

POJO层

  • User类:
1
2
3
4
5
6
7
8
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
}
  • Result类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;
private String msg;
private Object data;

// 用于快速返回Result对象的三个静态方法
public static Result success(Object data) {
return new Result(1, "success", data);
}

public static Result success() {
return new Result(1, "success", null);
}

public static Result error(String msg) {
return new Result(0, msg, null);
}
}

Mapper层

  • UserMapper接口:
1
2
3
4
5
6
//根据用户名和密码返回用户
@Mapper
public interface UserMapper {
@Select("select * from user where username = #{username} and password = #{password}")
User getUser(User user);
}

Service层

  • UserService接口:
1
2
3
public interface UserService {
User login(User user);
}
  • UserServiceImpl实现类:
1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper;

@Override
public User login(User user) {
return userMapper.getUser(user);
}
}

application.properties配置数据库连接信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring.application.name=springboot_demo

# 驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_demo
# 连接数据库的用户名
spring.datasource.username=root
# 连接数据库的密码
spring.datasource.password=root

# 配置mybatis的日志,指定输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# 开启mybatis的驼峰命名自动映射开关 a_column --> aCloumn
mybatis.configuration.map-underscore-to-camel-case=true

创建一个名为springboot_demo的数据库,在其中新建一个user表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'huazhu', '123456');

SET FOREIGN_KEY_CHECKS = 1;

JWT令牌

简介

  • JWT:JSON Web Token(官网:JSON Web Tokens - jwt.io

  • 定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

  • 应用场景:登录认证

    1. 浏览器向服务器发起登录请求
    2. 登录接口访问成功,服务器会生成一个JWT令牌,并返回给浏览器。
    3. 浏览器收到JWT令牌会将其存储起来(token),并在之后的每一次请求都携带该令牌。服务器在每次处理请求之前,都会先校验令牌。

    不同于基于Cookie和基于Session的会话跟踪技术,JWT令牌技术不需要服务器存储相应的令牌信息,并且该技术可以自定义设计有效时间,过期令牌作废。

  • 组成:

    • 第一部分:Header(头),记录令牌类型、签名算法等。例如:{“alg”:“HS256”,“type”:“JWT”}
    • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{“id”:“1”,“username”:“Tom”}
    • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload加入指定密钥,通过指定的签名算法计算而来。

JWT生成&测试

pom.xml配置文件中引入JWT依赖:

1
2
3
4
5
6
<!--JWT令牌-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

生成

在测试目录下新建立一个测试生成方法testGenJwt()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootTest
public class test {

// 生成JWT令牌
@Test
public void testGenJwt() {

Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("name", "tom");

String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "CNHuaZhu") //签名算法和密钥(密钥内容长度不得小于4个字符)
.setClaims(claims) //载荷(自定义内容)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) //设置有效期为1h
.compact();

System.out.println(jwt);
}
}

通过Jwts.builder()方法构建一个JWT令牌,并通过如下方法设置参数:

  • signWith():设置签名算法和密钥(注意:密钥内容长度不得小于4个字符)
  • setClaims():设置载荷部分,即自定义的数据(JSON格式),可以通过Map集合进行封装
  • setExpiration():设置令牌有效期(如果超出设置的有效期,那么令牌失效)

调用compact()得到一个String类型的JWT令牌返回值。

测试结果:

1
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTcyMzQzNzkxOX0.xSpMEChn44Nd41kuojWKs1cQD8v5gaWx-ar9Q_x7P3w

可以将生成的结果输入至JWT官网查看:

解析

在测试目录下新建立一个测试解析方法testParseJwt()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
public class test {

//解析JWT令牌
@Test
public void testParseJwt() {
Claims claims = Jwts.parser()
.setSigningKey("CNHuaZhu") //指定签名密钥
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTcyMzQzNzkxOX0.xSpMEChn44Nd41kuojWKs1cQD8v5gaWx-ar9Q_x7P3w")
.getBody();

System.out.println(claims);
}
}

通过Jwts.parser()方法解析JWT令牌,并通过如下方法设置参数:

  • setSigningKey():设置解析的签名密钥(必须同生成JWT令牌时设置的密钥一致)
  • parseClaimsJws():传递被解析的JWT令牌

调用getBody():获取载荷部分(JWT令牌中设置的自定义内容)

关于Claims类:Claims类是JWT(JSON Web Token)的一部分,用于存储和传输身份验证和授权信息。它包含了一组键值对,每个键值对都表示一个声明。声明是关于实体(通常是用户)和其他一些实体(如客户端、服务等)的声明性陈述。

1
import io.jsonwebtoken.Claims;

测试结果:

1
{name=tom, id=1, exp=1723437919}

如果JWT令牌解析校验时报错,则说明JWT令牌非法(被篡改过期失效)。

Demo实例:登录后下发令牌

需求:如果登录请求成功,生成并返回JWT令牌;如果失败,返回错误信息。

创建utils.JwtUtils工具类,方便快速生生成和解析使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class JwtUtils {

private static String signKey = "CNHuaZhu"; //签名密钥
private static Long expire = 30000L; //JWT令牌有效时间(单位:ms)

//生成JWT令牌
public static String generateJwt(Map<String, Object> claims) {
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}

//解析JWT令牌
public static Claims parseJWT(String jwt) {
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}

创建controller.LoginController类:

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
@Slf4j
@RestController
public class LoginController {

@Autowired
private UserService userService;

@PostMapping("/login")
public Result login(@RequestBody User user) {
log.info("用户登录:{}", user);
User u = userService.login(user);

//登录成功,生成令牌,下发令牌
if (u != null) {
Map<String, Object> claims = new HashMap<>();
claims.put("id", u.getId()); //获取当前用户的id
claims.put("username", u.getUsername()); //获取当前用户的username

String jwt = JwtUtils.generateJwt(claims); //jwt包含了当前员工的登录信息
return Result.success(jwt);
}

//登陆失败,返回错误信息
return Result.error("用户名或密码错误!");
}
}
  • POST正确请求:

  • POST错误请求:

Interceptor拦截器

简介

  • 概念:拦截器(Interceptor)是一种动态拦截方法调用的机制,类似于过滤器(Filter)。Interceptor是Spring框架中提供的,用于动态拦截控制器(Controller)方法的执行。
  • 作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。

Demo实例:拦截请求

需求:

  1. 正确发送登录请求,返回JWT令牌;否则返回错误信息。

    要求请求头(Headers)中携带token字段,其值为JWT令牌。

  2. 携带正确JWT令牌发送其他请求,放行;否则返回错误信息。

首先创建interceptor.LoginCheckInterceptor类,定义拦截器(实现HandlerInterceptor接口,并重写其所有方法):

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
//定义拦截器
@Slf4j
@Component //交给IOC容器管理,提供Bean
public class LoginCheckInterceptor implements HandlerInterceptor {
// 目标资源方法运行前运行,返回true:放行;返回false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取请求的url
String url = request.getRequestURL().toString();
log.info("请求的url:{}", url);

//2.判断请求的url中是否包含/login,如果包含,说明是登录操作,放行。
if (url.contains("login")) {
log.info("登录操作,放行 ...");
return true;
}
//-------------以上,在拦截器注册中已经设置 .excludePathPatterns("/login"),可省略 ----------------------

//3.获取请求头中的令牌(token)
String jwt = request.getHeader("token");

//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if (!StringUtils.hasLength(jwt)) {
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//将Result对象手动转换为json格式
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}

//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) { //解析失败
e.printStackTrace();
log.info("解析令牌失败,返回未登录的错误信息");
Result error = Result.error("NOT_LOGIN");
//将Result对象手动转换为json格式
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}

//6.放行
log.info("令牌合法,放行");
return true;
}

// 目标资源方法运行后运行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ...");
}

// 视图渲染完毕后运行(最后运行)
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion ...");
}
}

说明:

  • preHandle()方法:在目标资源方法运行前运行。返回true:放行;返回false:不放行
  • postHandle()方法:在目标资源方法运行后运行。
  • afterCompletion()方法:在视图渲染完毕后运行(最后运行)。

然后创建config.WebConfig配置类,用于注册拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration  //表示当前类为一个配置类
public class WebConfig implements WebMvcConfigurer {

@Autowired //注入LoginCheckInterceptor对象
private LoginCheckInterceptor loginCheckInterceptor;

//注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); // “/**” -> 拦截所有资源
}
}
  • 通过registry.addInterceptor()方法添加指定的拦截器。
    • addPathPatterns()方法:需要拦截哪些资源
    • excludePathPatterns()方法:不需要拦截哪些资源
拦截路径 含义 举例
/* 一级路径 能匹配/users/depts/login
不能匹配/users/1
/** 任意级路径 能匹配/users/users/1/users/1/2
/users/* /users下的一级路径 能匹配/users/1
不能匹配/users/1/2/depts
/users/** /users下的任意级路径 能匹配/users/users/1/users/1/2
不能匹配/depts

最后创建一个controller.TestController类,用于测试拦截器效果:

1
2
3
4
5
6
7
8
9
@Slf4j
@RestController
public class TestController {
@PostMapping("/test")
public Result Test() {
log.info("测试接口执行(成功放行)");
return Result.success("成功放行!");
}
}
  • 正确的登录请求:

    返回的JWT令牌:

    1
    eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJodWF6aHUiLCJleHAiOjE3MjM0NDcxMzV9.YWwz1QXkeA0Av4vs4G2EKrBq4qlhIQ8YdpZvmZwFjiw
  • 错误的登录请求:

  • 正确的测试请求:

    在请求头(Headers)中添加token字段,其值为需要携带的令牌。

  • 错误的测试请求(未携带JWT令牌):

  • 错误的测试请求(携带错误的JWT令牌):


后记

至此,Web最常见的一些业务操作记录完毕。