Java JWT 完整教程

目录

  1. 什么是 JWT

  2. JWT 结构详解

  3. JJWT 库使用

  4. Spring Boot 集成

  5. 安全最佳实践


1. 什么是 JWT

1.1 JWT 简介

JSON Web Token (JWT) 是一种开放标准 (RFC 7519),用于在各方之间以 JSON 对象的形式安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。

1.2 传统认证 vs Token 认证

| 传统 Session 认证 | Token 认证 |

|------------------|-----------|

| 服务端存储 Session ID | 无状态,服务端不存储 |

| 需要 Session 存储层 | Token 自包含用户信息 |

| 扩展性差 | 易于水平扩展 |

| 跨域支持困难 | 天然支持跨域 |

1.3 为什么使用 JWT?

  • 无状态: 降低服务器负载

  • 可扩展: 适合分布式/云架构

  • 跨域友好: 适用于微服务架构

  • 自包含: Token 中包含所有必要信息


2. JWT 结构详解

JWT 由三部分组成,用点 (.) 分隔:


[Header].[Payload].[Signature]

示例 Token


eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

2.1 Header(头部)

Base64 解码后:


{

"alg": "HS256",

"typ": "JWT"

}

  • alg: 签名算法(如 HS256、RS256)

  • typ: Token 类型

2.2 Payload(载荷)

Base64 解码后:


{

"sub": "1234567890",

"name": "John Doe",

"admin": true,

"iat": 1466633317,

"exp": 1466636917

}

标准声明(Registered Claims)

| 声明 | 全称 | 说明 |

|-----|------|-----|

| iss | Issuer | 签发者 |

| sub | Subject | 主题(通常是用户ID) |

| aud | Audience | 接收方 |

| exp | Expiration | 过期时间 |

| nbf | Not Before | 生效时间 |

| iat | Issued At | 签发时间 |

| jti | JWT ID | 唯一标识 |

2.3 Signature(签名)

签名计算方式:


HMACSHA256(

base64UrlEncode(header) + "." + base64UrlEncode(payload),

secret

)

⚠️ 重要: 签名只能验证数据完整性,不能加密数据。JWT 内容是 Base64 编码的,任何人都可以解码查看。


3. JJWT 库使用

3.1 添加依赖


<!-- pom.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>

3.2 生成 Token


import io.jsonwebtoken.Jwts;

import io.jsonwebtoken.SignatureAlgorithm;

import io.jsonwebtoken.security.Keys;

import java.security.Key;

import java.util.Date;

  

public class JwtUtil {

// 生成安全的密钥(推荐方式)

private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);

// Token 有效期:10小时

public static final long TOKEN_VALIDITY = 10 * 60 * 60 * 1000;

/**

* 生成 JWT Token

*/

public String generateToken(String username) {

Date now = new Date();

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

return Jwts.builder()

.setSubject(username) // 设置主题(用户名)

.setIssuer("your-app") // 设置签发者

.setIssuedAt(now) // 设置签发时间

.setExpiration(expiration) // 设置过期时间

.claim("role", "admin") // 自定义声明

.signWith(SECRET_KEY) // 签名

.compact(); // 生成紧凑格式

}

}

3.3 解析和验证 Token


import io.jsonwebtoken.Claims;

import io.jsonwebtoken.Jwts;

import io.jsonwebtoken.JwtException;

  

public class JwtUtil {

/**

* 解析 JWT Token

*/

public Claims parseToken(String token) {

return Jwts.parserBuilder()

.setSigningKey(SECRET_KEY)

.build()

.parseClaimsJws(token)

.getBody();

}

/**

* 从 Token 中获取用户名

*/

public String getUsernameFromToken(String token) {

return parseToken(token).getSubject();

}

/**

* 验证 Token 是否有效

*/

public boolean validateToken(String token, String username) {

try {

Claims claims = parseToken(token);

String tokenUsername = claims.getSubject();

Date expiration = claims.getExpiration();

return tokenUsername.equals(username)

&& expiration.after(new Date());

} catch (JwtException e) {

// Token 无效或已被篡改

return false;

}

}

}

3.4 完整的 TokenManager 类


package com.example.jwt.util;

  

import io.jsonwebtoken.Claims;

import io.jsonwebtoken.Jwts;

import io.jsonwebtoken.SignatureAlgorithm;

import io.jsonwebtoken.io.Decoders;

import io.jsonwebtoken.security.Keys;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.stereotype.Component;

  

import java.security.Key;

import java.util.Date;

import java.util.HashMap;

import java.util.Map;

import java.util.function.Function;

  

@Component

public class TokenManager {

  

public static final long TOKEN_VALIDITY = 10 * 60 * 60; // 10小时(秒)

  

@Value("${jwt.secret}")

private String jwtSecret;

  

/**

* 生成 JWT Token

*/

public String generateToken(UserDetails userDetails) {

Map<String, Object> claims = new HashMap<>();

// 可以添加自定义声明

claims.put("roles", userDetails.getAuthorities());

return Jwts.builder()

.setClaims(claims)

.setSubject(userDetails.getUsername())

.setIssuedAt(new Date())

.setExpiration(new Date(System.currentTimeMillis() + TOKEN_VALIDITY * 1000))

.signWith(getSigningKey(), SignatureAlgorithm.HS256)

.compact();

}

  

/**

* 验证 Token

*/

public boolean validateToken(String token, UserDetails userDetails) {

final String username = getUsernameFromToken(token);

return username.equals(userDetails.getUsername()) && !isTokenExpired(token);

}

  

/**

* 从 Token 获取用户名

*/

public String getUsernameFromToken(String token) {

return getClaimFromToken(token, Claims::getSubject);

}

  

/**

* 从 Token 获取过期时间

*/

public Date getExpirationFromToken(String token) {

return getClaimFromToken(token, Claims::getExpiration);

}

  

/**

* 检查 Token 是否过期

*/

private boolean isTokenExpired(String token) {

Date expiration = getExpirationFromToken(token);

return expiration.before(new Date());

}

  

/**

* 获取指定声明

*/

public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {

final Claims claims = getAllClaimsFromToken(token);

return claimsResolver.apply(claims);

}

  

/**

* 获取所有声明

*/

private Claims getAllClaimsFromToken(String token) {

return Jwts.parserBuilder()

.setSigningKey(getSigningKey())

.build()

.parseClaimsJws(token)

.getBody();

}

  

/**

* 获取签名密钥

*/

private Key getSigningKey() {

byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);

return Keys.hmacShaKeyFor(keyBytes);

}

}


4. Spring Boot 集成

4.1 项目配置

application.properties


spring.application.name=jwt-demo

jwt.secret=你的Base64编码的密钥(至少64字符)

4.2 JWT 过滤器


package com.example.jwt.filter;

  

import com.example.jwt.service.JwtUserDetailsService;

import com.example.jwt.util.TokenManager;

import io.jsonwebtoken.ExpiredJwtException;

import jakarta.servlet.FilterChain;

import jakarta.servlet.ServletException;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.context.SecurityContextHolder;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;

import org.springframework.stereotype.Component;

import org.springframework.web.filter.OncePerRequestFilter;

  

import java.io.IOException;

  

@Component

public class JwtFilter extends OncePerRequestFilter {

  

@Autowired

private JwtUserDetailsService userDetailsService;

  

@Autowired

private TokenManager tokenManager;

  

@Override

protected void doFilterInternal(HttpServletRequest request,

HttpServletResponse response,

FilterChain filterChain)

throws ServletException, IOException {

  

// 从请求头获取 Authorization

String authHeader = request.getHeader("Authorization");

String username = null;

String token = null;

  

// 检查是否是 Bearer Token

if (authHeader != null && authHeader.startsWith("Bearer ")) {

token = authHeader.substring(7);

try {

username = tokenManager.getUsernameFromToken(token);

} catch (IllegalArgumentException e) {

System.out.println("无法获取 JWT Token");

} catch (ExpiredJwtException e) {

System.out.println("JWT Token 已过期");

}

}

  

// 验证 Token 并设置认证信息

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

UserDetails userDetails = userDetailsService.loadUserByUsername(username);

if (tokenManager.validateToken(token, userDetails)) {

UsernamePasswordAuthenticationToken authToken =

new UsernamePasswordAuthenticationToken(

userDetails, null, userDetails.getAuthorities());

authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authToken);

}

}

  

filterChain.doFilter(request, response);

}

}

4.3 Security 配置


package com.example.jwt.config;

  

import com.example.jwt.filter.JwtFilter;

import com.example.jwt.security.JwtAuthenticationEntryPoint;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;

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.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.web.SecurityFilterChain;

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

  

@Configuration

@EnableWebSecurity

public class SecurityConfig {

  

@Autowired

private JwtAuthenticationEntryPoint authenticationEntryPoint;

  

@Autowired

private JwtFilter jwtFilter;

  

@Bean

public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

  

@Bean

public AuthenticationManager authenticationManager(

AuthenticationConfiguration authConfig) throws Exception {

return authConfig.getAuthenticationManager();

}

  

@Bean

public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

http

// 禁用 CSRF(使用 JWT 不需要)

.csrf(csrf -> csrf.disable())

// 配置请求授权

.authorizeHttpRequests(auth -> auth

.requestMatchers("/api/auth/**").permitAll() // 登录注册接口放行

.requestMatchers("/api/public/**").permitAll() // 公开接口放行

.anyRequest().authenticated() // 其他接口需要认证

)

// 异常处理

.exceptionHandling(ex -> ex

.authenticationEntryPoint(authenticationEntryPoint)

)

// 无状态 Session

.sessionManagement(session -> session

.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

)

// 添加 JWT 过滤器

.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

  

return http.build();

}

}

4.4 登录控制器


package com.example.jwt.controller;

  

import com.example.jwt.dto.LoginRequest;

import com.example.jwt.dto.LoginResponse;

import com.example.jwt.service.JwtUserDetailsService;

import com.example.jwt.util.TokenManager;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.http.ResponseEntity;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.authentication.BadCredentialsException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.web.bind.annotation.*;

  

@RestController

@RequestMapping("/api/auth")

@CrossOrigin

public class AuthController {

  

@Autowired

private AuthenticationManager authenticationManager;

  

@Autowired

private JwtUserDetailsService userDetailsService;

  

@Autowired

private TokenManager tokenManager;

  

@PostMapping("/login")

public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {

try {

// 认证用户

authenticationManager.authenticate(

new UsernamePasswordAuthenticationToken(

request.getUsername(),

request.getPassword()

)

);

} catch (BadCredentialsException e) {

throw new RuntimeException("用户名或密码错误");

}

  

// 生成 Token

UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());

String token = tokenManager.generateToken(userDetails);

  

return ResponseEntity.ok(new LoginResponse(token));

}

}


5. 安全最佳实践

5.1 常见攻击与防护

5.1.1 None 算法攻击

问题: 攻击者将算法改为 none,绕过签名验证。

防护:


// 明确指定期望的算法

JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret))

.build();

5.1.2 Token 劫持(Sidejacking)

问题: Token 被拦截后,攻击者可以冒充用户。

防护: 添加用户上下文指纹


// 生成随机指纹

String fingerprint = generateRandomString();

  

// 将指纹哈希存入 Token

String fingerprintHash = sha256(fingerprint);

String token = Jwts.builder()

.claim("fingerprint", fingerprintHash)

// ...

.compact();

  

// 将原始指纹存入 HttpOnly Cookie

Cookie cookie = new Cookie("__Secure-Fgp", fingerprint);

cookie.setHttpOnly(true);

cookie.setSecure(true);

cookie.setPath("/");

5.1.3 弱密钥攻击

问题: 使用弱密钥,攻击者可以暴力破解。

防护:


// ❌ 错误示例

String secret = "secret";

  

// ✅ 正确示例:使用足够长度的随机密钥

Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

// 或使用至少 64 字符的随机字符串

5.2 Token 存储建议

| 存储方式 | 优点 | 缺点 | 推荐场景 |

|---------|------|------|---------|

| sessionStorage | 标签页关闭即清除 | 不能跨标签页 | 高安全要求 |

| localStorage | 持久化存储 | XSS 风险 | 一般应用 |

| HttpOnly Cookie | 防 XSS | 需防 CSRF | 传统 Web 应用 |

| 内存(闭包) | 最安全 | 刷新丢失 | SPA 应用 |

5.3 前端存储示例


// 使用 sessionStorage 存储

function login(username, password) {

fetch('/api/auth/login', {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: JSON.stringify({ username, password })

})

.then(res => res.json())

.then(data => {

sessionStorage.setItem('token', data.token);

});

}

  

// 请求时携带 Token

function fetchWithAuth(url, options = {}) {

const token = sessionStorage.getItem('token');

return fetch(url, {

...options,

headers: {

...options.headers,

'Authorization': `Bearer ${token}`

}

});

}

5.4 Token 刷新机制


@PostMapping("/refresh")

public ResponseEntity<LoginResponse> refreshToken(

@RequestHeader("Authorization") String authHeader) {

String oldToken = authHeader.substring(7);

String username = tokenManager.getUsernameFromToken(oldToken);

// 验证旧 Token(可以允许已过期但未超过刷新窗口的 Token)

UserDetails userDetails = userDetailsService.loadUserByUsername(username);

// 生成新 Token

String newToken = tokenManager.generateToken(userDetails);

return ResponseEntity.ok(new LoginResponse(newToken));

}

5.5 Token 黑名单(登出实现)


@Service

public class TokenBlacklistService {

// 使用 Redis 存储黑名单

@Autowired

private RedisTemplate<String, String> redisTemplate;

public void blacklistToken(String token) {

// 获取 Token 剩余有效时间

Date expiration = tokenManager.getExpirationFromToken(token);

long ttl = expiration.getTime() - System.currentTimeMillis();

if (ttl > 0) {

// 将 Token 加入黑名单,过期时间与 Token 一致

redisTemplate.opsForValue().set(

"blacklist:" + token,

"1",

ttl,

TimeUnit.MILLISECONDS

);

}

}

public boolean isBlacklisted(String token) {

return redisTemplate.hasKey("blacklist:" + token);

}

}


附录

A. 常用签名算法

| 算法 | 类型 | 密钥长度 | 说明 |

|-----|------|---------|------|

| HS256 | 对称 | 256 位 | HMAC + SHA-256 |

| HS384 | 对称 | 384 位 | HMAC + SHA-384 |

| HS512 | 对称 | 512 位 | HMAC + SHA-512 |

| RS256 | 非对称 | 2048+ 位 | RSA + SHA-256 |

| ES256 | 非对称 | 256 位 | ECDSA + SHA-256 |

B. 参考资源


📝 提示: 生产环境中务必使用 HTTPS,并定期轮换密钥。

写文章用