jwt原理及Java中实现过程

 更新时间:2025年08月28日 08:53:09   作者:༒࿈༙྇洞察༙༙྇྇࿈༒  
JWT是无状态认证的JSON令牌,包含头部、载荷和签名,用于身份验证与权限管理,需注意设置过期时间、使用非对称算法、安全存储传输,及合理刷新策略,确保系统安全与效率

一、JWT 是什么?解决什么问题?

  • 我们先来一张图看一下这个过程:


JWT(JSON Web Token)是一种把“认证信息(Claims)+ 完整性校验”打包成 自包含 的字符串的规范。

它主要用于无状态认证:服务端验证签名即可信任其中的身份与权限,无需每次查库或维护会话(session)。

  • 无状态:后端不存会话;减少分布式共享状态的复杂度。
  • 可扩展:把自定义字段写入 claims(如角色、租户、权限)。
  • 可委托:不同服务/网关只要有验证密钥就能核验并信任。

但请记住:JWT 只保证 完整性(没被篡改),默认不保密(除非用 JWE 加密)。敏感信息不要塞进未加密的 JWT。

二、JWT 的结构与签名流程

JWT 的字符串形如:<header>.<payload>.<signature>

Header(JSON,Base64URL)

  • alg: 签名算法(如 HS256 / RS256 / ES256 / EdDSA
  • typ: 通常为 "JWT"
  • kid(可选):密钥标识,用于密钥轮换

Payload/Claims(JSON,Base64URL)

常见注册声明:

  • iss(颁发者)、sub(主体,通常是用户ID)、aud(受众)
  • exp(过期时间,秒级时间戳,必须!)、nbf(不早于)、iat(签发时间)
  • jti(唯一ID,用于一次性/黑名单)
    以及你的自定义字段rolestenantIdscope 等。

Signature

  • 计算方式:signature = Sign( base64url(header) + "." + base64url(payload), key, alg )
  • 验证时:使用共享密钥(HMAC)或公钥(RSA/ECDSA/EdDSA)验证。

JWS vs JWE

  • JWS(最常用):签名但不加密;任何人拿到 token 都能看到 payload。
  • JWE:加密(可选),适用于含敏感信息的场景。

三、JWT 使用流程(最小闭环)

  1. 登录:用户名密码校验成功 → 颁发短期 Access Token(JWT)+ 较长期 Refresh Token(不可见给前端或放 HttpOnly Cookie)。
  2. 访问 API:前端将 Authorization: Bearer <jwt> 送给后端。
  3. 后端:验证签名、校验 exp/nbf/aud/iss 等 → 放行。
  4. 刷新:Access Token 过期,用 Refresh Token 换新(做轮换失效控制)。
  5. 登出/撤销(可选):把 jti 或 refresh 的标识加入黑名单,或进行密钥轮换

四、常见安全陷阱(一定要看)

  • ❌ 不设置 exp(永不过期,风险极大)。
  • alg: none(严格禁用)。
  • 密钥混淆:把对称密钥误当作公钥发布;或同一 kid 指向错密钥。
  • HS256 在多服务扩散:一旦泄露,所有服务都可伪造。跨服务建议 RS256/ES256/EdDSA(私钥签、公钥验)。
  • ❌ 不校验 aud/iss:导致“错配 token”被误信任。
  • ❌ 客户端 localStorage 存储 → 易受 XSS 影响。推荐 HttpOnly + Secure + SameSite Cookie
  • ❌ 忽视 CSRF:若用 Cookie 携带 Access Token,要配合 SameSite + CSRF 令牌 或改为 Bearer 头。
  • ❌ 不做 Refresh Token 轮换 与黑名单 → 被盗后长期可用。
  • ❌ 把敏感信息(如身份证、银行卡、密码)塞进未加密 JWT。

五、Java 手写(JJWT)创建与验证

1) 依赖(Maven)

<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> <!-- for JSON serialization -->
  <scope>runtime</scope>
</dependency>

若用 RSA/EC/EdDSA:还需对应的 jjwt-xxx 或者用 java.security 生成密钥。

2) 生成密钥(示例:RSA 与 Ed25519)

// RSA 2048(推荐生产至少 2048)
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair rsaKeyPair = kpg.generateKeyPair();

// Ed25519(更轻更快)
KeyPairGenerator ed = KeyPairGenerator.getInstance("Ed25519");
KeyPair edKeyPair = ed.generateKeyPair();

3) 颁发 JWT(RS256 或 EdDSA)

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.time.Instant;
import java.util.Date;
import java.util.Map;

// 使用 RSA 私钥签名(RS256)
String token = Jwts.builder()
    .setHeaderParam("kid", "key-2025-08")     // 便于轮换
    .setIssuer("https://auth.example.com")
    .setSubject("user-123")
    .setAudience("api.example.com")
    .setIssuedAt(new Date())
    .setExpiration(Date.from(Instant.now().plusSeconds(900))) // 15 分钟
    .addClaims(Map.of(
        "roles", new String[]{"ADMIN","USER"},
        "tenantId", "t-1001"
    ))
    .signWith(rsaKeyPair.getPrivate())        // 默认按密钥类型选择 RS256/ES256/EdDSA
    .compact();

signWith(PrivateKey):JJWT 会自动选合适 alg;如想强制算法,可用新版签名 API 指定 SignatureAlgorithm.

4) 验证 JWT(公钥验签 + 校验声明)

import io.jsonwebtoken.*;

Jws<Claims> jws = Jwts.parserBuilder()
    .requireIssuer("https://auth.example.com")
    .requireAudience("api.example.com")
    .setAllowedClockSkewSeconds(60) // 允许 60s 时钟偏差
    .setSigningKey(rsaKeyPair.getPublic())    // 或使用 JWKS 拉取的公钥
    .build()
    .parseClaimsJws(token);

Claims claims = jws.getBody();
String userId = claims.getSubject();
String[] roles = claims.get("roles", String[].class);

校验失败会抛异常(如 ExpiredJwtExceptionSignatureException)。

在网关/过滤器中统一捕获 → 返回 401/403。

六、Spring Boot(Resource Server)零胶水校验

最省心的是让 Spring Security 资源服务器替你做解析与校验,它支持 JWK 集合(JWKS) 自动远程拉取公钥。

1) 依赖

<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>

2) application.yml(通过 JWKS URL 校验)

server:
  port: 8080

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: https://auth.example.com/.well-known/jwks.json
          issuer-uri: https://auth.example.com   # 建议同时配置,做 iss 校验

你的授权服务器(自建或第三方,如 Auth0/Keycloak/Spring Authorization Server)对外暴露 JWKS。资源服自动缓存和轮询kid 取公钥

3) 安全配置

@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
    http
      .csrf(csrf -> csrf.disable()) // 如果前端走 Bearer 头,可关;若走 Cookie,需要保留并配置 CSRF
      .authorizeHttpRequests(reg -> reg
          .requestMatchers("/public/**").permitAll()
          .requestMatchers("/admin/**").hasRole("ADMIN")
          .anyRequest().authenticated()
      )
      .oauth2ResourceServer(oauth2 -> oauth2
          .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()))
      );
    return http.build();
}

// 可选:把自定义 claims 映射为权限
Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthConverter() {
    return jwt -> {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        List<String> roles = jwt.getClaimAsStringList("roles");
        if (roles != null) {
            roles.forEach(r -> authorities.add(new SimpleGrantedAuthority("ROLE_" + r)));
        }
        return new JwtAuthenticationToken(jwt, authorities, jwt.getSubject());
    };
}

4) 控制器示例

@RestController
public class DemoController {

  @GetMapping("/me")
  public Map<String, Object> me(@AuthenticationPrincipal Jwt jwt) {
    return Map.of(
      "sub", jwt.getSubject(),
      "roles", jwt.getClaimAsStringList("roles"),
      "tenantId", jwt.getClaim("tenantId")
    );
  }

  @GetMapping("/admin/hello")
  public String admin() { return "hello, admin"; }
}

七、发行端:用 Spring Authorization Server 签发 JWT(概念位)

如果你既要颁发又要验证

  • 引入 spring-authorization-server,配置客户端、用户认证、签名密钥(支持 RSA/ECDSA/EdDSA),开启 /.well-known/jwks.json
  • 认证成功后,框架自动颁发 Access Token(JWT)Refresh Token
  • 资源服务器只需 issuer-uri/jwk-set-uri 即可对接。

好处:密钥管理、轮换、标准化授权流程(OAuth2/OIDC) 都交给框架;你专注业务。

八、Cookie vs Header、CSRF 与前端存储

推荐Authorization: Bearer <jwt> 置于请求头,前端保存在内存(刷新丢失)或安全容器;刷新策略依赖 HttpOnly Refresh Cookie。

若必须把 Access Token 放 Cookie

  • 设置:HttpOnly + Secure + SameSite=Lax/Strict
  • 开启并正确处理 CSRF 防护(基于 Cookie 的双重提交策略或框架自带 CSRF Token)。

不要放 localStorage(XSS 风险大)。

九、刷新与撤销(实战策略)

短期 Access Token(5–15 分钟) + 长期 Refresh Token(7–30 天)

Refresh Token 轮换:每次刷新都颁发新 refresh,并使旧的失效(存库并维护 revoked 标记或版本号)。

黑名单/撤销

  • 记录 Access Token 的 jti(可选)用于紧急撤销;
  • 更常用的是缩短 Access Token寿命 + 轮换 Refresh;
  • 密钥轮换:更换私钥(新 kid),强制旧 token 逐步失效(需兼容一段时间,等旧 token 过期)。

十、微服务与网关

  • 首选:网关或每个服务自行校验 JWT(拿到 JWKS 公钥本地验);不要把解析结果当作“可信 JSON”直接传递。
  • aud/iss:为不同受众(微服务)使用不同 aud,防止“错用 token”。
  • 性能:缓存 JWKS、公钥对象;JWT 验证成本很低,通常不是瓶颈。

十一、完整示例:无授权服务器时的“轻量颁发 + 校验”

1) 颁发端(登录成功后)

// 假设你用 Spring Security 自己做用户名/密码认证
@PostMapping("/auth/login")
public Map<String, String> login(@RequestBody LoginReq req) {
    // 1. 校验用户名密码(略)
    // 2. 颁发 Token
    Instant now = Instant.now();
    String access = Jwts.builder()
        .setHeaderParam("kid", "key-2025-08")
        .setIssuer("https://auth.example.com")
        .setSubject("user-" + req.username())
        .setAudience("api.example.com")
        .setIssuedAt(Date.from(now))
        .setExpiration(Date.from(now.plusSeconds(900)))
        .claim("roles", List.of("USER"))
        .signWith(rsaPrivateKey) // 你的私钥
        .compact();

    String refreshId = UUID.randomUUID().toString(); // 存入数据库,标记有效
    String refresh = Jwts.builder()
        .setIssuer("https://auth.example.com")
        .setSubject("user-" + req.username())
        .setId(refreshId)
        .setIssuedAt(Date.from(now))
        .setExpiration(Date.from(now.plusSeconds(30 * 24 * 3600))) // 30 天
        .signWith(rsaPrivateKey)
        .compact();

    // refresh 建议放 HttpOnly Cookie 返回
    return Map.of("access_token", access, "token_type", "Bearer");
}

2) 刷新端点

@PostMapping("/auth/refresh")
public Map<String, String> refresh(@CookieValue("refresh_token") String refreshToken) {
    // 1. 验证 refreshToken 签名与过期
    Jws<Claims> jws = Jwts.parserBuilder()
        .setSigningKey(rsaPublicKey)
        .build()
        .parseClaimsJws(refreshToken);

    String jti = jws.getBody().getId();
    // 2. 校验 jti 是否未吊销,且未被使用(轮换)
    // 3. 颁发新 access(并轮换 refresh:生成新 refresh,旧的置为 revoked)

    String newAccess = ...;
    // Set-Cookie: refresh_token=<new>; HttpOnly; Secure; SameSite=Strict
    return Map.of("access_token", newAccess, "token_type", "Bearer");
}

3) 资源服务(校验端,若不用 Resource Server Starter)

自定义过滤器(不建议重复造轮子,演示用):

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

  private final PublicKey publicKey;

  public JwtAuthFilter(PublicKey publicKey) { this.publicKey = publicKey; }

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

    String auth = req.getHeader("Authorization");
    if (auth != null && auth.startsWith("Bearer ")) {
      String token = auth.substring(7);
      try {
        Jws<Claims> jws = Jwts.parserBuilder()
            .requireIssuer("https://auth.example.com")
            .requireAudience("api.example.com")
            .setAllowedClockSkewSeconds(60)
            .setSigningKey(publicKey)
            .build()
            .parseClaimsJws(token);

        Claims c = jws.getBody();
        List<GrantedAuthority> auths = new ArrayList<>();
        List<String> roles = c.get("roles", List.class);
        if (roles != null) roles.forEach(r -> auths.add(new SimpleGrantedAuthority("ROLE_" + r)));

        UsernamePasswordAuthenticationToken authentication =
            new UsernamePasswordAuthenticationToken(c.getSubject(), null, auths);

        SecurityContextHolder.getContext().setAuthentication(authentication);
      } catch (JwtException e) {
        res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        return;
      }
    }
    chain.doFilter(req, res);
  }
}

十二、测试要点清单(上线前自查)

  • exp/nbf/iat/iss/aud 均有严格校验;允许小量 clock skew
  • ✅ 禁止 alg: none,不允许客户端指定算法。
  • ✅ 使用 非对称算法(RS/ES/EdDSA) 做跨服务验证;对称密钥仅限单体/网关内部。
  • ✅ 开启并演练 密钥轮换kid + JWKS),旧公钥保留到所有 token 过期。
  • ✅ 访问控制基于 最小权限(角色/权限来源可在 claims 或 DB)。
  • ✅ 选择合适的 存储与传输方式(Bearer 头 或 HttpOnly Cookie + CSRF 防护)。
  • 短期 Access + 轮换 Refresh;可选黑名单(jti)应急撤销。
  • ✅ 日志中绝不打印完整 token(最多打前后各 6 位用于排错)。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • springBoot解决static和@Component遇到的bug

    springBoot解决static和@Component遇到的bug

    这篇文章主要介绍了springBoot解决static和@Component遇到的bug,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • Java用文件流下载网络文件示例代码

    Java用文件流下载网络文件示例代码

    这篇文章主要介绍了Java用文件流的方式下载网络文件,大家参考使用吧
    2013-11-11
  • SpringBoot统一数据返回的几种方式

    SpringBoot统一数据返回的几种方式

    在Web应用程序开发中,统一数据返回格式对于前后端分离项目尤为重要,本文就来介绍一下SpringBoot统一数据返回的几种方式,具有一定的参考价值,感兴趣的可以了解一下
    2024-07-07
  • Java中的Vector详细解读

    Java中的Vector详细解读

    这篇文章主要介绍了Java中的Vector详细解读,Vector是实现了List接口的子类,其底层是一个对象数组,维护了一个elementData数组,是线程安全的,Vector类的方法带有synchronized关键字,在开发中考虑线程安全中使用Vector,需要的朋友可以参考下
    2023-09-09
  • springboot后端接收前端传数组参数三种方法

    springboot后端接收前端传数组参数三种方法

    这篇文章主要给大家介绍了关于springboot后端接收前端传数组参数三种方法,文中通过实例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2023-07-07
  • logback整合rabbitmq实现消息记录日志的配置

    logback整合rabbitmq实现消息记录日志的配置

    这篇文章主要介绍了logback整合rabbitmq实现消息记录日志的配置,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-12-12
  • Java基础教程之类型转换与多态

    Java基础教程之类型转换与多态

    这篇文章主要介绍了Java基础教程之类型转换与多态,本文讲解了 基本类型转换、 upcast与多态、 Object类等内容,需要的朋友可以参考下
    2014-09-09
  • java微信server录音下载到自己server

    java微信server录音下载到自己server

    这篇文章主要为大家详细介绍了java微信server录音下载到自己server的相关代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-05-05
  • SpringBoot浅析Redis访问操作使用

    SpringBoot浅析Redis访问操作使用

    Redis是一个速度非常快的非关系数据库(Non-Relational Database),它可以存储键(Key)与多种不同类型的值(Value)之间的映射(Mapping),可以将存储在内存的键值对数据持久化到硬盘,可以使用复制特性来扩展读性能,还可以使用客户端分片来扩展写性能
    2022-11-11
  • SpringBoot对接Spark过程详解

    SpringBoot对接Spark过程详解

    这篇文章主要介绍SpringBoot接入Spark的方法的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望能帮助大家解决问题
    2023-02-02

最新评论