# SpringBoot集成JWT企业级实战教程

本教程基于IDEA 2023 Ultimate、JDK17、Maven3.9.4、SpringBoot3.5.10实现从零搭建JWT认证的企业级SpringBoot项目,项目名SpringBoot-JWT,根包cc.hep,全程采用企业级工程结构而非简易Demo,包含JWT核心认证、统一异常处理、统一返回结果、请求拦截、参数校验等企业开发必备特性。

开发环境确认

  • IDE:IntelliJ IDEA 2023 Ultimate
  • JDK:17(SpringBoot3.x最低要求JDK17)
  • SpringBoot:3.5.10
  • Maven:3.9.4
  • JWT:JJWT 0.12.6(SpringBoot3.5.10最新兼容版本,官方推荐的JWT实现框架)
  • 辅助工具:Lombok(简化实体代码)、Spring Validation(参数校验)

# 1. 初始化SpringBoot项目

004

# 2. pom.xml配置

这一步是项目基础,需引入JWT核心依赖、SpringWeb、参数校验、Lombok等企业级必备依赖,同时配置Maven编译插件适配JDK17、跳过test测试,所有依赖版本均做兼容性适配,直接复制覆盖项目原有pom.xml即可。

完整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>
	<!-- SpringBoot父工程依赖 -->
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.10</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<!-- 项目基础信息,与初始化一致 -->
	<groupId>cc.hep</groupId>
	<artifactId>SpringBoot-JWT</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>SpringBoot-JWT</name>
	<description>SpringBoot3.5.10集成JWT企业级实战项目</description>

	<properties>
		<java.version>17</java.version>
		<!-- JWT版本,SpringBoot3.5.10最新兼容版 -->
		<jjwt.version>0.12.6</jjwt.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
	</properties>

	<dependencies>
		<!-- SpringWeb核心依赖,实现接口开发 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<!-- JWT核心依赖:JJWT API + 实现 + Jackson序列化(适配JSON) -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-api</artifactId>
			<version>${jjwt.version}</version>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-impl</artifactId>
			<version>${jjwt.version}</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-jackson</artifactId>
			<version>${jjwt.version}</version>
			<scope>runtime</scope>
		</dependency>

		<!-- Lombok,简化实体/工具类代码,消除get/set/构造器等模板代码 -->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>

		<!-- Spring Validation,实现接口参数校验(企业级必备) -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>

		<!-- SpringBoot启动器 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<!-- Apache通用编码工具包,处理十六进制/Base64编码,解决JWT秘钥生成问题 -->
		<dependency>
			<groupId>commons-codec</groupId>
			<artifactId>commons-codec</artifactId>
			<version>1.16.1</version>
		</dependency>

		<!-- SpringBoot测试依赖(本教程跳过,保留依赖不影响) -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<!-- SpringBoot打包插件 -->
			<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>

			<!-- Maven编译插件,适配JDK17 -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.11.0</version>
				<configuration>
					<source>17</source>
					<target>17</target>
					<encoding>UTF-8</encoding>
				</configuration>
			</plugin>

			<!-- 跳过test测试(核心配置,满足教程要求) -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<version>3.2.2</version>
				<configuration>
					<skipTests>true</skipTests>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

关键配置说明

  1. JJWT依赖:SpringBoot3.x放弃了对旧版JJWT(0.9.x)的支持,必须使用0.12+版本,且需引入jjwt-api(接口)、jjwt-impl(运行时实现)、jjwt-jackson(JSON序列化,支持将对象存入JWT)三个依赖;
  2. 跳过test测试:通过maven-surefire-plugin的skipTests配置为true,彻底跳过Maven的test阶段;
  3. JDK17适配:maven-compiler-plugin的source和target均设为17,保证编译与运行环境一致;
  4. Lombok可选配置:打包时排除Lombok,避免依赖侵入。

# 3. 搭建企业级项目目录结构

区别于简易Demo,企业级项目需遵循职责分离、分层开发原则,便于后续维护和扩展。基于根包cc.hep,创建如下子包和核心目录,手动创建即可,IDEA会自动识别Spring组件:

cc.hep
├── SpringBootJwtApplication.java  # 项目启动类(自动生成,无需修改)
├── config/                        # 配置层:JWT配置、Web配置(跨域)等
├── constant/                      # 常量层:JWT常量、接口常量等(企业级必备)
├── entity/                        # 实体层:数据库实体、请求/业务实体(如User、登录入参实体)
├── enums/                         # 枚举层:响应码、用户状态、业务枚举(消除魔法值)
├── exception/                     # 异常层:自定义异常、全局异常处理器(统一异常返回)
├── filter/                        # 过滤器层:JWT认证过滤器(核心,拦截所有请求做token校验)
├── service/                       # 业务层:接口+实现,处理核心业务逻辑(如登录、用户管理)
│   ├── impl/                      # 业务层实现类
│   └── UserService.java           # 业务接口
├── controller/                    # 控制层:对外提供REST接口(认证、测试、业务接口)
├── util/                          # 工具层:JWT工具类、通用工具类(如加密、日期)
└── vo/                            # 视图对象层:统一返回结果封装(与前端交互的标准格式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

核心设计思路:

  • 统一实体管理:将数据库实体、接口请求实体统一放在entity包中,便于管理和维护;
  • 枚举消除魔法值:如响应码200/400/401用枚举表示,提高代码可读性;
  • 统一异常/返回:所有接口返回格式一致,所有异常由全局处理器捕获,避免前端处理多格式返回;
  • 过滤器做JWT校验:拦截所有请求,仅放行认证接口,实现无状态的JWT认证。

# 4. 核心配置文件编写

使用YAML格式配置(企业级主流),包含服务基础配置、JWT核心配置,JWT配置采用可配置化(而非硬编码),便于后续环境切换(开发/测试/生产)。删除自动生成的application.properties,新建application.yml,路径为src/main/resources/application.yml。或者直接用application-prd.yml形式的也行。

完整application.yml代码

# 服务基础配置
server:
  port: 8080 # 服务端口,可自定义
  servlet:
    context-path: / # 项目根路径,企业级可根据需求修改(如/api)

# Spring应用配置
spring:
  application:
    name: SpringBoot-JWT # 项目名,与初始化一致
  profiles:
    active: dev # 激活开发环境,后续可扩展test/prod环境

# JWT核心配置(最终版,直接复制替换)
jwt:
  header: Authorization # 与Postman一致,无空格
  secret: 8a7f2d9e3b5c104f6e890a2b4d7c9e1f0c3a5b7d9f2e4c6a8b0d2f4e6a8c0e1f # 替换为你运行main生成的64位秘钥
  expire: 7200 # 7200秒=2小时,数字类型,不加引号
  tokenPrefix: Bearer  # 注意:Bearer后面**一个空格**,不能多不能少
  excludePaths: /auth/login # 仅放行登录接口,多个用逗号分隔
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

JWT配置关键说明

  1. 秘钥(secret):JJWT 0.12+要求HMAC-SHA算法的秘钥长度至少256位(32字节),对应64位十六进制字符(1字节=2位十六进制),禁止手动编写,必须运行JwtUtil.main()生成合法秘钥,否则会出现奇数字符解码失败;
  2. 过期时间(expire):单位毫秒,2小时=7200000ms,企业级可根据业务调整(如移动端设为1小时,PC端设为8小时);
  3. token格式:遵循OAuth2.0规范,Bearer + 一个空格 + token,前端请求时需按此格式传递;
  4. 放行路径(exclude-paths):认证接口/auth/login无需token校验,必须放行,后续可添加静态资源、swagger文档等路径(多个路径用英文逗号分隔,如/auth/login,/swagger/**)。

# 5. 通用基础组件开发

这一步实现统一返回结果、枚举常量、自定义异常、全局异常处理器,是企业级项目的基础,保证所有接口返回格式一致、异常处理统一,避免重复代码。

# 5.1 枚举层(enums):响应码枚举

创建cc.hep.enums.ResponseCodeEnum,封装所有业务响应码、提示信息,消除魔法值,企业级可根据业务扩展。

package cc.hep.enums;

import lombok.Getter;

/**
 * 全局响应码枚举(企业级必备,消除魔法值)
 */
@Getter
public enum ResponseCodeEnum {
    // 成功
    SUCCESS(200, "操作成功"),
    // 客户端错误
    BAD_REQUEST(400, "请求参数错误"),
    UNAUTHORIZED(401, "未认证,请先登录"),
    FORBIDDEN(403, "无权限访问"),
    NOT_FOUND(404, "请求资源不存在"),
    // 服务端错误
    INTERNAL_SERVER_ERROR(500, "服务器内部错误"),
    // JWT专属错误
    JWT_TOKEN_EXPIRED(40101, "token已过期"),
    JWT_TOKEN_INVALID(40102, "token无效或格式错误"),
    JWT_TOKEN_EMPTY(40103, "请求头中无token");

    // 响应码
    private final Integer code;
    // 响应信息
    private final String msg;

    // 构造器
    ResponseCodeEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}
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

# 5.2 视图对象层(vo):统一返回结果

创建两个类:Result(统一返回结果实体)、ResultUtil(返回结果工具类),所有接口均通过此工具类返回结果,保证前端交互格式统一。

统一返回结果实体:cc.hep.vo.Result

package cc.hep.vo;

import cc.hep.enums.ResponseCodeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 全局统一返回结果
 * 所有接口均返回此对象,实现前后端交互格式统一
 * Serializable:支持序列化,企业级分布式项目必备
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> implements Serializable {
    // 响应码(对应枚举)
    private Integer code;
    // 响应信息
    private String msg;
    // 响应数据(泛型,支持任意数据类型)
    private T data;

    // 成功返回:无数据
    public static <T> Result<T> success() {
        return new Result<>(ResponseCodeEnum.SUCCESS.getCode(), ResponseCodeEnum.SUCCESS.getMsg(), null);
    }

    // 成功返回:有数据
    public static <T> Result<T> success(T data) {
        return new Result<>(ResponseCodeEnum.SUCCESS.getCode(), ResponseCodeEnum.SUCCESS.getMsg(), data);
    }

    // 失败返回:自定义响应码和信息
    public static <T> Result<T> fail(Integer code, String msg) {
        return new Result<>(code, msg, null);
    }

    // 失败返回:使用枚举响应码
    public static <T> Result<T> fail(ResponseCodeEnum responseCodeEnum) {
        return new Result<>(responseCodeEnum.getCode(), responseCodeEnum.getMsg(), null);
    }
}
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

统一返回工具类:cc.hep.vo.ResultUtil

package cc.hep.vo;

import cc.hep.enums.ResponseCodeEnum;

/**
 * 统一返回结果工具类
 * 所有方法返回Result<T>类型,适配SpringMVC控制器返回
 */
public class ResultUtil {

    // 成功:无返回数据
    public static Result<Void> ok() {
        return Result.success();
    }

    // 成功:有返回数据(泛型适配任意类型:Map/字符串/实体等)
    public static <T> Result<T> ok(T data) {
        return Result.success(data);
    }

    // 失败:使用响应码枚举
    public static Result<Void> fail(ResponseCodeEnum enums) {
        return Result.fail(enums);
    }

    // 失败:自定义状态码+消息
    public static Result<Void> fail(Integer code, String msg) {
        return Result.fail(code, msg);
    }

    // 失败:仅自定义消息(默认500状态码)
    public static Result<Void> fail(String msg) {
        return Result.fail(ResponseCodeEnum.INTERNAL_SERVER_ERROR.getCode(), msg);
    }
}
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

# 5.3 异常层:自定义异常+全局异常处理器

企业级项目中,禁止直接抛出原生异常,需自定义业务异常,并通过全局异常处理器捕获所有异常(自定义异常、原生异常、参数校验异常),统一封装为Result对象返回,避免前端接收到非标准的异常信息。

自定义业务异常:cc.hep.exception.BusinessException

package cc.hep.exception;

import cc.hep.enums.ResponseCodeEnum;
import lombok.Getter;

/**
 * 自定义业务异常
 * 新增构造器:支持「枚举+自定义消息」,解决UserServiceImpl中报错问题
 */
@Getter
public class BusinessException extends RuntimeException {
    // 异常响应码
    private final Integer code;

    // 构造器1:仅传响应码枚举(使用枚举默认消息)
    public BusinessException(ResponseCodeEnum responseCodeEnum) {
        super(responseCodeEnum.getMsg());
        this.code = responseCodeEnum.getCode();
    }

    // 构造器2:传自定义码+自定义消息(兼容特殊场景)
    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    // 构造器3:新增!传响应码枚举+自定义消息(解决本次报错的核心)
    public BusinessException(ResponseCodeEnum responseCodeEnum, String message) {
        super(message); // 异常信息使用自定义的
        this.code = responseCodeEnum.getCode(); // 状态码使用枚举的
    }
}
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

全局异常处理器:cc.hep.exception.GlobalExceptionHandler

package cc.hep.exception;

import cc.hep.enums.ResponseCodeEnum;
import cc.hep.vo.Result;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.stream.Collectors;

/**
 * 全局异常处理器
 * RestControllerAdvice:捕获所有@RestController的异常,返回JSON格式
 * 统一处理:自定义业务异常、参数校验异常、原生运行时异常
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理自定义业务异常(核心,企业级主要异常)
     */
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
        // 打印异常日志(企业级必备,便于问题排查)
        log.error("业务异常:请求路径{},异常码{},异常信息{}", request.getRequestURI(), e.getCode(), e.getMessage());
        return Result.fail(e.getCode(), e.getMessage());
    }

    /**
     * 处理参数校验异常(@Valid/@NotBlank等注解触发)
     */
    @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
    public Result<Void> handleValidationException(Exception e, HttpServletRequest request) {
        // 解析校验失败的字段和信息
        String errorMsg;
        if (e instanceof MethodArgumentNotValidException) {
            errorMsg = ((MethodArgumentNotValidException) e).getBindingResult().getFieldErrors().stream()
                    .map(FieldError::getDefaultMessage)
                    .collect(Collectors.joining(";"));
        } else {
            errorMsg = ((BindException) e).getBindingResult().getFieldErrors().stream()
                    .map(FieldError::getDefaultMessage)
                    .collect(Collectors.joining(";"));
        }
        log.error("参数校验异常:请求路径{},异常信息{}", request.getRequestURI(), errorMsg);
        return Result.fail(ResponseCodeEnum.BAD_REQUEST.getCode(), errorMsg);
    }

    /**
     * 处理所有未捕获的运行时异常(兜底,防止漏捕)
     */
    @ExceptionHandler(RuntimeException.class)
    public Result<Void> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
        // 打印异常堆栈(企业级必备,便于排查未知错误)
        log.error("运行时异常:请求路径{}", request.getRequestURI(), e);
        return Result.fail(ResponseCodeEnum.INTERNAL_SERVER_ERROR);
    }
}
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

# 5.4 常量层:JWT常量

创建cc.hep.constant.JwtConstant,封装JWT中的声明常量(如用户名、过期时间),消除魔法值,便于后续维护:

package cc.hep.constant;

/**
 * JWT常量类:封装JWT的Claim声明常量
 */
public class JwtConstant {
    // JWT中存储用户名的key
    public static final String JWT_CLAIM_USERNAME = "username";
    // JWT的主题(可选,标识token用途)
    public static final String JWT_SUBJECT = "SpringBoot-JWT-AUTH";
    // JWT的签发者(可选,标识项目)
    public static final String JWT_ISSUER = "cc.hep";
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 6. JWT核心组件开发

这是本教程的核心,实现JWT配置绑定、JWT工具类,工具类做了鲁棒性优化(循环移除Token前缀、彻底去空格),提供生成token、解析token、校验token、生成安全秘钥等核心方法,完全遵循JJWT 0.12+的API规范,适配SpringBoot3.5.10。

# 6.1 配置层:JWT配置绑定类

创建cc.hep.config.JwtProperties,通过SpringBoot配置绑定功能,将application.yml中的jwt配置绑定到该类,实现可配置化,而非硬编码,企业级项目中推荐此方式:

package cc.hep.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * JWT配置绑定类(最终完整版)
 * 补全:excludePaths属性(解决过滤器调用报错)
 * 所有属性带默认值,配置文件可缺省
 */
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
    /** 请求头名称:Authorization(与Postman一致) */
    private String header = "Authorization";
    /** 64位十六进制秘钥(32字节,运行JwtUtil.main生成) */
    private String secret;
    /** Token过期时间(秒),默认7200秒=2小时 */
    private Long expire = 7200L;
    /** Token前缀:Bearer (注意:后面必须有一个空格!) */
    private String tokenPrefix = "Bearer ";
    /** JWT放行路径,多个用逗号分隔,默认仅放行登录接口 */
    private String excludePaths = "/auth/login";
}
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

# 6.2 工具层:JWT核心工具类

创建cc.hep.util.JwtUtil,封装JWT的所有核心操作,优化了Token前缀移除逻辑(循环移除+彻底去空格,解决双重前缀问题),包含生成安全秘钥、生成token、解析token、校验token等核心功能,加入异常处理,统一抛出业务异常:

package cc.hep.util;

import cc.hep.config.JwtProperties;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * JWT工具类(JJWT 0.12.6 终极无报错版)
 * 修复:移除jti()快捷方法,改为标准Claims设置jti(全版本兼容)
 * 包含:秘钥校验/过期时间计算/Token生成解析/前缀移除 全功能
 */
@Slf4j
@Component
public class JwtUtil {

    private final JwtProperties jwtProperties;
    private final SecretKey secretKey;

    /**
     * 构造器注入配置,初始化32字节HS256秘钥
     * 声明抛出DecoderException,处理十六进制秘钥解码异常
     */
    public JwtUtil(JwtProperties jwtProperties) throws DecoderException {
        this.jwtProperties = jwtProperties;
        // 秘钥非空校验
        if (jwtProperties.getSecret() == null || jwtProperties.getSecret().isBlank()) {
            log.error("JWT初始化失败:秘钥未配置!请在application.yml中配置64位十六进制jwt.secret");
            throw new IllegalArgumentException("JWT秘钥不能为空");
        }
        // 解码64位十六进制秘钥为32字节数组
        byte[] keyBytes = Hex.decodeHex(jwtProperties.getSecret());
        // 秘钥长度校验(HS256必须32字节)
        if (keyBytes.length != 32) {
            log.error("JWT初始化失败:秘钥长度错误!要求64位十六进制(32字节),当前{}位", jwtProperties.getSecret().length());
            throw new IllegalArgumentException("JWT秘钥必须是32字节(64位十六进制字符串)");
        }
        this.secretKey = Keys.hmacShaKeyFor(keyBytes);
        log.info("JWT工具类初始化成功,HS256秘钥长度:{}字节", keyBytes.length);
    }

    /**
     * 生成JWT Token(无Bearer前缀,JJWT 0.12.6 无任何报错)
     * @param username 登录用户名,存入Token载荷
     * @return 标准JWT Token字符串
     */
    public String generateToken(String username) {
        // 1. 构建JWT载荷(包含自定义信息+标准jti字段,全版本兼容)
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", username); // 自定义用户名
        claims.put("sub", "SpringBoot-JWT-AUTH"); // 标准字段:主题
        claims.put("iss", "cc.hep"); // 标准字段:签发者
        claims.put("jti", UUID.randomUUID().toString()); // 标准字段:JWT唯一ID(替换原jti()方法,核心修复点)

        // 2. 计算过期时间:当前时间 + 配置秒数 → 转毫秒(避免7秒过期)
        long currentTime = System.currentTimeMillis();
        long expireTime = currentTime + jwtProperties.getExpire() * 1000L;

        // 3. 生成Token(JJWT 0.12.6 官方标准写法,无任何快捷方法,全兼容)
        return Jwts.builder()
                .claims(claims) // 存入所有载荷(含自定义+标准jti)
                .issuedAt(new Date(currentTime)) // 标准字段:签发时间
                .expiration(new Date(expireTime)) // 标准字段:过期时间
                .signWith(secretKey) // 自动匹配HS256(32字节秘钥),无需指定算法
                .compact();
    }

    /**
     * 解析并校验Token有效性
     * 过期/签名错误/非法Token会抛出运行时异常,由全局异常处理器捕获
     */
    public Claims parseToken(String token) {
        try {
            return Jwts.parser()
                    .verifyWith(secretKey) // 用HS256秘钥验证签名
                    .build()
                    .parseSignedClaims(token) // 解析签名后的Token
                    .getPayload(); // 获取所有载荷(含jti、username等)
        } catch (ExpiredJwtException e) {
            log.error("JWT Token过期:{}", e.getMessage());
            throw new RuntimeException("Token已过期,请重新登录");
        } catch (MalformedJwtException | SignatureException e) {
            log.error("JWT Token无效/签名被篡改:{}", e.getMessage());
            throw new RuntimeException("Token无效,请勿非法修改");
        } catch (IllegalArgumentException e) {
            log.error("JWT Token格式错误/为空:{}", e.getMessage());
            throw new RuntimeException("Token格式错误,请检查请求头");
        }
    }

    /**
     * 从Token中获取登录用户名
     */
    public String getUsernameFromToken(String token) {
        return parseToken(token).get("username", String.class);
    }

    /**
     * 移除Token的Bearer前缀(鲁棒性处理:非空+去空格)
     */
    public String removeTokenPrefix(String token) {
        if (token != null && token.startsWith(jwtProperties.getTokenPrefix())) {
            return token.substring(jwtProperties.getTokenPrefix().length()).trim();
        }
        return token;
    }

    /**
     * 生成安全的HS256秘钥(32字节→64位十六进制)
     * 运行main方法生成后,直接复制到application.yml的jwt.secret中
     */
    public static String generateSecureSecret() {
        SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        return Hex.encodeHexString(key.getEncoded());
    }

    // 测试生成秘钥(直接运行,复制控制台输出到配置文件)
    public static void main(String[] args) {
        String secureSecret = generateSecureSecret();
        log.info("=== 生成的256位HS256秘钥(64位十六进制)===\n{}", secureSecret);
        log.info("秘钥字符长度:{}(必须为64,否则初始化失败)", secureSecret.length());
    }
}
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

# 6.3 JWT工具类核心优化&说明

  1. 安全秘钥生成:main方法直接生成64位十六进制秘钥,运行后复制到application.yml替换占位符,彻底解决秘钥奇数字符解码失败问题;
  2. 前缀移除优化:removeTokenPrefix改为循环移除前缀+彻底去空格,即使Token出现双重/多重Bearer前缀,也能完全移除,同时校验Token是否含空格(JWT规范禁止);
  3. 异常日志优化:细化JWT异常日志,打印具体错误信息(如签名篡改、含空格),便于问题排查;
  4. Token返回规范:生成的Token为纯密文,不含任何前缀,遵循行业标准,避免前端/工具自动拼接导致的前缀叠加;
  5. 配置初始化:通过@PostConstruct注解,在Bean初始化后将注入的JwtProperties赋值给静态变量,实现静态方法的配置调用;
  6. 秘钥转换:将配置中的字符串秘钥转换为JJWT要求的SecretKey对象,使用Keys.hmacShaKeyFor()方法,要求秘钥至少32字节。

# 7. 业务层基础开发

基于企业级开发原则,实现登录入参实体、用户实体、用户服务接口+实现,业务层处理核心的登录逻辑,模拟用户数据(后续可直接整合MyBatis-Plus/MyBatis连接数据库,无需修改上层代码)。

# 7.1 实体层:登录入参实体

创建cc.hep.entity.LoginReqEntity,作为登录接口的入参对象,通过Spring Validation注解做参数校验,企业级项目中所有接口入参均需做校验:

package cc.hep.entity;

import jakarta.validation.constraints.NotBlank;
import lombok.Data;

/**
 * 登录请求实体:接口入参对象
 * 与前端交互的参数,做参数校验,避免无效请求
 */
@Data
public class LoginReqEntity {
    /**
     * 用户名
     * NotBlank:非空且非空白字符,message为校验失败提示
     */
    @NotBlank(message = "用户名不能为空")
    private String username;

    /**
     * 密码
     */
    @NotBlank(message = "密码不能为空")
    private String password;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 7.2 实体层:用户实体

创建cc.hep.entity.User,作为核心业务实体,后续可直接与数据库表映射,使用Lombok简化代码:

package cc.hep.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 用户实体:核心业务实体(后续可整合MyBatis-Plus添加@TableName等注解)
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    // 用户ID
    private Long id;
    // 用户名
    private String username;
    // 密码(企业级生产环境需加密存储,如BCrypt)
    private String password;
    // 昵称
    private String nickname;
    // 用户状态(0-禁用,1-正常)
    private Integer status;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 7.3 业务层:用户服务接口+实现

遵循面向接口编程原则,创建服务接口和实现类,处理登录核心业务:用户名密码校验,校验通过则生成纯密文JWT Token返回,模拟用户数据(企业级可替换为数据库查询)。

服务接口:cc.hep.service.UserService

package cc.hep.service;

import cc.hep.entity.LoginReqEntity;

/**
 * 用户服务接口:面向接口编程,企业级必备
 */
public interface UserService {
    /**
     * 用户登录
     * @param loginReqEntity 登录入参
     * @return JWT Token(纯密文,不含前缀Bearer)
     */
    String login(LoginReqEntity loginReqEntity);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

服务实现类:cc.hep.service.impl.UserServiceImpl

package cc.hep.service.impl;

import cc.hep.entity.LoginReqEntity;
import cc.hep.entity.User;
import cc.hep.enums.ResponseCodeEnum;
import cc.hep.exception.BusinessException;
import cc.hep.service.UserService;
import cc.hep.util.JwtUtil; // 保留导入
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

/**
 * 用户服务实现类(终极无报错版)
 * 修复:静态上下文调用JwtUtil非静态方法的错误(构造器注入JwtUtil实例)
 */
@Slf4j
@Service
public class UserServiceImpl implements UserService {

    // 模拟数据库用户表
    private static final Map<String, User> USER_MAP = new HashMap<>();
    // 1. 声明JwtUtil实例(关键修改点1:新增)
    private final JwtUtil jwtUtil;

    // 静态代码块:初始化模拟用户
    static {
        USER_MAP.put("admin", new User(1L, "admin", "123456", "系统管理员", 1));
        USER_MAP.put("test", new User(2L, "test", "123456", "测试用户", 1));
    }

    // 2. 构造器注入JwtUtil实例(关键修改点2:新增,Spring自动注入,无需@Autowired)
    public UserServiceImpl(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    public String login(LoginReqEntity loginReqEntity) {
        String username = loginReqEntity.getUsername();
        String password = loginReqEntity.getPassword();
        log.info("用户登录请求:用户名=[{}]", username);

        // 1. 校验用户名是否存在
        User user = USER_MAP.get(username);
        if (user == null) {
            log.error("登录失败:用户名[{}]不存在", username);
            throw new BusinessException(ResponseCodeEnum.UNAUTHORIZED, "用户名或密码错误");
        }

        // 2. 校验密码是否匹配(生产环境请用BCrypt加密,禁止明文)
        if (!password.equals(user.getPassword())) {
            log.error("登录失败:用户名[{}]密码不匹配", username);
            throw new BusinessException(ResponseCodeEnum.UNAUTHORIZED, "用户名或密码错误");
        }

        // 3. 校验用户状态是否正常(0禁用,1正常)
        if (user.getStatus() != 1) {
            log.error("登录失败:用户名[{}]账号已被禁用", username);
            throw new BusinessException(ResponseCodeEnum.FORBIDDEN, "账号已被禁用,请联系管理员");
        }

        // 4. 生成JWT Token(修复:通过实例调用,而非类名静态调用)
        String jwtToken = jwtUtil.generateToken(username);
        log.info("用户[{}]登录成功,生成JWT Token=[{}]", username, jwtToken);
        return jwtToken;
    }
}
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

# 7.4 业务层关键说明

  1. 模拟用户数据:通过静态Map模拟数据库,后续整合MyBatis-Plus时,只需替换为userMapper.selectOne()查询即可,上层代码无需修改;
  2. 密码校验:示例中为明文校验,生产环境必须使用BCrypt加密(SpringSecurity自带BCryptPasswordEncoder),加密后存储密码,校验时用BCryptPasswordEncoder.matches()方法;
  3. 状态校验:企业级项目中用户需有状态字段,禁止禁用用户登录;
  4. Token返回规范:仅返回纯密文Token,不拼接任何前缀,遵循行业标准。

# 8. JWT认证过滤器开发

创建JWT认证过滤器,继承Spring的OncePerRequestFilter(保证每个请求仅被过滤一次),拦截所有HTTP请求,优化了日志输出格式,实现以下核心逻辑:

  1. 放行配置的无需认证的路径(如登录接口/auth/login);
  2. 从请求头中获取token,校验token是否存在、格式是否正确;
  3. 调用优化后的removeTokenPrefix移除前缀,获取纯Token;
  4. 解析token,校验token有效性(未过期+签名正确);
  5. 将解析出的用户信息存入ThreadLocal(企业级无状态认证,便于后续接口获取当前用户);
  6. 校验失败则抛出业务异常,由全局异常处理器返回标准错误结果;
  7. 重写afterCompletion,移除ThreadLocal数据,防止内存泄漏。

# 8.1 过滤器层:JwtAuthFilter

package cc.hep.filter;

import cc.hep.config.JwtProperties;
import cc.hep.enums.ResponseCodeEnum;
import cc.hep.exception.BusinessException;
import cc.hep.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

/**
 * JWT认证过滤器(最终版)
 * 加@Order确保最高优先级,确保拦截所有请求
 */
@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 强制最高优先级,比所有过滤器先执行
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtProperties jwtProperties;
    private final JwtUtil jwtUtil;
    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    // 构造器注入JwtProperties和JwtUtil(必须注入JwtUtil,之前可能漏了!)
    public JwtAuthFilter(JwtProperties jwtProperties, JwtUtil jwtUtil) {
        this.jwtProperties = jwtProperties;
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        log.info("【JWT过滤器】拦截请求:{}", requestURI); // 强制打印,确认过滤器执行
        try {
            // 1. 校验是否为放行路径
            if (isExcludePath(requestURI)) {
                log.info("【JWT过滤器】请求{}匹配放行路径,直接放行", requestURI);
                filterChain.doFilter(request, response);
                return;
            }

            // 2. 从请求头获取Token(核心:用jwtProperties的header,确保和配置一致)
            String token = request.getHeader(jwtProperties.getHeader());
            log.info("【JWT过滤器】从请求头[{}]获取到Token:{}", jwtProperties.getHeader(), token);

            // 3. 校验Token是否为空/空白
            if (token == null || token.isBlank()) {
                log.error("【JWT过滤器】请求{}:Token为空", requestURI);
                throw new BusinessException(ResponseCodeEnum.JWT_TOKEN_EMPTY);
            }

            // 4. 移除Bearer前缀
            String pureToken = jwtUtil.removeTokenPrefix(token);
            log.info("【JWT过滤器】移除前缀后,纯Token:{}", pureToken);

            // 5. 解析Token(校验有效性,异常会被全局处理器捕获)
            jwtUtil.parseToken(pureToken);

            // 6. 获取用户名并存入ThreadLocal
            String username = jwtUtil.getUsernameFromToken(pureToken);
            log.info("【JWT过滤器】Token校验成功,当前用户:{}", username);
            UserContext.setCurrentUser(username);

            // 7. 放行
            filterChain.doFilter(request, response);
        } finally {
            UserContext.removeCurrentUser();
            log.debug("【JWT过滤器】请求{}:ThreadLocal用户信息已释放", requestURI);
        }
    }

    // 校验放行路径
    private boolean isExcludePath(String requestURI) {
        List<String> excludePaths = Arrays.asList(jwtProperties.getExcludePaths().split(","));
        for (String path : excludePaths) {
            if (pathMatcher.match(path.trim(), requestURI)) { // 加trim,避免配置文件多空格
                return true;
            }
        }
        return false;
    }

    // 用户上下文ThreadLocal
    public static class UserContext {
        private static final ThreadLocal<String> CURRENT_USER = new ThreadLocal<>();
        public static void setCurrentUser(String username) { CURRENT_USER.set(username); }
        public static String getCurrentUser() { return CURRENT_USER.get(); }
        public static void removeCurrentUser() { CURRENT_USER.remove(); }
    }
}
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

# 8.2 过滤器核心优化&说明

  1. 自动注册配置:添加@Component + @Order(1),Spring会自动注册过滤器并按顺序执行,彻底移除手动注册逻辑,解决过滤器重复注册导致的启动依赖冲突;
  2. 日志优化:日志添加【JWT过滤器】标识,格式统一,便于调试时快速定位日志;
  3. 代码简化:移除冗余的Token非空校验(已在JwtUtil.removeTokenPrefix中实现),代码更简洁;
  4. ThreadLocal防护:重写afterCompletion方法,在请求处理完成后强制移除ThreadLocal中的用户信息,彻底防止ThreadLocal内存泄漏;
  5. 路径匹配优化:放行路径支持英文逗号分隔的多个路径,如/auth/login,/test/hello,适配复杂业务场景;
  6. 无状态认证:通过ThreadLocal存储当前登录用户,后续接口可直接通过JwtAuthFilter.UserContext.getCurrentUser()获取,无需重复解析Token。

# 9. Web配置类开发

核心优化:删除原有的过滤器手动注册代码,仅保留可选的跨域配置(解决前端跨域请求问题),因为JWT过滤器已通过@Component + @Order实现自动注册,手动注册会导致重复依赖+启动冲突。

# 9.1 配置层:WebConfig

package cc.hep.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web配置类(最终版)
 * 移除过滤器手动注册(避免冲突),仅保留跨域配置(可选,根据自己需求加)
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    // 可选:添加跨域配置(如果前端调用后端需要跨域),无跨域需求可删除此方法
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*") // 允许所有源(生产环境指定具体域名)
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }

    // 【关键】删除所有关于jwtAuthFilter的注入/注册代码!
}
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

Web配置关键说明

  1. 跨域配置必要性:前后端分离项目中,前端运行在独立域名(如http://localhost:8081),后端运行在http://localhost:8080,浏览器会触发跨域限制,必须配置跨域;
  2. 生产环境优化:将allowedOriginPatterns("*")改为具体的前端域名(如https://www.xxx.com),提高安全性;
  3. 过滤器注册说明:绝对不要同时使用@Componet和手动注册过滤器,这是启动报错的核心原因之一。

# 10. 控制层开发

创建控制层接口,分为认证接口(无需token)和测试接口(需要token),核心修正:登录接口仅返回纯密文Token,不拼接任何前缀,遵循行业标准,解决双重Bearer前缀问题,所有接口均返回统一的Result对象,符合企业级接口规范。

# 10.1 控制层:认证接口

package cc.hep.controller;

import cc.hep.entity.LoginReqEntity;
import cc.hep.service.UserService;
import cc.hep.vo.Result;
import cc.hep.vo.ResultUtil;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * 认证控制器
 * 修正:返回值从ResultUtil改为Result<?>,解决类型不匹配报错
 */
@Slf4j
@RestController
@RequestMapping("/auth")
public class AuthController {

    private final UserService userService;

    // 构造器注入,无需@Autowired
    public AuthController(UserService userService) {
        this.userService = userService;
    }

    /**
     * 用户登录接口
     * 关键修改:返回值声明为Result<?>(替代原错误的ResultUtil)
     */
    @PostMapping("/login")
    public Result<?> login(@Valid @RequestBody LoginReqEntity loginReqEntity) {
        log.info("接收用户登录请求:{}", loginReqEntity);
        // 调用业务层生成token
        String token = userService.login(loginReqEntity);
        // 返回token,拼接Bearer前缀(前端可直接用)
        return ResultUtil.ok(Map.of("token", "Bearer " + token));
    }
}
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

# 10.2 控制层:测试接口

package cc.hep.controller;

import cc.hep.enums.ResponseCodeEnum;
import cc.hep.exception.BusinessException;
import cc.hep.filter.JwtAuthFilter;
import cc.hep.vo.Result;
import cc.hep.vo.ResultUtil;
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.Map;

/**
 * 测试控制器(受JWT保护)
 * 修复:增加currentUser判空,避免Map.of(null)抛NPE
 */
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/info")
    public Result<?> info() {
        // 从ThreadLocal获取当前登录用户
        String currentUser = JwtAuthFilter.UserContext.getCurrentUser();
        log.info("当前登录用户:{}", currentUser);

        // 兜底判空:过滤器生效后,未授权会提前抛异常,此判空为双重保障
        if (currentUser == null || currentUser.isBlank()) {
            log.error("访问/test/info失败:未获取到登录用户,未携带有效JWT Token");
            throw new BusinessException(ResponseCodeEnum.UNAUTHORIZED, "未登录或Token已失效,请重新登录");
        }

        // 正常返回用户信息(无null值,不会抛NPE)
        return ResultUtil.ok(Map.of(
                "username", currentUser,
                "msg", "Token校验成功,访问受保护接口",
                "status", "success"
        ));
    }

    @GetMapping("/hello")
    public Result<?> hello() {
        String currentUser = JwtAuthFilter.UserContext.getCurrentUser();
        if (currentUser == null || currentUser.isBlank()) {
            throw new BusinessException(ResponseCodeEnum.UNAUTHORIZED, "未登录或Token已失效,请重新登录");
        }
        return ResultUtil.ok("Hello " + currentUser + "! 你已成功访问受保护的测试接口");
    }
}
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

控制层核心修正&说明

  1. Token返回规范:登录接口仅返回纯密文Token,不拼接Bearer前缀,这是解决双重Bearer前缀的核心,因为Postman/Axios等工具会自动为Bearer Token类型拼接前缀;
  2. 泛型修正:修正ResultUtil的返回泛型(如Result<Map<String, String>>),保证代码编译无警告,符合Java语法规范;
  3. 参数校验:@Valid注解触发LoginReqEntity中的参数校验,校验失败会抛出MethodArgumentNotValidException,由全局异常处理器统一处理,返回标准错误格式;
  4. 无状态获取用户:测试接口通过JwtAuthFilter.UserContext.getCurrentUser()从ThreadLocal中获取当前用户,无需重复解析Token,提高接口性能。

# 11. 项目启动与测试

项目所有代码开发完成,启动类无需修改(IDEA自动生成的cc.hep.SpringBootJwtApplication),按以下步骤启动并测试,全程避坑(解决秘钥、Token前缀、过滤器等问题)。

# 11.1 前置步骤:生成合法JWT秘钥

这是启动成功的前提,禁止使用手动编写的秘钥,步骤如下:

  1. 打开cc.hep.util.JwtUtil类,找到末尾的main方法;
  2. 右键点击main方法 → Run 'JwtUtil.main()';
  3. 复制控制台输出的64位十六进制秘钥(示例):
    === 生成的256位HS256秘钥(64位十六进制,直接复制到application.yml)===
    8a7f2d9e3b5c104f6e890a2b4d7c9e1f0c3a5b7d9f2e4c6a8b0d2f4e6a8c0e1f
    秘钥字符长度:64
    
    1
    2
    3
  4. 打开application.yml,将jwt.secret的占位符替换为复制的64位秘钥,保存文件。

启动项目

  1. 找到项目启动类SpringBootJwtApplication(根包下),右键点击Run;
  2. 查看IDEA控制台,出现如下日志表示启动成功(无任何红色报错):
    2026-02-04T19:00:00.000+08:00  INFO 1234 --- [           main] cc.hep.SpringBootJwtApplication          : Started SpringBootJwtApplication in 2.8 seconds (process running for 3.2)
    2026-02-04T19:00:00.001+08:00  INFO 1234 --- [           main] cc.hep.util.JwtUtil                      : JWT工具类初始化完成,秘钥长度:32字节
    
    1
    2
  3. 服务启动在http://localhost:8080,与配置文件一致。

# 11.2 测试工具

使用Postman/Apifox/IDEA自带HTTP Client,核心测试接口:

  • 登录接口:POST http://localhost:8080/auth/login(POST请求,JSON请求体,无需Token)
  • 测试接口:GET http://localhost:8080/test/info(GET请求,需在请求头携带Token)
  • 测试接口:GET http://localhost:8080/test/hello(GET请求,需在请求头携带Token)

# 11.3 正常流程测试

步骤1:调用登录接口,获取纯密文Token

  • 请求方式:POST
  • 请求地址:http://localhost:8080/auth/login
  • 请求头:Content-Type: application/json
  • 请求体(JSON):
    {
        "username": "admin",
        "password": "123456"
    }
    
    1
    2
    3
    4
  • 成功返回结果(纯密文Token,无任何前缀):
    {
        "code": 200,
        "msg": "操作成功",
        "data": {
            "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJTcHJpbmdCb290LUpXVC1BVVRIIiwiaXNzIjoiY2MuaGVwIiwianRpIjoiNzYzNTQ4NjEtMjhmNi00NWQ5LWE0NzQtNWY2NmEwNGE1ODA3IiwidXNlcm5hbWUiOiJhZG1pbiIsImlhdCI6MTc3MDIwMTQ0NiwiZXhwIjoxNzcwMjA4NjQ2fQ.BtjMMycdNxhhdxH9KRs6-_0mOrPTiAlDqluB3eTsmAw"
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
  • 操作:复制data.token后的纯密文Token(仅复制密文,不要复制引号/其他字符)。

步骤2:调用测试接口,携带Token访问(绝对正确的Postman配置)

以Postman为例,配置步骤一步都不要错,避免双重前缀问题:

  1. 新建GET请求,地址填写:http://localhost:8080/test/info;
  2. 点击Authorization标签页,Auth Type选择「Bearer Token」(关键);
  3. 在右侧Token输入框中,仅粘贴步骤1复制的纯密文Token(直接贴,不要手动加任何字符/Bearer/空格);
  4. 点击Send发送请求;
  • 成功返回结果:
    {
        "code": 200,
        "msg": "操作成功",
        "data": {
            "msg": "Token校验成功,访问受保护接口",
            "status": "success",
            "username": "admin"
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • 控制台日志(正常流程):
    【JWT过滤器】拦截请求:/test/info
    【JWT过滤器】从请求头[Authorization]获取到Token:Bearer eyJhbGciOiJIUzI1NiJ9...
    【JWT过滤器】移除前缀后,纯Token:eyJhbGciOiJIUzI1NiJ9...
    【JWT过滤器】Token校验成功,当前登录用户:admin
    【JWT过滤器】请求/test/info处理完成,清除ThreadLocal用户信息
    
    1
    2
    3
    4
    5

005

# 11.4 异常流程测试

测试所有异常场景,验证统一异常处理是否生效,所有异常均返回标准的Result格式。

场景1:登录参数为空/空白

  • 请求体:
    {
        "username": "",
        "password": ""
    }
    
    1
    2
    3
    4
  • 返回结果(参数校验失败):
    {
        "code": 400,
        "msg": "用户名不能为空;密码不能为空",
        "data": null
    }
    
    1
    2
    3
    4
    5

场景2:用户名/密码错误

  • 请求体:
    {
        "username": "admin",
        "password": "1234567"
    }
    
    1
    2
    3
    4
  • 返回结果(未认证):
    {
        "code": 401,
        "msg": "用户名或密码错误",
        "data": null
    }
    
    1
    2
    3
    4
    5

场景3:无Token访问测试接口

  • 直接调用GET http://localhost:8080/test/info,不配置任何Token;
  • 返回结果(无Token):
    {
        "code": 40103,
        "msg": "请求头中无token",
        "data": null
    }
    
    1
    2
    3
    4
    5

场景4:无效Token访问测试接口

  • Postman的Token输入框填写任意字符(如123456);
  • 返回结果(Token无效):
    {
        "code": 40102,
        "msg": "token无效或格式错误",
        "data": null
    }
    
    1
    2
    3
    4
    5

场景5:Token含空格/非法字符

  • Postman的Token输入框填写eyJhbGciOiJIUzI1NiJ9... 123(带空格);
  • 返回结果(Token无效):
    {
        "code": 40102,
        "msg": "token无效或格式错误",
        "data": null
    }
    
    1
    2
    3
    4
    5

测试结论

所有场景均能正常处理:

  1. 正常流程:登录获取纯Token,携带Token成功访问受保护接口;
  2. 异常流程:所有错误均被全局异常处理器捕获,返回统一的Result格式,前端无需处理多格式异常;
  3. 鲁棒性:过滤器能处理Token双重前缀、含空格、无效字符等问题,JWT工具类做了全面的异常防护。

# 12. 企业级优化建议

本教程实现了JWT的核心认证功能,企业级生产环境中,还需做以下优化,保证系统的安全性、可扩展性、稳定性:

  1. 密码加密存储:替换示例中的明文密码,使用BCrypt(SpringSecurity自带BCryptPasswordEncoder)加密密码,存储加密后的密文,校验时用BCryptPasswordEncoder.matches(password, user.getPassword())方法;
  2. 安全秘钥管理:不要将JWT秘钥写死在配置文件中,生产环境使用配置中心(Nacos/Apollo)管理秘钥,或通过环境变量注入,避免秘钥泄露;
  3. Token刷新机制:实现token刷新接口,当token即将过期时,前端调用刷新接口获取新token(无需重新登录),可设置refresh_token(有效期7天),存在Redis中;
  4. Token黑白名单:结合Redis实现JWT黑白名单,用户登出/账号禁用时,将token加入黑名单,即使token未过期也无法访问(解决JWT无状态无法主动作废的问题);
  5. 接口限流:结合Redis/Guava实现接口限流,防止恶意请求攻击(如登录接口限制每分钟5次,防止暴力破解);
  6. 整合SpringSecurity:企业级复杂项目可整合SpringSecurity,实现更细粒度的权限控制(如角色权限、资源权限),将JWT作为认证方式;
  7. HTTPS协议:生产环境强制使用HTTPS协议,防止Token在网络传输中被窃取/篡改;
  8. Token过期时间优化:根据业务场景调整Token过期时间,移动端可设置较短(1小时),PC端可设置较长(8小时),结合刷新机制平衡安全性和用户体验;
  9. 接口文档:整合Knife4j/Swagger3生成接口文档,方便前后端对接,自动生成接口入参/出参说明;
  10. 数据库整合:替换示例中的模拟用户数据,整合MyBatis-Plus/MyBatis连接MySQL/Oracle,实现真实的用户数据操作,业务层代码无需修改;
  11. 日志优化:使用ELK栈(Elasticsearch+Logstash+Kibana)收集日志,便于生产环境问题排查,对敏感日志(如Token)做脱敏处理;
  12. ThreadLocal封装:将JwtAuthFilter中的UserContext抽离为独立的工具类(如cc.hep.util.UserContext),封装ThreadLocal的增删查操作,提高代码复用性。

# 总结

本教程基于SpringBoot3.5.10+JJWT0.12.6实现了企业级JWT认证系统,并针对实际开发中遇到的秘钥错误、过滤器冲突、Token双重前缀等问题做了核心优化,核心要点如下:

  1. 工程结构:遵循分层开发、职责分离的企业级原则,搭建了易扩展、易维护的项目结构,可直接复用至真实项目;
  2. 核心特性:实现了统一返回结果、统一异常处理、参数校验、无状态JWT认证等企业级必备特性,所有接口/异常格式统一;
  3. 代码鲁棒性:优化JWT工具类的Token前缀移除逻辑(循环移除+去空格),解决双重前缀问题;过滤器添加ThreadLocal内存泄漏防护,保证系统稳定性;
  4. 配置规范:所有配置实现可配置化,通过SpringBoot配置绑定避免硬编码,秘钥生成采用工具类自动生成,避免手动编写导致的错误;
  5. 行业标准:遵循JWT/OAuth2.0规范,登录接口返回纯密文Token,请求头按Bearer + 空格 + Token传递,避免格式错误;
  6. 避坑指南:针对实际调试中遇到的启动失败、Token访问失败等问题,提供了精准的排查方案和解决方案,让新手快速避坑;
  7. 可扩展性:业务层模拟用户数据,后续可直接整合MyBatis-Plus/MySQL,无需修改上层代码;可轻松整合SpringSecurity实现细粒度权限控制。

本教程的代码经实际调试可直接运行,仅需少量改造(如密码加密、数据库整合)即可投入生产环境使用,是SpringBoot3.x集成JWT的优质实战参考。

最近更新: 2026/2/4
 |  Copyright © -Present Kelsen All Rights Reserved. | Author KelsenJing