JWT 认证全面解析:原理、流程与 Spring Boot 实战

JWT 认证全面解析:原理、流程与 Spring Boot 实战

一、什么是 JWT

JWT(JSON Web Token)是一种开放标准(RFC 7519),定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象安全地传递信息。这些信息可以被验证和信任,因为它是数字签名的。

核心特征

  • 无状态:服务端不需要存储 Session,Token 本身包含所有信息
  • 自包含:Token 中携带用户身份和权限数据
  • 可验证:通过签名确保 Token 未被篡改

二、JWT 的结构

一个 JWT 由三部分组成,用 . 分隔:

复制代码
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIyMDExMDEwMSIsIm5hbWUiOiLlvKDkuIkifQ.signature
|______ Header ______||_________________ Payload __________________||_ Signature _|

2.1 Header(头部)

声明 Token 类型和签名算法:

json 复制代码
{
  "alg": "RS256",    // 签名算法
  "typ": "JWT"       // Token 类型
}

经过 Base64Url 编码后成为第一段。

2.2 Payload(载荷)

携带的实际数据(Claims):

json 复制代码
{
  "sub": "20110101",           // Subject:用户标识
  "name": "张三",              // 自定义字段
  "role": "admin",             // 自定义字段
  "iat": 1718150400,           // Issued At:签发时间
  "exp": 1718236800,           // Expiration:过期时间
  "aud": "my-service"         // Audience:接收方
}

标准 Claims(预定义)

字段 全称 说明
iss Issuer 签发人
sub Subject 主题(通常是用户ID)
aud Audience 受众(Token 面向的服务)
exp Expiration 过期时间(Unix 时间戳)
nbf Not Before 生效时间
iat Issued At 签发时间
jti JWT ID 唯一标识(用于防重放)

经过 Base64Url 编码后成为第二段。

注意 :Payload 只是 Base64 编码,不是加密。任何人都能解码看到内容,所以不要放敏感信息(如密码)。

2.3 Signature(签名)

用于验证 Token 未被篡改:

复制代码
Signature = Algorithm(
    Base64Url(Header) + "." + Base64Url(Payload),
    密钥
)
  • 对称算法(HS256):用同一个 Secret 签名和验证
  • 非对称算法(RS256):用私钥签名,公钥验证

三、签名算法对比

3.1 对称加密(HMAC)

复制代码
签发方:HMAC-SHA256(header.payload, secret) → signature
验证方:HMAC-SHA256(header.payload, secret) → 比较 signature
优点 缺点
简单,性能高 签发方和验证方必须共享同一个 secret
适合单服务 secret 泄露则任何人可以伪造 Token

3.2 非对称加密(RSA)

复制代码
签发方(认证中心):RSA-SHA256(header.payload, 私钥) → signature
验证方(业务服务):RSA-SHA256-Verify(header.payload, signature, 公钥) → true/false
优点 缺点
只有认证中心持有私钥,业务服务只需公钥 性能略低于 HMAC
公钥可以公开分发,不怕泄露 密钥管理复杂一些
适合微服务架构 -

示例:项目使用 RS256(RSA + SHA256),application.yml 中配置了公钥用于验证:

yaml 复制代码
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          public-key-location: |
            -----BEGIN PUBLIC KEY-----
            xxxxxxxx...
            -----END PUBLIC KEY-----

四、认证流程

4.1 完整时序

复制代码
用户/前端                  认证中心(Auth Server)           业务服务(Resource Server)
  │                              │                              │
  │── 1.登录(用户名+密码) ──→     │                              │
  │                              │── 2.验证账号密码               │
  │                              │── 3.生成JWT(用私钥签名)        │
  │← 4.返回 JWT Token ──         │                              │
  │                              │                              │
  │── 5.请求业务接口 ──→           │                              │
  │   Header: Authorization:     │                              │
  │   Bearer eyJhbGciOi...       │                              │
  │                              │                              │── 6.取出Token
  │                              │                              │── 7.用公钥验签
  │                              │                              │── 8.检查是否过期
  │                              │                              │── 9.解析用户信息
  │                              │                              │── 10.执行业务逻辑
  │← 11.返回业务数据 ──            │                              │

4.2 各步骤详解

步骤 说明
1-4 认证阶段:只发生一次(或 Token 过期后重新认证)
5 前端每次请求在 Header 中携带 Token
6 Security Filter 从请求头提取 Token
7 用公钥验证签名(确保 Token 未被篡改)
8 检查 exp 字段(确保 Token 未过期)
9 从 Payload 中解析用户ID、角色等信息
10-11 正常执行业务逻辑并返回

4.3 Token 存储位置(前端)

存储位置 优点 缺点
localStorage 持久化,刷新页面不丢失 容易被 XSS 攻击读取
sessionStorage 关闭标签页后清除 同样有 XSS 风险
Cookie (httpOnly) XSS 无法读取 需要处理 CSRF
内存变量 最安全 刷新页面丢失

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

五、Spring Security 中的 JWT 处理流程

5.1 Filter 链

复制代码
HTTP请求
  ↓
SecurityFilterChain
  ├── CharacterEncodingFilter
  ├── CorsFilter
  ├── BearerTokenAuthenticationFilter  ← JWT 在这里被处理
  │     ├── 从 Authorization Header 提取 Token
  │     ├── 调用 JwtDecoder 解码验证
  │     ├── 验证通过 → 创建 Authentication 对象放入 SecurityContext
  │     └── 验证失败 → 返回 401
  ├── AuthorizationFilter
  │     ├── 检查 SecurityContext 中是否有认证信息
  │     ├── 检查是否匹配 permitAll 规则
  │     └── 不匹配且未认证 → 返回 403
  └── DispatcherServlet → Controller

5.2 permitAll 的位置

yaml 复制代码
security:
  matchers:
    - path: /api/xxxx/**
      attribute: permitAll

这条规则在 AuthorizationFilter 中生效:

  • 请求路径匹配 /api/xxxx/** → 跳过认证检查,直接放行
  • 请求路径不在 permitAll 列表中 → 必须有有效的 JWT Token

5.3 JwtDecoder 的工作

java 复制代码
// Spring Security 内部逻辑(简化)
public Jwt decode(String token) {
    // 1. 按 "." 分割为三段
    String[] parts = token.split("\\.");
    
    // 2. Base64 解码 Header 和 Payload
    Header header = decodeHeader(parts[0]);
    Claims claims = decodeClaims(parts[1]);
    
    // 3. 用公钥验证签名
    boolean valid = rsaVerify(parts[0] + "." + parts[1], parts[2], publicKey);
    if (!valid) throw new InvalidTokenException("签名验证失败");
    
    // 4. 检查过期时间
    if (claims.getExp().before(new Date())) {
        throw new ExpiredTokenException("Token已过期");
    }
    
    // 5. 检查 audience
    if (!claims.getAud().contains(expectedAudience)) {
        throw new InvalidTokenException("Token受众不匹配");
    }
    
    return new Jwt(header, claims);
}

六、Token 刷新机制

6.1 为什么需要刷新

  • Token 有过期时间(如2小时),过期后用户需要重新登录
  • 频繁让用户重新登录体验差
  • 用 Refresh Token 机制实现"无感续期"

6.2 双 Token 机制

Token 类型 有效期 用途 存储
Access Token 短(如2小时) 访问业务接口 内存/localStorage
Refresh Token 长(如7天) 换取新的 Access Token httpOnly Cookie

6.3 刷新流程

复制代码
前端                           认证中心
 │                               │
 │── 请求业务接口(Access Token) ──→ │
 │← 401 Token过期 ──              │
 │                               │
 │── 刷新请求(Refresh Token) ──→  │
 │                               │── 验证Refresh Token
 │                               │── 生成新Access Token
 │← 返回新Access Token ──         │
 │                               │
 │── 重试业务请求(新Token) ──→     │
 │← 正常返回 ──                   │

七、JWT vs Session 对比

维度 JWT Session
存储位置 客户端 服务端(内存/Redis)
服务端状态 无状态 有状态
水平扩展 天然支持(任何节点都能验证) 需要 Session 共享(如 Redis)
性能 每次请求解析 Token(CPU) 每次请求查 Session(IO)
注销 困难(Token 签发后无法撤回) 简单(删除 Session 即可)
信息传递 Token 自带用户信息 需要通过 SessionId 查服务端
安全性 泄露后无法立即失效 可以立即失效
跨域 天然支持 需要额外处理
适用场景 微服务、移动端、第三方API 传统单体Web应用

八、安全注意事项

风险 防护措施
Token 泄露 短过期时间 + HTTPS + httpOnly Cookie
Token 篡改 签名验证(RS256/HS256)
重放攻击 jti 唯一标识 + 短过期时间
XSS 窃取 httpOnly Cookie 存储 Token
暴力破解 RS256 用 2048+ 位密钥
无法注销 维护 Token 黑名单(Redis)
Payload 信息泄露 不放敏感数据,只放 ID 和角色

九、完整代码示例

9.1 依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>

9.2 配置

yaml 复制代码
app:
  jwt:
    secret: xxxxxxxxxxxx!!
    expiration: 7200        # Access Token 有效期(秒)
    refresh-expiration: 604800  # Refresh Token 有效期(秒)

9.3 JWT 工具类

java 复制代码
package com.example.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * JWT 工具类:生成、解析、验证 Token.
 */
@Component
public class JwtUtil {

  @Value("${app.jwt.secret}")
  private String secret;

  @Value("${app.jwt.expiration}")
  private long expiration;

  private SecretKey getSigningKey() {
    return Keys.hmacShaKeyFor(secret.getBytes());
  }

  /**
   * 生成 Token.
   */
  public String generateToken(String userId, String username, String role) {
    Map<String, Object> claims = new HashMap<>();
    claims.put("userId", userId);
    claims.put("username", username);
    claims.put("role", role);

    return Jwts.builder()
        .claims(claims)
        .subject(userId)
        .issuedAt(new Date())
        .expiration(new Date(System.currentTimeMillis() + expiration * 1000))
        .signWith(getSigningKey())
        .compact();
  }

  /**
   * 解析 Token,返回 Claims.
   */
  public Claims parseToken(String token) {
    return Jwts.parser()
        .verifyWith(getSigningKey())
        .build()
        .parseSignedClaims(token)
        .getPayload();
  }

  /**
   * 验证 Token 是否有效.
   */
  public boolean validateToken(String token) {
    try {
      parseToken(token);
      return true;
    } catch (ExpiredJwtException e) {
      return false; // 过期
    } catch (Exception e) {
      return false; // 无效
    }
  }

  /**
   * 从 Token 中获取用户ID.
   */
  public String getUserId(String token) {
    return parseToken(token).getSubject();
  }

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

  /**
   * 判断 Token 是否过期.
   */
  public boolean isTokenExpired(String token) {
    try {
      Date exp = parseToken(token).getExpiration();
      return exp.before(new Date());
    } catch (ExpiredJwtException e) {
      return true;
    }
  }
}

9.4 JWT 认证过滤器

java 复制代码
package com.example.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

/**
 * JWT 认证过滤器.
 * 每次请求检查 Authorization Header 中的 Token.
 */
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

  private static final String AUTH_HEADER = "Authorization";
  private static final String TOKEN_PREFIX = "Bearer ";

  @Autowired
  private JwtUtil jwtUtil;

  @Override
  protected void doFilterInternal(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain)
      throws ServletException, IOException {

    // 1. 从请求头提取 Token
    String header = request.getHeader(AUTH_HEADER);
    if (header == null || !header.startsWith(TOKEN_PREFIX)) {
      chain.doFilter(request, response);
      return;
    }

    String token = header.substring(TOKEN_PREFIX.length());

    // 2. 验证 Token
    if (!jwtUtil.validateToken(token)) {
      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
      response.getWriter().write("{\"error\":\"Token无效或已过期\"}");
      return;
    }

    // 3. 解析用户信息,放入 SecurityContext
    String userId = jwtUtil.getUserId(token);
    String username = jwtUtil.getUsername(token);

    UsernamePasswordAuthenticationToken authentication =
        new UsernamePasswordAuthenticationToken(userId, null, null);
    // 可以把更多信息放到 details 中
    authentication.setDetails(username);
    SecurityContextHolder.getContext().setAuthentication(authentication);

    // 4. 继续执行后续 Filter 和 Controller
    chain.doFilter(request, response);
  }
}

9.5 Security 配置类

java 复制代码
package com.example.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * Spring Security 配置.
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

  private final JwtAuthenticationFilter jwtFilter;

  public SecurityConfig(JwtAuthenticationFilter jwtFilter) {
    this.jwtFilter = jwtFilter;
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // 禁用 CSRF(JWT 无状态不需要)
        .csrf(csrf -> csrf.disable())
        // 无状态 Session(不创建 HttpSession)
        .sessionManagement(session ->
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        // URL 权限规则
        .authorizeHttpRequests(auth -> auth
            // 放行的接口
            .requestMatchers("/api/auth/login").permitAll()
            .requestMatchers("/api/auth/refresh").permitAll()
            .requestMatchers("/actuator/**").permitAll()
            .requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
            // 其他所有接口需要认证
            .anyRequest().authenticated()
        )
        // 添加 JWT 过滤器(在 UsernamePasswordAuthenticationFilter 之前)
        .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
  }
}

9.6 登录接口(签发 Token)

java 复制代码
package com.example.controller;

import com.example.security.JwtUtil;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
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;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

  @Autowired
  private JwtUtil jwtUtil;

  /**
   * 登录接口:验证账号密码,签发 Token.
   */
  @PostMapping("/login")
  public Map<String, Object> login(@RequestBody LoginRequest request) {
    // 实际项目中这里查数据库验证密码
    if (!"admin".equals(request.getUsername()) || !"123456".equals(request.getPassword())) {
      return Map.of("success", false, "errorMsg", "用户名或密码错误");
    }

    // 验证通过,生成 Token
    String token = jwtUtil.generateToken("10001", "admin", "ROLE_ADMIN");

    return Map.of(
        "success", true,
        "data", Map.of(
            "token", token,
            "tokenType", "Bearer",
            "expiresIn", 7200
        )
    );
  }

  public static class LoginRequest {
    private String username;
    private String password;
    // getter/setter...
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
  }
}

9.7 业务接口中获取当前用户

java 复制代码
package com.example.controller;

import java.util.Map;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/user")
public class UserController {

  /**
   * 获取当前登录用户信息.
   * 必须携带有效 Token 才能访问.
   */
  @GetMapping("/me")
  public Map<String, Object> getCurrentUser() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();

    String userId = (String) auth.getPrincipal();   // 从 Token 中解析的 userId
    String username = (String) auth.getDetails();   // 从 Token 中解析的 username

    return Map.of(
        "success", true,
        "data", Map.of(
            "userId", userId,
            "username", username
        )
    );
  }
}

9.8 测试流程

bash 复制代码
# 1. 登录获取 Token
curl -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"123456"}'

# 返回:
# {"success":true,"data":{"token":"eyJhbGciOiJIUzI1NiJ9.xxx.yyy","tokenType":"Bearer","expiresIn":7200}}

# 2. 携带 Token 访问受保护接口
curl http://localhost:8080/api/user/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxx.yyy"

# 返回:
# {"success":true,"data":{"userId":"10001","username":"admin"}}

# 3. 不带 Token 访问(被拦截)
curl http://localhost:8080/api/user/me

# 返回:401 Unauthorized

十、总结

复制代码
JWT 认证的本质:
  用密码学签名代替服务端存储,实现无状态的身份验证。

核心流程:
  登录 → 认证中心用私钥签发Token → 前端存储Token
  → 每次请求携带Token → 业务服务用公钥验签 → 通过则放行

关键配置:
  permitAll → 跳过认证,直接放行(公开接口)
  authenticated → 必须携带有效Token(受保护接口)
相关推荐
王小王-1231 小时前
基于Django的个性化餐饮场所推荐系统
后端·python·django·个性化餐厅推荐·个性化餐饮推荐
TeamDev1 小时前
JxBrowser 9.1.2 版本发布啦!
java·跨平台·混合应用·jxbrowser·浏览器控件·compose 多平台
逢君学术论文AI写作1 小时前
Java第21课:JavaWeb入门——Tomcat+第一个Servlet
java·servlet·tomcat
就叫_这个吧1 小时前
Java使用tomcat+servlet+filter实现简单的登录功能,需先登录再进行页面数据管理操作
java·开发语言·servlet·tomcat·jsp·filter
十五年专注C++开发2 小时前
ANTLR4: CORBA IDL、C++ 语法文件分析利器
java·开发语言·c++·antlr4
子非衣2 小时前
Java使用Aspose进行Word转PDF时异常卡主问题
java·pdf·word
此生决int2 小时前
Java面向对象进阶精讲:抽象类、接口、内部类与Object类万字详解
java
阿维的博客日记2 小时前
‘version‘ must be a constant version but is ‘${revision}‘
java·spring boot·后端