jwt原理及Java中实现

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

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

JWT(JSON Web Token)是一种把"认证信息(Claims)+ 完整性校验"打包成 自包含 的字符串的规范。
它主要用于
无状态认证
:服务端验证签名即可信任其中的身份与权限,无需每次查库或维护会话(session)。

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

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


二、JWT 的结构与签名流程

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

  1. Header(JSON,Base64URL)

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

    常见注册声明:

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

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

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

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

java 复制代码
// 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)

java 复制代码
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(公钥验签 + 校验声明)

java 复制代码
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) 依赖

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>

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

yaml 复制代码
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) 安全配置

java 复制代码
@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) 控制器示例

java 复制代码
@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) 颁发端(登录成功后)

java 复制代码
// 假设你用 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) 刷新端点

java 复制代码
@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)

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

java 复制代码
@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 位用于排错)。
相关推荐
考虑考虑19 小时前
Jpa使用union all
java·spring boot·后端
用户37215742613520 小时前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊21 小时前
Java学习第22天 - 云原生与容器化
java
渣哥1 天前
原来 Java 里线程安全集合有这么多种
java
间彧1 天前
Spring Boot集成Spring Security完整指南
java
间彧1 天前
Spring Secutiy基本原理及工作流程
java
Java水解1 天前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆1 天前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学1 天前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole1 天前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端