本文将进入企业级实战阶段 ,从环境搭建 开始,一步步实现SpringBoot+MyBatis+JWT 的完整整合:搭建基础项目、设计用户表、实现用户登录接口并生成 JWT 令牌、编写SpringMVC 拦截器 实现所有接口的 JWT 统一校验,还会规范前后端的交互格式,解决实际开发中代码冗余 、鉴权不统一的问题。
📚 博客合集: https://www.yuque.com/u12587869/zplytb/ur5ohwqxd2axtiny
一、整合前的准备:技术栈与核心思路
1.1 本次整合使用的技术栈
| 技术框架 / 工具 | 版本 | 作用 |
|---|---|---|
| SpringBoot | 2.7.x | 后端基础框架,快速搭建项目 |
| MyBatis | 2.1.3 | 持久层框架,操作数据库 |
| MySQL | 5.7+ | 关系型数据库,存储用户信息 |
| Druid | 1.1.19 | 数据库连接池,提升性能 |
| Lombok | 1.18.12 | 简化实体类代码,消除 get/set |
| java-jwt | 3.4.0 | JWT 核心工具包,生成 / 解析令牌 |
1.2 核心业务流程
本次整合实现的核心业务是用户登录认证 和接口统一鉴权,整体流程遵循 JWT 的标准认证逻辑,也是企业开发中的通用流程:
1. 前端用户输入用户名+密码,发送POST/GET请求到后端登录接口
2. 后端验证用户名+密码:
- 验证失败:返回登录失败的提示信息
- 验证成功:通过JWT工具类生成令牌,将用户核心信息(ID/用户名)放入载荷,返回令牌给前端
3. 前端接收到令牌后,存储在localStorage/sessionStorage中
4. 前端后续请求所有需要鉴权的接口时,将令牌放入请求头中传递给后端
5. 后端通过自定义拦截器拦截所有请求,统一校验请求头中的令牌:
- 令牌无效/过期/不存在:返回对应的错误信息,拒绝访问
- 令牌有效:放行请求,接口正常处理业务并返回数据
1.3 项目整体结构
为了保证项目的规范性和可维护性,我们采用分层架构开发,核心包结构如下(后续代码将严格遵循此结构):
com.baizhi.jwt
├── config/ # 配置类:拦截器配置、MyBatis配置等
├── controller/ # 控制层:用户登录接口、测试接口
├── entity/ # 实体层:用户实体类
├── mapper/ # 持久层:MyBatis的Mapper接口+XML映射文件
├── service/ # 服务层:接口+实现类,处理业务逻辑
│ ├── impl/ # 服务层实现类
├── utils/ # 工具类:JWT工具类(复用第二篇封装的)
├── JwtApplication # SpringBoot启动类
├── application.yml # 项目配置文件:端口、数据库、MyBatis配置等
二、第一步:搭建 SpringBoot 项目并引入依赖
2.1 创建 SpringBoot 项目
可以通过Spring Initializr (https://start.spring.io/)快速创建 SpringBoot 项目,也可以在 IDEA 中直接创建,核心选择Spring Web依赖即可,其他依赖后续手动引入。
2.2 引入所有核心依赖
在项目的pom.xml中引入本次整合需要的所有依赖,版本与上文技术栈保持一致,确保无版本冲突:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.baizhi</groupId>
<artifactId>jwt-springboot-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jwt-springboot-demo</name>
<description>SpringBoot整合JWT实战项目</description>
<dependencies>
<!-- SpringWeb核心依赖:实现接口开发 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis整合SpringBoot依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
<scope>runtime</scope>
</dependency>
<!-- Druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
<!-- Lombok:简化实体类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<optional>true</optional>
</dependency>
<!-- JWT核心依赖 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<!-- 测试依赖(可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
三、第二步:配置项目核心参数
在resources目录下创建application.yml配置文件,配置端口 、数据库 、MyBatis 、日志等核心参数(注意修改数据库用户名和密码):
# 服务器配置
server:
port: 8989 # 自定义项目端口,避免冲突
servlet:
context-path: / # 项目根路径
# 应用名称
spring:
application:
name: jwt-springboot-demo
# 数据库配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 使用Druid连接池
driver-class-name: com.mysql.jdbc.Driver # MySQL5.7驱动类
url: jdbc:mysql://localhost:3306/jwt?characterEncoding=UTF-8&useSSL=false # 数据库地址,需先创建jwt库
username: root # 你的数据库用户名
password: root # 你的数据库密码
# MyBatis配置
mybatis:
type-aliases-package: com.baizhi.jwt.entity # 实体类别名包路径
mapper-locations: classpath:com/baizhi/jwt/mapper/*.xml # MapperXML文件路径
configuration:
map-underscore-to-camel-case: true # 开启下划线转驼峰(数据库user_name -> 实体类userName)
# 日志配置:开启mapper包的debug日志,方便查看SQL执行语句
logging:
level:
com.baizhi.jwt.mapper: debug
org.springframework.web: info
四、第三步:数据库与实体类开发
4.1 创建用户表
首先在 MySQL 中创建jwt 数据库,然后执行以下 SQL 创建user用户表,表结构极简,仅包含核心的用户 ID、用户名、密码,满足登录认证的基本需求:
-- 创建jwt数据库
CREATE DATABASE IF NOT EXISTS jwt DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE jwt;
-- 创建用户表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '用户主键ID,自增',
`name` VARCHAR(80) NOT NULL COMMENT '用户名(唯一)',
`password` VARCHAR(40) NOT NULL COMMENT '用户密码(实际开发需加密存储)',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='JWT测试用户表';
-- 插入测试数据:用户名zhangsan,密码123456(实际开发需用BCrypt加密)
INSERT INTO `user` VALUES (1, 'zhangsan', '123456');
INSERT INTO `user` VALUES (2, 'lisi', '654321');
注意 :实际企业开发中,用户密码绝对不能明文存储 ,需要通过BCrypt等加密算法加密后存储,本文为了简化演示,暂时使用明文,后续分布式实战篇会优化为加密存储。
4.2 开发用户实体类
在com.baizhi.jwt.entity包下创建User实体类,使用 Lombok 的@Data和@Accessors注解简化代码,字段与数据库表严格对应(开启下划线转驼峰后,无需关注字段命名格式):
package com.baizhi.jwt.entity;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 用户实体类
* 与数据库user表一一对应
*/
@Data // 自动生成get/set/toString/equals/hashCode等方法
@Accessors(chain = true) // 开启链式编程,如user.setId(1).setName("zhangsan")
public class User {
/**
* 用户主键ID
*/
private Integer id;
/**
* 用户名
*/
private String name;
/**
* 用户密码
*/
private String password;
}
五、第四步:持久层与服务层开发
5.1 开发 MyBatis Mapper 接口
在com.baizhi.jwt.mapper包下创建UserMapper接口,定义用户登录查询 的方法,使用@Mapper注解让 MyBatis 扫描并创建代理对象:
package com.baizhi.jwt.mapper;
import com.baizhi.jwt.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户持久层接口
* 操作数据库user表
*/
@Mapper // 标记为MyBatis的Mapper接口
public interface UserMapper {
/**
* 用户登录:根据用户名和密码查询用户
* @param user 封装了用户名和密码的User对象
* @return 查到则返回User对象,查不到则返回null
*/
User login(User user);
}
5.2 开发 Mapper XML 映射文件
在resources/com/baizhi/jwt/mapper目录下创建UserMapper.xml映射文件,编写登录查询的 SQL 语句,与 Mapper 接口的方法一一对应:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 命名空间:对应Mapper接口的全类名 -->
<mapper namespace="com.baizhi.jwt.mapper.UserMapper">
<!-- 用户登录:根据用户名和密码查询用户 -->
<!-- id:对应Mapper接口的方法名;parameterType:参数类型;resultType:返回值类型 -->
<select id="login" parameterType="User" resultType="User">
select id, name, password from user where name = #{name} and password = #{password}
</select>
</mapper>
5.3 开发服务层接口
在com.baizhi.jwt.service包下创建UserService接口,定义业务层的用户登录方法,遵循面向接口编程的思想:
package com.baizhi.jwt.service;
import com.baizhi.jwt.entity.User;
/**
* 用户服务层接口
* 处理用户相关业务逻辑
*/
public interface UserService {
/**
* 用户登录业务
* @param user 封装用户名和密码的User对象
* @return 登录成功返回User对象,失败抛出异常
*/
User login(User user);
}
5.4 开发服务层实现类
在com.baizhi.jwt.service.impl包下创建UserServiceImpl实现类,实现UserService接口,注入UserMapper并实现登录业务逻辑,登录失败时抛出运行时异常,由控制层捕获:
package com.baizhi.jwt.service.impl;
import com.baizhi.jwt.entity.User;
import com.baizhi.jwt.mapper.UserMapper;
import com.baizhi.jwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* 用户服务层实现类
* 实现具体的业务逻辑
*/
@Service // 标记为Spring的服务层组件,交由IOC容器管理
@Transactional // 开启事务管理(增删改时必需,查询时可指定为SUPPORTS)
public class UserServiceImpl implements UserService {
// 注入UserMapper对象(Spring自动注入MyBatis的代理对象)
@Autowired
private UserMapper userMapper;
/**
* 实现用户登录业务
* @param user 封装用户名和密码的User对象
* @return 登录成功返回User对象
*/
@Override
@Transactional(propagation = Propagation.SUPPORTS) // 查询操作,事务为支持性
public User login(User user) {
// 调用Mapper的login方法查询用户
User userDB = userMapper.login(user);
// 判空:查到则返回,查不到则抛出运行时异常
if (userDB != null) {
return userDB;
}
// 异常信息会被控制层捕获,返回给前端
throw new RuntimeException("用户名或密码错误,登录失败~~");
}
}
六、第五步:复用 JWT 工具类
将封装的通用 JWT 工具类 复制到com.baizhi.jwt.utils包下,稍作调整(确保包导入正确),该工具类提供生成令牌 、验证令牌 、获取载荷信息三大核心功能,直接复用即可:
package com.baizhi.jwt.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
/**
* JWT通用工具类
* 提供令牌生成、验证、载荷获取功能
*/
public class JWTUtils {
// 服务端唯一密钥:建议后续移到配置文件中,此处为演示硬编码
private static final String SECRET_KEY = "token!Q2W#E$RW123456abcdefg_baizhi";
// 默认过期时间:30分钟(1800秒)
private static final int DEFAULT_EXPIRE_SECOND = 1800;
/**
* 生成JWT令牌:使用默认过期时间
* @param claimMap 自定义载荷字段(键值对)
* @return 生成的JWT令牌
*/
public static String generateToken(Map<String, String> claimMap) {
return generateToken(claimMap, DEFAULT_EXPIRE_SECOND);
}
/**
* 重载方法:自定义过期时间生成JWT令牌
* @param claimMap 自定义载荷字段(键值对)
* @param expireSecond 过期时间(秒)
* @return 生成的JWT令牌
*/
public static String generateToken(Map<String, String> claimMap, int expireSecond) {
// 设置过期时间
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, expireSecond);
// 构建JWT令牌
com.auth0.jwt.JWTCreator.Builder builder = JWT.create();
// 遍历添加所有自定义载荷字段
claimMap.forEach((key, value) -> builder.withClaim(key, value));
// 设置过期时间并生成签名(HS256算法+密钥)
return builder.withExpiresAt(calendar.getTime())
.sign(Algorithm.HMAC256(SECRET_KEY));
}
/**
* 验证JWT令牌的有效性
* @param token 待验证的令牌
* @throws Exception 验证失败抛出对应异常(签名错误/过期/算法不匹配等)
*/
public static void verifyToken(String token) throws Exception {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET_KEY)).build();
jwtVerifier.verify(token);
}
/**
* 解析JWT令牌并获取DecodedJWT对象
* @param token 待解析的令牌
* @return 解析后的DecodedJWT对象
* @throws Exception 验证失败抛出对应异常
*/
public static DecodedJWT getDecodedJWT(String token) throws Exception {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET_KEY)).build();
return jwtVerifier.verify(token);
}
/**
* 快捷获取载荷中的指定字段值
* @param token 待解析的令牌
* @param key 载荷字段名
* @return 字段值
* @throws Exception 验证失败/字段不存在抛出异常
*/
public static String getClaimValue(String token, String key) throws Exception {
return getDecodedJWT(token).getClaim(key).asString();
}
}
七、第六步:控制层开发 ------ 实现登录接口 + 测试接口
7.1 开发用户登录接口
在com.baizhi.jwt.controller包下创建UserController控制层类,注入UserService,实现用户登录接口 ,登录成功后生成 JWT 令牌并返回给前端,返回结果封装为统一的 Map 格式,方便前端解析:
package com.baizhi.jwt.controller;
import com.baizhi.jwt.entity.User;
import com.baizhi.jwt.service.UserService;
import com.baizhi.jwt.utils.JWTUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 用户控制层
* 处理用户相关的接口请求
*/
@RestController // 组合注解:@Controller + @ResponseBody,返回JSON数据
@RequestMapping("/user") // 接口统一前缀
@Slf4j // 开启日志记录(Lombok),直接使用log对象
public class UserController {
// 注入UserService对象
@Autowired
private UserService userService;
/**
* 用户登录接口
* 访问路径:http://localhost:8989/user/login?name=zhangsan&password=123456
* @param user 前端传递的用户名和密码(SpringMVC自动封装为User对象)
* @return 统一的Map格式结果,包含状态、信息、令牌(登录成功)
*/
@GetMapping("/login")
public Map<String, Object> login(User user) {
// 初始化返回结果Map
Map<String, Object> result = new HashMap<>();
try {
// 日志记录:打印前端传递的用户名和密码(实际开发中密码可脱敏)
log.info("用户名: [{}]", user.getName());
log.info("密码: [{}]", user.getPassword());
// 调用服务层登录方法
User userDB = userService.login(user);
// 准备JWT载荷字段:放入用户ID和用户名(非敏感信息)
Map<String, String> claimMap = new HashMap<>();
claimMap.put("userId", userDB.getId().toString());
claimMap.put("userName", userDB.getName());
// 生成JWT令牌(使用默认30分钟过期)
String token = JWTUtils.generateToken(claimMap);
// 封装登录成功的结果
result.put("state", true); // 状态:成功
result.put("msg", "登录成功!!!"); // 提示信息
result.put("token", token); // 核心:返回JWT令牌
} catch (Exception e) {
// 捕获所有异常(服务层的业务异常+其他未知异常)
e.printStackTrace();
// 封装登录失败的结果
result.put("state", false); // 状态:失败
result.put("msg", e.getMessage()); // 异常信息作为提示
}
// 返回结果(SpringMVC自动转为JSON)
return result;
}
}
7.2 开发测试接口
为了测试后续的拦截器鉴权功能,创建TestController,实现一个需要鉴权的测试接口,访问该接口时必须携带有效的 JWT 令牌:
package com.baizhi.jwt.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 测试控制层
* 提供需要鉴权的测试接口
*/
@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {
/**
* 测试接口:需要携带有效的JWT令牌才能访问
* 访问路径:http://localhost:8989/test/hello
* @return 测试结果
*/
@GetMapping("/hello")
public Map<String, Object> hello() {
Map<String, Object> result = new HashMap<>();
result.put("state", true);
result.put("msg", "接口访问成功!这是需要鉴权的测试接口");
result.put("data", "Hello JWT + SpringBoot!");
return result;
}
}
7.3 测试登录接口
启动 SpringBoot 项目,通过浏览器 / Postman/Curl访问登录接口,传入正确的用户名和密码:
请求地址:http://localhost:8989/user/login?name=zhangsan&password=123456
请求方式:GET
返回结果(JSON):登录成功,返回 JWT 令牌
{
"msg": "登录成功!!!",
"state": true,
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6InpoYW5nc2FuIiwidXNlcklkIjoiMSIsImV4cCI6MTcxNzUwNjE2NH0.5a9s8d7f6g5h4j3k2l1m0n9o8p7q6r5s4t3u2v1w0"
}
若传入错误的用户名 / 密码,返回结果:
{
"msg": "用户名或密码错误,登录失败~~",
"state": false
}
八、第七步:自定义拦截器 ------ 实现 JWT 统一校验
8.1 问题分析
此时直接访问测试接口http://localhost:8989/test/hello,无需携带令牌也能正常访问,这显然不符合需求。如果在每个需要鉴权的接口中都编写 JWT 验证代码,会导致代码大量冗余 、维护困难。
解决方案:使用SpringMVC 的拦截器(Interceptor) ,拦截所有请求,统一校验 JWT 令牌 ,无需在每个接口中重复编写验证代码,符合AOP 面向切面编程的思想。
8.2 核心拦截逻辑
自定义拦截器需要实现HandlerInterceptor接口,重写preHandle方法(请求处理前执行),核心逻辑:
1. 从请求头中获取JWT令牌(前端将令牌放入请求头的token字段)
2. 调用JWT工具类的verifyToken方法验证令牌:
- 令牌不存在:返回“请先登录,未携带令牌”
- 令牌无效/过期/签名错误:捕获对应异常,返回具体的错误信息
- 令牌有效:返回true,放行请求,接口正常处理
3. 验证失败时,将错误信息封装为JSON,返回给前端,返回false,拒绝访问
8.3 开发 JWT 拦截器
在com.baizhi.jwt.config包下创建JwtTokenInterceptor拦截器类,实现HandlerInterceptor接口,完成 JWT 统一校验逻辑,带详细注释:
package com.baizhi.jwt.config;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.baizhi.jwt.utils.JWTUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* JWT令牌拦截器
* 统一校验所有请求的JWT令牌
*/
public class JwtTokenInterceptor implements HandlerInterceptor {
/**
* 请求处理前执行:核心的JWT校验逻辑
* @param request 请求对象
* @param response 响应对象
* @param handler 处理器
* @return true:放行;false:拒绝访问
* @throws Exception 异常
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从请求头中获取JWT令牌(前端约定将令牌放入token字段)
String token = request.getHeader("token");
// 2. 初始化返回结果Map
Map<String, Object> result = new HashMap<>();
try {
// 3. 验证令牌:无异常则验证通过
JWTUtils.verifyToken(token);
// 4. 验证通过,放行请求
return true;
} catch (TokenExpiredException e) {
// 令牌过期异常
result.put("state", false);
result.put("msg", "Token已经过期,请重新登录!!!");
} catch (SignatureVerificationException e) {
// 签名不一致异常(令牌被篡改/密钥错误)
result.put("state", false);
result.put("msg", "签名错误,令牌无效!!!");
} catch (AlgorithmMismatchException e) {
// 算法不匹配异常
result.put("state", false);
result.put("msg", "加密算法不匹配,令牌无效!!!");
} catch (InvalidClaimException e) {
// 载荷字段失效异常
result.put("state", false);
result.put("msg", "载荷信息无效,令牌无效!!!");
} catch (NullPointerException e) {
// 令牌不存在异常(请求头中无token字段)
result.put("state", false);
result.put("msg", "请先登录,未携带令牌!!!");
} catch (Exception e) {
// 其他未知异常
e.printStackTrace();
result.put("state", false);
result.put("msg", "无效token,访问被拒绝~~~");
}
// 5. 验证失败:将结果转为JSON,返回给前端
// 设置响应头:返回JSON格式,编码为UTF-8,解决中文乱码
response.setContentType("application/json;charset=UTF-8");
// 将Map转为JSON字符串
String json = new ObjectMapper().writeValueAsString(result);
// 将JSON写入响应体
response.getWriter().println(json);
// 6. 返回false,拒绝访问
return false;
}
}
8.4 配置拦截器 ------ 注册并设置拦截规则
创建WebConfig配置类,实现WebMvcConfigurer接口,将自定义的 JWT 拦截器注册到 Spring 容器 ,并设置拦截规则 和放行规则(如登录接口需要放行,无需鉴权):
package com.baizhi.jwt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* SpringMVC配置类
* 注册拦截器并设置拦截规则
*/
@Configuration // 标记为Spring的配置类,交由IOC容器管理
public class WebConfig implements WebMvcConfigurer {
/**
* 将JWT拦截器注册为Spring的Bean
* @return JwtTokenInterceptor对象
*/
@Bean
public JwtTokenInterceptor jwtTokenInterceptor() {
return new JwtTokenInterceptor();
}
/**
* 配置拦截器:设置拦截规则和放行规则
* @param registry 拦截器注册器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtTokenInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/user/login"); // 放行登录接口,无需鉴权
}
}
拦截规则说明:
-
addPathPatterns("/**"):拦截项目中所有的接口请求 -
excludePathPatterns("/user/login"):放行登录接口,因为用户需要先登录获取令牌,登录接口本身不需要鉴权 -
实际开发中,可根据需求放行更多接口,如静态资源、注册接口、验证码接口等。
九、第八步:整体功能测试
9.1 测试 1:未携带令牌访问测试接口
访问测试接口http://localhost:8989/test/hello,请求头中不携带任何令牌,返回结果:
{
"msg": "请先登录,未携带令牌!!!",
"state": false
}
结果:拦截器生效,拒绝访问,符合预期。
9.2 测试 2:携带无效令牌访问测试接口
请求头中携带错误的令牌(如随意输入一串字符串),访问测试接口,返回结果:
{
"msg": "签名错误,令牌无效!!!",
"state": false
}
结果:拦截器校验出令牌无效,拒绝访问,符合预期。
9.3 测试 3:携带有效令牌访问测试接口
-
先调用登录接口获取有效令牌:
http://localhost:8989/user/login?name=zhangsan&password=123456 -
访问测试接口时,在请求头 中添加
token字段,值为获取到的有效令牌 -
返回结果:
{
"data": "Hello JWT + SpringBoot!",
"msg": "接口访问成功!这是需要鉴权的测试接口",
"state": true
}
结果:拦截器校验令牌有效,放行请求,接口正常访问,符合预期。
9.4 测试 4:令牌过期后访问测试接口
修改 JWT 工具类的过期时间为10 秒,重新生成令牌,10 秒后携带该令牌访问测试接口,返回结果:
{
"msg": "Token已经过期,请重新登录!!!",
"state": false
}
结果:拦截器校验出令牌过期,拒绝访问,符合预期。
十、前后端交互规范
在实际开发中,前后端需要约定好JWT 令牌的传递和存储规范,避免出现交互问题,以下是企业开发中的通用规范:
10.1 令牌存储
前端获取到令牌后,建议存储在localStorage 中(持久化,关闭浏览器后仍存在),也可存储在sessionStorage中(会话级,关闭浏览器后失效):
// 登录成功后,存储令牌到localStorage
localStorage.setItem("token", res.data.token);
10.2 令牌传递
前端后续所有请求,通过请求头 传递令牌,建议使用token 作为字段名,也可使用标准的Authorization 字段(值为Bearer + 令牌):
// axios请求拦截器,统一为所有请求添加令牌
axios.interceptors.request.use(config => {
// 从localStorage中获取令牌
const token = localStorage.getItem("token");
// 若有令牌,添加到请求头
if (token) {
config.headers.token = token;
// 或使用标准格式:config.headers.Authorization = "Bearer " + token;
}
return config;
});
10.3 令牌失效处理
前端接收到后端返回的令牌失效信息后,需要做统一处理:
-
清除本地存储的令牌
-
跳转到登录页面,提示用户重新登录
// axios响应拦截器,统一处理令牌失效
axios.interceptors.response.use(
res => res,
err => {
if (err.response && err.response.data && !err.response.data.state) {
// 令牌失效的提示信息,可根据实际情况扩展
const msg = err.response.data.msg;
if (msg.includes("过期") || msg.includes("未携带令牌") || msg.includes("签名错误")) {
// 清除令牌
localStorage.removeItem("token");
// 提示用户
alert(msg);
// 跳转到登录页面
window.location.href = "/login.html";
}
}
return Promise.reject(err);
}
);