Java框架快速入门: Spring Security+OAuth2之JWT核心概念与实战

纲要

  • JWT 核心概念
    • 什么是 JWT(JSON Web Token)
    • 自包含、紧凑、可信赖的特性
  • JWT 工作流程
    • 登录签发流程
    • 后续请求验证流程
  • JWT 数据结构
    • Header:签名算法
    • Payload:标准声明与自定义声明
    • Signature:签名与验证
  • 实战:在 Spring Security 环境中生成与解析 JWT
    • 项目依赖配置
    • 工具类 JwtUtil 实现
    • 单元测试验证
  • 安全注意事项
  • 总结

JWT 核心概念

JWT 是 JSON Web Token 的缩写,基于 RFC 7519 标准,是一种用于安全传输信息的紧凑、自包含的 JSON 对象。紧凑意味着它体积小,可以通过 URL、POST 参数或 HTTP 头部传输;自包含是指载荷中包含了所有必要的信息,避免了服务端再次查询数据库的开销。搭配数字签名后,令牌可以被信任和验证,非常适合前后端分离架构下的无状态认证。

JWT 工作流程

整个认证与授权过程可以划分为两个阶段:登录签发 与 请求验证。
服务端 客户端 服务端 客户端 #mermaid-svg-9VuHAB6pmB7NCpeH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9VuHAB6pmB7NCpeH .error-icon{fill:#552222;}#mermaid-svg-9VuHAB6pmB7NCpeH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9VuHAB6pmB7NCpeH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9VuHAB6pmB7NCpeH .marker.cross{stroke:#333333;}#mermaid-svg-9VuHAB6pmB7NCpeH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9VuHAB6pmB7NCpeH p{margin:0;}#mermaid-svg-9VuHAB6pmB7NCpeH .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9VuHAB6pmB7NCpeH text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9VuHAB6pmB7NCpeH .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-9VuHAB6pmB7NCpeH .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-9VuHAB6pmB7NCpeH #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-9VuHAB6pmB7NCpeH .sequenceNumber{fill:white;}#mermaid-svg-9VuHAB6pmB7NCpeH #sequencenumber{fill:#333;}#mermaid-svg-9VuHAB6pmB7NCpeH #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-9VuHAB6pmB7NCpeH .messageText{fill:#333;stroke:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9VuHAB6pmB7NCpeH .labelText,#mermaid-svg-9VuHAB6pmB7NCpeH .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .loopText,#mermaid-svg-9VuHAB6pmB7NCpeH .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9VuHAB6pmB7NCpeH .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9VuHAB6pmB7NCpeH .noteText,#mermaid-svg-9VuHAB6pmB7NCpeH .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9VuHAB6pmB7NCpeH .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9VuHAB6pmB7NCpeH .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9VuHAB6pmB7NCpeH .actorPopupMenu{position:absolute;}#mermaid-svg-9VuHAB6pmB7NCpeH .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-9VuHAB6pmB7NCpeH .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9VuHAB6pmB7NCpeH .actor-man circle,#mermaid-svg-9VuHAB6pmB7NCpeH line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-9VuHAB6pmB7NCpeH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 后续请求 POST /login (用户名+密码)校验凭证,查询数据库返回 JWT TokenAPI 请求 (Header: Authorization: Bearer <JWT>)验证签名,解析载荷响应数据

  1. 客户端提交用户名、密码进行登录。
  2. 服务端验证凭证后,从数据库加载用户信息,生成包含用户标识、权限等信息的 JWT,返回给客户端。
  3. 客户端在后续所有请求的 Authorization 头中携带该 Token。
  4. 服务端拦截请求,验证 JWT 的签名是否合法,并从载荷中直接获取用户身份与权限,无需再次查询数据库。
  5. 验证通过后,将请求的资源返回。

JWT 数据结构

一个典型的 JWT 字符串如下所示,由两个小数点分割为三部分:

复制代码
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfQURNSU4iXSwiZXhwIjoxNjgxMjM0NTY3LCJpYXQiOjE2ODEyMzQ1MDd9.签名部分

#mermaid-svg-qJHAB2THRdlap0vd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qJHAB2THRdlap0vd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qJHAB2THRdlap0vd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qJHAB2THRdlap0vd .error-icon{fill:#552222;}#mermaid-svg-qJHAB2THRdlap0vd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qJHAB2THRdlap0vd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qJHAB2THRdlap0vd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qJHAB2THRdlap0vd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qJHAB2THRdlap0vd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qJHAB2THRdlap0vd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qJHAB2THRdlap0vd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qJHAB2THRdlap0vd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qJHAB2THRdlap0vd .marker.cross{stroke:#333333;}#mermaid-svg-qJHAB2THRdlap0vd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qJHAB2THRdlap0vd p{margin:0;}#mermaid-svg-qJHAB2THRdlap0vd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qJHAB2THRdlap0vd .cluster-label text{fill:#333;}#mermaid-svg-qJHAB2THRdlap0vd .cluster-label span{color:#333;}#mermaid-svg-qJHAB2THRdlap0vd .cluster-label span p{background-color:transparent;}#mermaid-svg-qJHAB2THRdlap0vd .label text,#mermaid-svg-qJHAB2THRdlap0vd span{fill:#333;color:#333;}#mermaid-svg-qJHAB2THRdlap0vd .node rect,#mermaid-svg-qJHAB2THRdlap0vd .node circle,#mermaid-svg-qJHAB2THRdlap0vd .node ellipse,#mermaid-svg-qJHAB2THRdlap0vd .node polygon,#mermaid-svg-qJHAB2THRdlap0vd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qJHAB2THRdlap0vd .rough-node .label text,#mermaid-svg-qJHAB2THRdlap0vd .node .label text,#mermaid-svg-qJHAB2THRdlap0vd .image-shape .label,#mermaid-svg-qJHAB2THRdlap0vd .icon-shape .label{text-anchor:middle;}#mermaid-svg-qJHAB2THRdlap0vd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qJHAB2THRdlap0vd .rough-node .label,#mermaid-svg-qJHAB2THRdlap0vd .node .label,#mermaid-svg-qJHAB2THRdlap0vd .image-shape .label,#mermaid-svg-qJHAB2THRdlap0vd .icon-shape .label{text-align:center;}#mermaid-svg-qJHAB2THRdlap0vd .node.clickable{cursor:pointer;}#mermaid-svg-qJHAB2THRdlap0vd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qJHAB2THRdlap0vd .arrowheadPath{fill:#333333;}#mermaid-svg-qJHAB2THRdlap0vd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qJHAB2THRdlap0vd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qJHAB2THRdlap0vd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qJHAB2THRdlap0vd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qJHAB2THRdlap0vd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qJHAB2THRdlap0vd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qJHAB2THRdlap0vd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qJHAB2THRdlap0vd .cluster text{fill:#333;}#mermaid-svg-qJHAB2THRdlap0vd .cluster span{color:#333;}#mermaid-svg-qJHAB2THRdlap0vd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qJHAB2THRdlap0vd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qJHAB2THRdlap0vd rect.text{fill:none;stroke-width:0;}#mermaid-svg-qJHAB2THRdlap0vd .icon-shape,#mermaid-svg-qJHAB2THRdlap0vd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qJHAB2THRdlap0vd .icon-shape p,#mermaid-svg-qJHAB2THRdlap0vd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qJHAB2THRdlap0vd .icon-shape .label rect,#mermaid-svg-qJHAB2THRdlap0vd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qJHAB2THRdlap0vd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qJHAB2THRdlap0vd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qJHAB2THRdlap0vd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Base64 解码
Base64 解码
JWT Token
Header
Payload
Signature
alg: HS512
sub: admin, exp, iat...
密钥对 Header+Payload 签名

部分 内容 说明
Header {"alg":"HS512"} 说明签名算法,这里使用 HMAC-SHA512
Payload {"sub":"admin","authorities":["ROLE_USER"],"exp":1681234567,"iat":1681234507} 存放声明(Claims),包含标准字段和自定义字段
Signature 签名结果 使用 Header 中指定的算法,对 Base64 编码后的 Header 和 Payload 进行签名,保证完整性

常用标准声明(Registered Claims)

字段 全称 说明
sub Subject 主题,通常为用户标识
exp Expiration 过期时间,UNIX 时间戳
iat Issued At 签发时间
iss Issuer 签发者
aud Audience 受众

重要 :Header 和 Payload 仅经过 Base64 编码,并非加密,因此绝对不能存放密码、密钥等敏感信息。安全性完全由最后的签名保证。

实战:集成 JJWT 库

在 Java 生态中,JJWT 是使用最广泛的 JWT 库,支持服务端和 Android 端。下面演示如何在 Spring Boot 项目中引入依赖,并编写工具类实现 JWT 的创建与解析。

项目依赖

使用 Maven 时,在 pom.xml 中添加:

xml 复制代码
<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>

若使用 Gradle,可对应转换。这三个模块分别提供 API、实现以及 Jackson 序列化支持。

项目结构

dir 复制代码
src
└── main
    └── java
        └── com
            └── example
                └── security
                    ├── util
                    │   └── JwtUtil.java
                    └── model
                        └── (Spring Security UserDetails 实现)

工具类实现:JwtUtil

该类负责根据 UserDetails 生成 JWT,并提供解析方法。需要注意的是,密钥在实际项目中应从配置中读取,这里为演示方便定义为静态常量。

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

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.security.Key;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

public class JwtUtil {

    // 使用 HS512 算法生成一个符合长度要求的密钥,生产环境请从外部配置读取
    private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS512);

    // Token 有效期,单位:毫秒(示例设为 1 小时)
    private static final long EXPIRATION_MS = 3600_000;

    /**
     * 根据 UserDetails 生成 JWT Token
     * @param userDetails 用户信息
     * @return JWT 字符串
     */
    public static String generateToken(UserDetails userDetails) {
        // 提取权限列表,转换为纯字符串集合
        List<String> authorities = userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());

        Date now = new Date();
        Date expiration = new Date(now.getTime() + EXPIRATION_MS);

        return Jwts.builder()
                .setId(userDetails.getUsername() + "_" + System.currentTimeMillis()) // JWT ID
                .claim("authorities", authorities)                // 自定义声明:权限列表
                .setSubject(userDetails.getUsername())            // 主题:用户名
                .setIssuedAt(now)                                 // 签发时间
                .setExpiration(expiration)                        // 过期时间
                .signWith(SECRET_KEY, SignatureAlgorithm.HS512)   // 签名算法与密钥
                .compact();
    }

    /**
     * 解析 JWT 并返回 Claims
     * @param token JWT 字符串
     * @return 包含声明的 Claims 对象
     */
    public static Claims parseToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(SECRET_KEY)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 从 Token 中提取用户名
     */
    public static String getUsernameFromToken(String token) {
        return parseToken(token).getSubject();
    }

    /**
     * 验证 Token 是否过期
     */
    public static boolean isTokenExpired(String token) {
        return parseToken(token).getExpiration().before(new Date());
    }
}

单元测试

以下测试验证 Token 的生成与解析是否正确,确保载荷中的 subject 和自定义 authorities 与原始数据一致。

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

import org.junit.jupiter.api.Test;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class JwtUtilTest {

    @Test
    void shouldGenerateAndParseTokenSuccessfully() {
        // 1. 构造 UserDetails 对象
        UserDetails user = new User("admin", "password",
                List.of(new SimpleGrantedAuthority("ROLE_USER"),
                        new SimpleGrantedAuthority("ROLE_ADMIN")));

        // 2. 生成 JWT
        String token = JwtUtil.generateToken(user);
        assertNotNull(token);

        // 3. 解析 JWT
        var claims = JwtUtil.parseToken(token);

        // 4. 验证主题(用户名)
        assertEquals("admin", claims.getSubject());

        // 5. 验证自定义权限字段
        List<String> authorities = claims.get("authorities", List.class);
        assertNotNull(authorities);
        assertTrue(authorities.contains("ROLE_USER"));
        assertTrue(authorities.contains("ROLE_ADMIN"));

        // 6. 验证签发时间与过期时间
        assertNotNull(claims.getIssuedAt());
        assertNotNull(claims.getExpiration());
        assertTrue(claims.getExpiration().after(claims.getIssuedAt()));
    }
}

运行测试,若全部通过,说明 JWT 的生成与解析逻辑正确。

安全注意事项

  • 密钥管理:示例中将密钥硬编码在代码中,仅用于演示。实际项目应通过配置文件、环境变量或密钥管理服务注入,并对密钥进行严格的访问控制。
  • 算法选择:HMAC-SHA512 适用于单体或内部服务。在分布式、多服务验签场景下,推荐使用非对称算法(如 RSA 或 ECDSA),由授权服务器持有私钥,资源服务器使用公钥验证。
  • 载荷敏感数据:切勿将密码、令牌 Secret 等敏感信息放入 Payload,因为 Base64 编码极易被解码。
  • 过期时间 :务必设置合理的 exp 声明,并配合刷新令牌机制,降低 Token 泄漏后的风险。

总结

本文从 JWT 的基本概念入手,分析了其自包含、紧凑、可签名的特性,并通过时序图展现了在前后端分离架构下的工作流程。在数据结构部分,我们拆解了 Header、Payload、Signature 三部分的构成与作用,并强调了安全禁忌。

随后,在 Spring Security 技术栈下,基于 JJWT 库完成了依赖引入、工具类编写以及单元测试,完整展示了 JWT 的签发与解析逻辑。掌握这些核心内容,将为后续深入学习 OAuth2 + JWT 的实战奠定坚实基础。

相关推荐
晒太羊的猫11 分钟前
hot100子串
java·算法·leetcode
开开心心就好35 分钟前
本地文件搜索工具,双击Ctrl就能呼出搜索
java·服务器·前端·支持向量机·ffmpeg·音视频·模拟退火算法
独隅40 分钟前
Claude 高效使用指南深度评测
java·开发语言·claude·claude code
ByteCoder.1 小时前
班前十分钟的清单,按体检引导单来写
java·服务器·前端·助播虾
海的透彻1 小时前
如何让docker容器启动时用自定义的用户
java·docker·dubbo
疯狂学习GIS1 小时前
将谷歌地球引擎账号升级为Contributor与Partner层级的方法
java·数据库·人工智能
GG.WinGel1 小时前
质数数目(蓝桥杯练习题)
java·数据结构·算法·青少年编程·蓝桥杯
泡泡糖的中文规格书1 小时前
HDLbits 2.2 学习笔记 · 多路复用器
java·开发语言
John_Hui1 小时前
第 3 篇:配置 JDK 17 环境
java·开发语言·ubuntu