SpringBoot JWT安全认证指南
JWT概述
什么是JWT?
- JSON Web Token (JWT) 是一种紧凑、自包含的方式,用于在各方之间安全地传输信息
- 主要用于身份验证和信息交换
- 由三部分组成:Header、Payload、Signature
JWT的优势
- 无状态认证
- 轻量级
- 跨语言支持
- 可扩展性强
项目依赖配置
Maven依赖
<dependencies>
<!-- 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>
</dependencies>JWT配置属性类
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
private String secret;
private Long expiration;
// Getter and Setter
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Long getExpiration() {
return expiration;
}
public void setExpiration(Long expiration) {
this.expiration = expiration;
}
}application.yml
jwt:
secret: your_secure_secret_key_here
expiration: 3600000 # 1小时过期时间配置启动类
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class AppConfig {
}JwtUtil工具类
package com.cq.rssdemo.utils;
import com.cq.rssdemo.config.JwtProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
@Autowired
private JwtProperties jwtProperties;
// 生成签名密钥
private Key getSigningKey() {
// 确保秘钥长度至少为256位(32字节)
byte[] keyBytes = jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8);
// 如果秘钥太短,可以使用以下方法生成安全的秘钥
return Keys.hmacShaKeyFor(ensureKeyLength(keyBytes));
}
// 确保密钥长度至少为32字节的辅助方法
private byte[] ensureKeyLength(byte[] originalKey) {
if (originalKey.length >= 32) {
return originalKey;
}
// 如果原始密钥太短,使用填充或重复方法扩展
byte[] extendedKey = new byte[32];
for (int i = 0; i < 32; i++) {
extendedKey[i] = originalKey[i % originalKey.length];
}
return extendedKey;
}
// 从token中获取用户名
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// 获取token过期时间
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
// 通用的获取Claim的方法
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
// 解析token的所有声明
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// 检查token是否已过期
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// 生成token
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, username);
}
// 生成token的具体实现
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// 验证token是否有效
public Boolean validateToken(String token, String username) {
final String tokenUsername = getUsernameFromToken(token);
return (tokenUsername.equals(username) && !isTokenExpired(token));
}
// 刷新token
public String refreshToken(String token) {
final Claims claims = getAllClaimsFromToken(token);
claims.setIssuedAt(new Date());
claims.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()));
return Jwts.builder()
.setClaims(claims)
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
}常见问题与解决方案
秘钥长度问题
- 确保秘钥长度至少为256位(32字节)
- 使用
Keys.hmacShaKeyFor()生成安全秘钥 - 避免使用过短或可预测的秘钥
秘钥管理建议
- 使用环境变量存储秘钥
- 定期轮换秘钥
- 避免将秘钥硬编码
- 使用安全的秘钥管理系统
测试
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class JwtUtilTest {
@Autowired
private JwtUtil jwtUtil;
private String username;
private String token;
@BeforeEach
public void setup() {
// 设置测试用户名
username = "testuser@example.com";
// 生成测试Token
token = jwtUtil.generateToken(username);
}
@Test
public void testTokenGeneration() {
// 测试令牌生成
assertNotNull(token, "Token should not be null");
assertTrue(token.length() > 0, "Token should have length");
}
@Test
public void testUsernameExtraction() {
// 测试从令牌中提取用户名
String extractedUsername = jwtUtil.getUsernameFromToken(token);
assertEquals(username, extractedUsername, "Extracted username should match original");
}
@Test
public void testTokenValidation() {
// 测试令牌验证
boolean isValid = jwtUtil.validateToken(token, username);
assertTrue(isValid, "Token should be valid");
}
@Test
public void testTokenValidationWithWrongUsername() {
// 测试使用错误用户名验证
boolean isValid = jwtUtil.validateToken(token, "wronguser");
assertFalse(isValid, "Token should be invalid with wrong username");
}
@Test
public void testTokenExpiration() throws InterruptedException {
// 模拟令牌过期场景
// 注意:这里需要在application.yml中设置很短的过期时间,比如1秒
Thread.sleep(2000); // 等待超过令牌过期时间
boolean isValid = jwtUtil.validateToken(token, username);
assertFalse(isValid, "Expired token should be invalid");
}
@Test
public void testTokenRefresh() {
// 测试令牌刷新
String originalToken = token;
String refreshedToken = jwtUtil.refreshToken(originalToken);
assertNotNull(refreshedToken, "Refreshed token should not be null");
assertNotEquals(originalToken, refreshedToken, "Refreshed token should be different from original");
}
@Test
public void testMultipleTokenGeneration() {
// 测试为不同用户生成令牌
String user1Token = jwtUtil.generateToken("user1");
String user2Token = jwtUtil.generateToken("user2");
assertNotEquals(user1Token, user2Token, "Tokens for different users should be unique");
}
@Test
public void testTokenClaims() {
// 测试提取令牌声明
String extractedUsername = jwtUtil.getUsernameFromToken(token);
assertNotNull(extractedUsername, "Username should be extractable from token");
assertEquals(username, extractedUsername, "Extracted username should match original");
}
}静态使用
Jwtutil
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 String SECRET = "YourSecretKeyForJwtTokenGeneration2024";
// 过期时间,1小时
private static final long EXPIRATION_TIME = 3600000;
// 生成token
public static String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// 获取签名密钥
private static Key getSigningKey() {
return Keys.hmacShaKeyFor(SECRET.getBytes());
}
// 验证token
public static boolean validateToken(String token, String username) {
try {
String tokenUsername = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
return tokenUsername.equals(username) &&
!isTokenExpired(token);
} catch (Exception e) {
return false;
}
}
// 检查token是否过期
private static boolean isTokenExpired(String token) {
Date expiration = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody()
.getExpiration();
return expiration.before(new Date());
}
// 从token获取用户名
public static String getUsernameFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}使用示例:
javaCopy// 生成token
String token = JwtUtil.generateToken("username");
// 验证token
boolean isValid = JwtUtil.validateToken(token, "username");
// 获取用户名
String username = JwtUtil.getUsernameFromToken(token);主要特点:
- 静态方法,方便直接调用
- 简单的token生成和验证
- 硬编码秘钥(小项目可以接受)
- 默认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>