【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);
    }
);
相关推荐
摇滚侠2 小时前
Spring SpringMVC SpringBoot SpringCloud SpringAI 分别是做什么的
spring boot·spring·spring cloud
Coder_Boy_2 小时前
从单体并发工具类到分布式并发:思想演进与最佳实践
java·spring boot·分布式·微服务
❀͜͡傀儡师3 小时前
SpringBoot渗透扫描Scan工具
java·spring boot·后端
是梦终空11 小时前
计算机毕业设计266—基于Springboot+Vue3的共享单车管理系统(源代码+数据库)
数据库·spring boot·vue·课程设计·计算机毕业设计·源代码·共享单车系统
m***066811 小时前
SpringBoot项目中读取resource目录下的文件(六种方法)
spring boot·python·pycharm
前路不黑暗@11 小时前
Java项目:Java脚手架项目的公共模块的实现(二)
java·开发语言·spring boot·学习·spring cloud·maven·idea
人道领域11 小时前
Spring核心注解全解析
java·开发语言·spring boot
金牌归来发现妻女流落街头13 小时前
日志级别是摆设吗?
java·spring boot·日志
无尽的沉默15 小时前
SpringBoot整合MyBatis-plus
spring boot·后端·mybatis