【JWT】整合 SpringBoot 实现认证和鉴权

本文将进入企业级实战阶段 ,从环境搭建 开始,一步步实现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 Initializrhttps://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:携带有效令牌访问测试接口

  1. 先调用登录接口获取有效令牌:http://localhost:8989/user/login?name=zhangsan&password=123456

  2. 访问测试接口时,在请求头 中添加token字段,值为获取到的有效令牌

  3. 返回结果

复制代码
{
    "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 令牌失效处理

前端接收到后端返回的令牌失效信息后,需要做统一处理:

  1. 清除本地存储的令牌

  2. 跳转到登录页面,提示用户重新登录

复制代码
// 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);
    }
);
相关推荐
用户908324602732 天前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840823 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解3 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解3 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记3 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者4 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840824 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解4 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
初次攀爬者5 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq
花花无缺5 天前
搞懂@Autowired 与@Resuorce
java·spring boot·后端