JWT 使用教程
2024/1/1大约 6 分钟
JWT (JSON Web Token) 使用教程
什么是JWT?
JWT (JSON Web Token) 是一种开放标准 (RFC 7519),用于在各方之间安全地传输信息作为JSON对象。JWT可以被验证和信任,因为它是数字签名的。
📋 目录
🎯 JWT 的组成部分
JWT由三个部分组成,用点 (.
) 分隔:
Header.Payload.Signature
1. Header (头部)
包含令牌的类型和使用的签名算法:
{
"alg": "HS256",
"typ": "JWT"
}
2. Payload (载荷)
包含声明 (claims),声明是关于实体(通常是用户)和其他数据的声明:
{
"sub": "1234567890",
"name": "张三",
"iat": 1516239022,
"exp": 1516242622
}
3. Signature (签名)
用于验证消息在传输过程中没有被篡改:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
🛠️ Java 中的 JWT 实现
依赖配置
首先,在 pom.xml
中添加JWT依赖:
<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>
JWT 工具类
点击查看完整的JWT工具类代码
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
// 密钥,实际项目中应该从配置文件读取
private static final String SECRET_KEY = "your-secret-key-must-be-at-least-256-bits-long";
// 过期时间:24小时
private static final long EXPIRATION_TIME = 24 * 60 * 60 * 1000;
// 生成密钥
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
}
/**
* 生成JWT令牌
* @param username 用户名
* @param userId 用户ID
* @return JWT令牌字符串
*/
public String generateToken(String username, Long userId) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", username);
claims.put("userId", userId);
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 验证JWT令牌
* @param token JWT令牌
* @return 是否有效
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
/**
* 从JWT令牌中提取用户名
* @param token JWT令牌
* @return 用户名
*/
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
/**
* 从JWT令牌中提取用户ID
* @param token JWT令牌
* @return 用户ID
*/
public Long extractUserId(String token) {
Claims claims = extractAllClaims(token);
return claims.get("userId", Long.class);
}
/**
* 检查令牌是否过期
* @param token JWT令牌
* @return 是否过期
*/
public boolean isTokenExpired(String token) {
Date expiration = extractExpiration(token);
return expiration.before(new Date());
}
/**
* 提取过期时间
* @param token JWT令牌
* @return 过期时间
*/
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
/**
* 提取指定声明
* @param token JWT令牌
* @param claimsResolver 声明解析器
* @return 声明值
*/
private <T> T extractClaim(String token, java.util.function.Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
/**
* 提取所有声明
* @param token JWT令牌
* @return 所有声明
*/
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
}
认证过滤器
点击查看JWT认证过滤器代码
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
// 检查Authorization头部是否以"Bearer "开头
if (authHeader != null && authHeader.startsWith("Bearer ")) {
jwt = authHeader.substring(7);
try {
username = jwtUtil.extractUsername(jwt);
} catch (Exception e) {
logger.error("JWT解析失败", e);
}
}
// 如果用户名不为空且当前没有认证信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 验证JWT令牌
if (jwtUtil.validateToken(jwt)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
控制器示例
点击查看认证控制器代码
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final UserService userService;
public AuthController(AuthenticationManager authenticationManager,
JwtUtil jwtUtil,
UserService userService) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
this.userService = userService;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
try {
// 验证用户名密码
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
// 获取用户信息
User user = userService.findByUsername(loginRequest.getUsername());
// 生成JWT令牌
String token = jwtUtil.generateToken(user.getUsername(), user.getId());
return ResponseEntity.ok(new LoginResponse(token, user));
} catch (Exception e) {
return ResponseEntity.badRequest().body("用户名或密码错误");
}
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest registerRequest) {
// 检查用户名是否已存在
if (userService.existsByUsername(registerRequest.getUsername())) {
return ResponseEntity.badRequest().body("用户名已存在");
}
// 创建新用户
User user = new User();
user.setUsername(registerRequest.getUsername());
user.setPassword(registerRequest.getPassword()); // 实际应该加密
user.setEmail(registerRequest.getEmail());
User savedUser = userService.save(user);
// 生成JWT令牌
String token = jwtUtil.generateToken(savedUser.getUsername(), savedUser.getId());
return ResponseEntity.ok(new LoginResponse(token, savedUser));
}
@GetMapping("/profile")
public ResponseEntity<?> getProfile(@RequestHeader("Authorization") String token) {
try {
String jwt = token.substring(7);
String username = jwtUtil.extractUsername(jwt);
Long userId = jwtUtil.extractUserId(jwt);
User user = userService.findById(userId);
return ResponseEntity.ok(user);
} catch (Exception e) {
return ResponseEntity.badRequest().body("令牌无效");
}
}
}
🔐 安全最佳实践
1. 密钥管理
重要提醒
- 密钥长度至少256位
- 不要将密钥硬编码在代码中
- 使用环境变量或配置文件存储密钥
- 定期轮换密钥
// 推荐做法:从配置文件读取密钥
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private long expirationTime;
2. 令牌过期策略
// 设置合理的过期时间
private static final long ACCESS_TOKEN_EXPIRATION = 15 * 60 * 1000; // 15分钟
private static final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60 * 1000; // 7天
3. 刷新令牌机制
点击查看刷新令牌实现
@Component
public class RefreshTokenService {
private final JwtUtil jwtUtil;
private final RefreshTokenRepository refreshTokenRepository;
public RefreshTokenService(JwtUtil jwtUtil, RefreshTokenRepository refreshTokenRepository) {
this.jwtUtil = jwtUtil;
this.refreshTokenRepository = refreshTokenRepository;
}
/**
* 生成刷新令牌
*/
public RefreshToken generateRefreshToken(Long userId) {
RefreshToken refreshToken = new RefreshToken();
refreshToken.setUserId(userId);
refreshToken.setToken(UUID.randomUUID().toString());
refreshToken.setExpiryDate(Instant.now().plusMillis(REFRESH_TOKEN_EXPIRATION));
return refreshTokenRepository.save(refreshToken);
}
/**
* 验证刷新令牌
*/
public RefreshToken validateRefreshToken(String token) {
RefreshToken refreshToken = refreshTokenRepository.findByToken(token)
.orElseThrow(() -> new RuntimeException("刷新令牌不存在"));
if (refreshToken.getExpiryDate().compareTo(Instant.now()) < 0) {
refreshTokenRepository.delete(refreshToken);
throw new RuntimeException("刷新令牌已过期");
}
return refreshToken;
}
/**
* 刷新访问令牌
*/
public String refreshAccessToken(String refreshToken) {
RefreshToken token = validateRefreshToken(refreshToken);
User user = userService.findById(token.getUserId());
return jwtUtil.generateToken(user.getUsername(), user.getId());
}
}
📝 常见问题解答
::: faq 常见问题
Q: JWT令牌存储在客户端安全吗?
A: JWT令牌本身是安全的,因为它们是签名的。但是,应该存储在安全的地方:
- 使用HttpOnly Cookie(推荐)
- 存储在localStorage(需要额外安全措施)
- 使用内存存储(单页应用)
Q: 如何撤销JWT令牌?
A: JWT本身是无状态的,无法直接撤销。可以通过以下方式实现:
- 维护一个黑名单
- 使用较短的过期时间
- 实现刷新令牌机制
Q: JWT和Session有什么区别?
A:
- JWT是无状态的,服务器不需要存储会话信息
- Session是有状态的,服务器需要存储会话数据
- JWT更适合分布式系统
- Session更适合单体应用
:::
🎨 界面美化
代码高亮
// 这段代码展示了如何生成JWT令牌
public String generateToken(String username, Long userId) {
return Jwts.builder()
.setSubject(username)
.claim("userId", userId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
信息提示框
提示
JWT是一种轻量级的认证方式,特别适合微服务架构和单页应用。
最佳实践
- 始终使用HTTPS传输JWT
- 设置合理的过期时间
- 实现令牌刷新机制
- 监控令牌使用情况
安全警告
- 不要在JWT中存储敏感信息
- 定期轮换密钥
- 实现令牌撤销机制
危险操作
- 不要将密钥提交到版本控制系统
- 不要在前端代码中硬编码密钥
- 不要使用过短的密钥
📚 相关资源
🏷️ 标签
JWTJava安全认证感谢阅读!如果您有任何问题,欢迎在评论区留言。