一、工程基础准备

1. 工程结构(标准 Maven 结构)

plaintext

src/main/java/com/example/token/
├── TokenApplication.java          // 启动类
├── config/
│   └── WebMvcConfig.java           // 拦截器配置类
├── interceptor/
│   └── AuthInterceptor.java        // Token认证拦截器
├── util/
│   ├── JwtUtil.java                // JWT Token工具类
│   └── UserContextHolder.java      // ThreadLocal存储用户ID
├── controller/
│   └── UserController.java         // 测试接口Controller
├── exception/
│   ├── GlobalExceptionHandler.java // 全局异常处理
│   └── TokenException.java         // 自定义Token异常
└── vo/
    └── ResultVo.java               // 统一响应体
2. Maven 依赖(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> <!-- 稳定版本,兼容JJWT -->
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>token-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>token-demo</name>
    <description>Spring Boot + JWT Token 实战示例</description>

    <dependencies>
        <!-- Spring Boot Web核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- JJWT依赖(JWT Token生成/验证) -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>

        <!-- Lombok(简化代码,可选) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </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>

二、核心代码实现

1. 启动类(TokenApplication.java)
package com.example.token;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TokenApplication {
    public static void main(String[] args) {
        SpringApplication.run(TokenApplication.class, args);
    }
}
2. 统一响应体(ResultVo.java)
package com.example.token.vo;

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

/**
 * 接口统一响应体
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResultVo<T> {
    // 响应码:200成功,401未授权,500服务器错误
    private int code;
    // 响应消息
    private String msg;
    // 响应数据
    private T data;

    // 快捷构造方法
    public static <T> ResultVo<T> success(T data) {
        return new ResultVo<>(200, "操作成功", data);
    }

    public static <T> ResultVo<T> fail(int code, String msg) {
        return new ResultVo<>(code, msg, null);
    }
}
3. 自定义 Token 异常(TokenException.java)
package com.example.token.exception;

/**
 * 自定义Token相关异常
 */
public class TokenException extends RuntimeException {
    private int code; // 异常对应的响应码

    public TokenException(int code, String message) {
        super(message);
        this.code = code;
    }

    public int getCode() {
        return code;
    }
}
4. 全局异常处理(GlobalExceptionHandler.java)
package com.example.token.exception;

import com.example.token.vo.ResultVo;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器,统一处理Token相关异常
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 处理自定义Token异常
    @ExceptionHandler(TokenException.class)
    public ResultVo<?> handleTokenException(TokenException e) {
        return ResultVo.fail(e.getCode(), e.getMessage());
    }

    // 处理JWT过期异常
    @ExceptionHandler(ExpiredJwtException.class)
    public ResultVo<?> handleExpiredJwtException(ExpiredJwtException e) {
        return ResultVo.fail(401, "Token已过期,请重新登录");
    }

    // 处理JWT签名异常(Token被篡改)
    @ExceptionHandler(SignatureException.class)
    public ResultVo<?> handleSignatureException(SignatureException e) {
        return ResultVo.fail(401, "Token签名非法,可能已被篡改");
    }

    // 处理其他所有异常
    @ExceptionHandler(Exception.class)
    public ResultVo<?> handleException(Exception e) {
        return ResultVo.fail(500, "服务器内部错误:" + e.getMessage());
    }
}
5. ThreadLocal 工具类(UserContextHolder.java)
package com.example.token.util;

/**
 * 存储当前请求的用户ID(ThreadLocal保证线程安全)
 */
public class UserContextHolder {
    // ThreadLocal:每个线程独立存储,避免多线程数据混乱
    private static final ThreadLocal<Long> USER_ID_HOLDER = new ThreadLocal<>();

    // 设置当前线程的用户ID
    public static void setUserId(Long userId) {
        USER_ID_HOLDER.set(userId);
    }

    // 获取当前线程的用户ID
    public static Long getUserId() {
        return USER_ID_HOLDER.get();
    }

    // 移除当前线程的用户ID(防止内存泄漏)
    public static void removeUserId() {
        USER_ID_HOLDER.remove();
    }
}
6. JWT 工具类(JwtUtil.java)
package com.example.token.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Date;

/**
 * JWT Token生成/验证工具类
 */
public class JwtUtil {
    // 核心密钥(生产环境建议存配置文件/环境变量,长度至少32位)
    private static final String SECRET_KEY = "your_very_secure_secret_key_1234567890abcdef";
    // Token过期时间:2小时(单位:毫秒)
    private static final long EXPIRE_TIME = 2 * 3600 * 1000L;

    /**
     * 生成JWT Token
     * @param userId 用户ID
     * @param username 用户名
     * @return JWT Token字符串
     */
    public static String generateToken(Long userId, String username) {
        // 生成加密密钥
        SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
        // 构建Token
        return Jwts.builder()
                // 自定义载荷:用户ID、用户名(非敏感信息)
                .claim("userId", userId)
                .claim("username", username)
                // 标准字段:主题(用户唯一标识)
                .setSubject(String.valueOf(userId))
                // 标准字段:签发时间
                .setIssuedAt(new Date())
                // 标准字段:过期时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME))
                // 签名:保证Token不可篡改
                .signWith(key)
                // 压缩为字符串
                .compact();
    }

    /**
     * 验证并解析Token,返回用户ID
     * @param token JWT Token
     * @return 用户ID
     */
    public static Long validateToken(String token) {
        SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
        // 解析Token(自动验证签名、过期时间)
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
        // 提取用户ID并返回
        return Long.parseLong(claims.get("userId").toString());
    }
}
7. Token 认证拦截器(AuthInterceptor.java)
package com.example.token.interceptor;

import com.example.token.exception.TokenException;
import com.example.token.util.JwtUtil;
import com.example.token.util.UserContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Token认证拦截器:统一验证接口的Token合法性
 */
public class AuthInterceptor implements HandlerInterceptor {

    /**
     * 请求处理前执行:验证Token
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 提取请求头中的Token(标准格式:Bearer <token>)
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            throw new TokenException(401, "未携带有效Token,请先登录");
        }
        // 截取"Bearer "后的Token字符串
        String token = authHeader.substring(7).trim();

        // 2. 验证Token并解析用户ID
        Long userId;
        try {
            userId = JwtUtil.validateToken(token);
        } catch (Exception e) {
            // 捕获JWT所有异常,包装为自定义异常
            throw new TokenException(401, e.getMessage());
        }

        // 3. 将用户ID存入ThreadLocal,供后续接口使用
        UserContextHolder.setUserId(userId);

        // 4. 验证通过,放行请求
        return true;
    }

    /**
     * 请求处理后执行:清理ThreadLocal,防止内存泄漏
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserContextHolder.removeUserId();
    }
}
8. 拦截器配置类(WebMvcConfig.java)
package com.example.token.config;

import com.example.token.interceptor.AuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * WebMvc配置:注册拦截器,指定拦截/放行规则
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * 注册Token拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                // 拦截所有接口
                .addPathPatterns("/**")
                // 放行无需认证的接口(如登录接口)
                .excludePathPatterns("/user/login");
    }
}
9. 测试 Controller(UserController.java)
package com.example.token.controller;

import com.example.token.util.JwtUtil;
import com.example.token.util.UserContextHolder;
import com.example.token.vo.ResultVo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 用户接口示例:包含登录(生成Token)、需要认证的接口
 */
@RestController
public class UserController {

    /**
     * 登录接口(无需认证,生成Token)
     * 模拟登录:传入用户名密码,验证通过后返回Token
     */
    @PostMapping("/user/login")
    public ResultVo<?> login(
            @RequestParam String username,
            @RequestParam String password
    ) {
        // 模拟验证用户名密码(实际开发查数据库)
        if (!"zhangsan".equals(username) || !"123456".equals(password)) {
            return ResultVo.fail(400, "用户名或密码错误");
        }

        // 模拟用户ID(实际从数据库查)
        Long userId = 1001L;
        // 生成JWT Token
        String token = JwtUtil.generateToken(userId, username);

        // 返回Token给客户端
        return ResultVo.success(token);
    }

    /**
     * 需要认证的接口:获取当前登录用户信息
     */
    @GetMapping("/user/info")
    public ResultVo<?> getUserInfo() {
        // 从ThreadLocal获取当前登录用户ID
        Long userId = UserContextHolder.getUserId();
        return ResultVo.success("当前登录用户ID:" + userId + ",用户名:zhangsan");
    }
}

三、运行与测试步骤

1. 启动项目

直接运行TokenApplication.java的main方法,Spring Boot 项目会启动在默认端口8080。

2. 测试接口(用 Postman/ApiPost/ 浏览器)
(1)测试登录接口(无需 Token,生成 Token)
  • 请求方式:POST
  • 请求地址:http://localhost:8080/user/login
  • 请求参数:username=zhangsan&password=123456
  • 响应示例(成功):
{
  "code": 200,
  "msg": "操作成功",
  "data": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEwMDEsInVzZXJuYW1lIjoiemhhbmdzYW4iLCJzdWIiOiIxMDAxIiwiaWF0IjoxNzM2OTYwNzY4LCJleHAiOjE3MzY5Njc5Njh9.xxxx"
}
(2)测试需要认证的接口(需携带 Token)
  • 请求方式:GET
  • 请求地址:http://localhost:8080/user/info
  • 请求头:Authorization: Bearer 上面生成的Token字符串
  • 响应示例(成功):
{
  "code": 200,
  "msg": "操作成功",
  "data": "当前登录用户ID:1001,用户名:zhangsan"
}
(3)测试异常场景
  • 不携带 Token 访问/user/info:返回{"code":401,"msg":"未携带有效Token,请先登录","data":null};
  • 携带过期 / 篡改的 Token:返回对应的 401 异常提示。

总结

  1. 核心流程:登录接口生成 Token → 客户端携带 Token 请求需认证接口 → 拦截器验证 Token → 验证通过后将用户 ID 存入 ThreadLocal 供业务使用;
  2. 关键要点:拦截器通过addPathPatterns/excludePathPatterns控制拦截范围,ThreadLocal 需在请求结束后清理,全局异常处理器统一返回标准响应;
  3. 生产环境优化:密钥存入配置文件 / 环境变量、Token 过期时间可配置、Redis 存储黑名单 Token(注销 / 踢下线场景)、密码加密存储(如 BCrypt)。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐