Serverless 安全增强篇:整合 OAuth2 + Token 黑名单 + Redis 缓存机制
随着 Serverless 架构的广泛应用,传统 Web 应用中依赖 Session 的认证模式面临重大挑战。本文将以 Spring Security 为核心,构建一套无状态、可扩展、支持 OAuth2、JWT、Redis 黑名单与 Refresh Token 的安全认证体系,适用于 Serverless 应用场景。
Serverless 应用的安全挑战
Serverless 应用的无状态特性决定了其认证模型不能依赖传统的会话管理。核心挑战包括:
- 身份认证用户身份需要跨请求验证,无 Session。
- 权限校验如何高效识别用户角色与权限。
- Token 生命周期管理访问令牌的过期续签、注销。
- 密钥管理JWT 签名密钥的安全管理。
Spring Security + JWT 构建轻量认证模型
我们采用如下组件构建无状态认证体系:
组件 | 作用 |
JWT | 用户身份令牌,无状态传递 |
OAuth2 | 多客户端支持与统一授权 |
Redis | 黑名单 + RefreshToken 存储 |
Spring Security | 安全拦截器与权限控制 |
系统结构设计图
+-----------------------------+
| 前端调用接口 |
+-----------------------------+
|
v
+-----------------------------+
| API网关 / Serverless函数 |
+-----------------------------+
|
v
+-----------------------------+
| Spring Security + Token拦截 |
+-----------------------------+
|
v
+------------+ Redis +-------------+
| JWT Token校验 | <----> | Token黑名单 |
+------------+ +-------------+
|
v
+--------------------------+
| 用户业务逻辑处理 |
+--------------------------+
关键模块代码实现
Spring Boot 启动类
@SpringBootApplication
public class ServerlessSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(ServerlessSecurityApplication.class, args);
}
}
Security 配置类(无状态、JWT、资源服务器)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**", "/api/token/refresh").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}
}
JWT 工具类
public class JwtUtils {
private static final Key key = Keys.hmacShaKeyFor("0123456789abcdef0123456789abcdef".getBytes());
public static String generateAccessToken(String username, String roles, String jti) {
return Jwts.builder()
.setSubject(username)
.setId(jti)
.claim("roles", roles)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600_000)) // 1小时
.signWith(key)
.compact();
}
public static String generateRefreshToken(String username, String jti) {
return Jwts.builder()
.setSubject(username)
.setId(jti)
.claim("type", "refresh")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 3600_000)) // 7天
.signWith(key)
.compact();
}
public static Claims getClaims(String token) throws JwtException {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}
Redis Token 黑名单工具类
@Component
public class TokenBlacklistUtil {
private final StringRedisTemplate redisTemplate;
public TokenBlacklistUtil(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void blacklistToken(String jti, long ttlSeconds) {
redisTemplate.opsForValue().set("blacklist:" + jti, "1", ttlSeconds, TimeUnit.SECONDS);
}
public boolean isTokenBlacklisted(String jti) {
return redisTemplate.hasKey("blacklist:" + jti);
}
}
RefreshToken 存储与刷新接口
@RestController
@RequestMapping("/api/token")
public class TokenController {
@Autowired
private StringRedisTemplate redisTemplate;
@PostMapping("/refresh")
public ResponseEntity> refresh(@RequestParam String refreshToken) {
Claims claims = JwtUtils.getClaims(refreshToken);
String jti = claims.getId();
String username = claims.getSubject();
// 校验类型与黑名单
if (!"refresh".equals(claims.get("type"))) {
return ResponseEntity.badRequest().body("非法 token 类型");
}
if (!Boolean.TRUE.equals(redisTemplate.hasKey("refresh:" + jti))) {
return ResponseEntity.status(401).body("refreshToken 已失效");
}
// 生成新 token
String newJti = UUID.randomUUID().toString();
String newAccessToken = JwtUtils.generateAccessToken(username, "USER", newJti);
return ResponseEntity.ok(Map.of("accessToken", newAccessToken));
}
}
登录、退出控制器(模拟)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private TokenBlacklistUtil blacklistUtil;
@PostMapping("/login")
public Map login(@RequestParam String username) {
String jti = UUID.randomUUID().toString();
String accessToken = JwtUtils.generateAccessToken(username, "USER", jti);
String refreshToken = JwtUtils.generateRefreshToken(username, jti);
// 保存 refreshToken 到 redis
redisTemplate.opsForValue().set("refresh:" + jti, username, 7, TimeUnit.DAYS);
return Map.of("accessToken", accessToken, "refreshToken", refreshToken);
}
@PostMapping("/logout")
public ResponseEntity logout(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.replace("Bearer ", "");
Claims claims = JwtUtils.getClaims(token);
String jti = claims.getId();
long remaining = (claims.getExpiration().getTime() - System.currentTimeMillis()) / 1000;
blacklistUtil.blacklistToken(jti, remaining);
// 同时清除 refreshToken
redisTemplate.delete("refresh:" + jti);
return ResponseEntity.ok().build();
}
}
application.yml 配置
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.icoderoad.com/oauth2
jwk-set-uri: https://auth.icoderoad.com/oauth2/jwks
redis:
host: localhost
port: 6379
总结
无状态 Serverless 环境下,Spring Security 可通过 JWT、OAuth2 与 Redis 轻松实现高效认证体系:
- 不依赖 Session
- 支持访问控制 + 黑名单管理
- Refresh Token 保证登录体验
- Redis 做 Token 生命周期缓存
今天就讲到这里,如果有问题需要咨询,大家可以直接留言或扫下方二维码来知识星球找我,我们会尽力为你解答。
本文地址:https://www.yitenyun.com/178.html
上一篇:MySQL同步ES的六种方案!
下一篇:深入理解MySQL binlog