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()生成安全秘钥
  • 避免使用过短或可预测的秘钥

秘钥管理建议

  1. 使用环境变量存储秘钥
  2. 定期轮换秘钥
  3. 避免将秘钥硬编码
  4. 使用安全的秘钥管理系统

测试

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

主要特点:

  1. 静态方法,方便直接调用
  2. 简单的token生成和验证
  3. 硬编码秘钥(小项目可以接受)
  4. 默认1小时过期时间
  5. 异常处理简单直接

需要在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>