目 录CONTENT

文章目录

使用 Spring Security 实现 JWT 和令牌刷新

在等晚風吹
2025-01-09 / 0 评论 / 0 点赞 / 5 阅读 / 0 字 / 正在检测是否收录...

使用 Spring Security 实现 JWT 和令牌刷新

本指南扩展了 Spring Security 设置,通过添加刷新令牌机制,使用户无需重新认证即可续订访问令牌。我们将把刷新令牌存储在 Redis 中,以实现安全的会话管理,并支持基于数据库的用户认证。


第一步:理解 JWT 和令牌刷新

为什么需要令牌刷新?

  • 访问令牌:生命周期较短,以减少被盗风险。
  • 刷新令牌:生命周期较长,允许客户端在无需用户重新登录的情况下获取新的访问令牌。

第二步:项目设置

pom.xml 中添加以下依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<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>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

第三步:用户实体

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private String roles;
    private String refreshToken; // Optional: Track refresh token
    // Getters and Setters
}

第四步:JWT 工具类

更新 JwtUtil 类以分别处理访问令牌和刷新令牌。

import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;
import java.util.Date;

@Component
public class JwtUtil {
    private static final String SECRET_KEY = "accessSecret";
    private static final String REFRESH_SECRET_KEY = "refreshSecret";
    private static final long ACCESS_EXPIRATION = 1000 * 60 * 15; // 15 minutes
    private static final long REFRESH_EXPIRATION = 1000 * 60 * 60 * 24; // 1 day

    public String generateAccessToken(String username) {
        return Jwts.builder()
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXPIRATION))
            .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
            .compact();
    }

    public String generateRefreshToken(String username) {
        return Jwts.builder()
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + REFRESH_EXPIRATION))
            .signWith(SignatureAlgorithm.HS256, REFRESH_SECRET_KEY)
            .compact();
    }

    public String extractUsername(String token, boolean isRefreshToken) {
        String secretKey = isRefreshToken ? REFRESH_SECRET_KEY : SECRET_KEY;
        return Jwts.parser()
            .setSigningKey(secretKey)
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }

    public boolean validateToken(String token, boolean isRefreshToken) {
        try {
            String secretKey = isRefreshToken ? REFRESH_SECRET_KEY : SECRET_KEY;
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

第五步:Redis 集成用于刷新令牌

配置 Redis

application.properties 中添加以下配置:

spring.redis.host=localhost
spring.redis.port=6379

第六步:认证控制器

实现登录和刷新令牌的 API:

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

@RestController
@RequestMapping("/auth")
public class AuthController {
    private final CustomUserDetailsService userDetailsService;
    private final JwtUtil jwtUtil;
    private final RefreshTokenService refreshTokenService;

    public AuthController(CustomUserDetailsService userDetailsService, JwtUtil jwtUtil, RefreshTokenService refreshTokenService) {
        this.userDetailsService = userDetailsService;
        this.jwtUtil = jwtUtil;
        this.refreshTokenService = refreshTokenService;
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody AuthRequest authRequest) {
        // Authenticate user
        UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
        if (!userDetails.getPassword().equals(authRequest.getPassword())) {
            return ResponseEntity.status(401).body("Invalid credentials");
        }
        // Generate tokens
        String accessToken = jwtUtil.generateAccessToken(userDetails.getUsername());
        String refreshToken = jwtUtil.generateRefreshToken(userDetails.getUsername());
        refreshTokenService.storeRefreshToken(userDetails.getUsername(), refreshToken);
        return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken));
    }

    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@RequestBody RefreshRequest refreshRequest) {
        String username = jwtUtil.extractUsername(refreshRequest.getRefreshToken(), true);
        if (!jwtUtil.validateToken(refreshRequest.getRefreshToken(), true)) {
            return ResponseEntity.status(403).body("Invalid refresh token");
        }
        String cachedToken = refreshTokenService.getRefreshToken(username);
        if (cachedToken == null || !cachedToken.equals(refreshRequest.getRefreshToken())) {
            return ResponseEntity.status(403).body("Invalid or expired refresh token");
        }
        String newAccessToken = jwtUtil.generateAccessToken(username);
        return ResponseEntity.ok(new AuthResponse(newAccessToken, refreshRequest.getRefreshToken()));
    }
}

DTO 类

public class AuthRequest {
    private String username;
    private String password;
    // Getters and Setters
}

public class AuthResponse {
    private String accessToken;
    private String refreshToken;

    public AuthResponse(String accessToken, String refreshToken) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
    }
    // Getters
}

public class RefreshRequest {
    private String refreshToken;
    // Getters and Setters
}

第七步:安全配置

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
        http.csrf().disable()
            .authorizeHttpRequests(auth -> auth
                .antMatchers("/auth/**").permitAll()
                .anyRequest().authenticated())
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

第八步:测试应用程序

测试流程

  1. 登录端点 (/auth/login)

    • 发送用户名和密码。
    • 接收 accessTokenrefreshToken
  2. 访问安全端点

    • 使用 accessToken 作为 Authorization 头。
  3. 刷新令牌 (/auth/refresh)

    • 发送 refreshToken 以续订 accessToken

结论

本指南使用 Spring Security、JWT、Redis 和数据库实现了一个高效的认证系统,具备以下特点:

  • 访问令牌:短期有效,用于保护 API。
  • 刷新令牌:长期有效,存储于 Redis 中,用于续订访问令牌。
  • Redis:提供快速查找和自动过期的会话管理。
  • Spring Security:灵活的框架,可与 JWT 和自定义过滤器集成。

该架构适合需要高安全性和性能的现代 Web 应用程序,并支持进一步扩展功能(如 RBAC、多因素认证等)。

0

评论区