Serverless 安全增強(qiáng)篇:整合 OAuth2 + Token 黑名單 + Redis 緩存機(jī)制
隨著 Serverless 架構(gòu)的廣泛應(yīng)用,傳統(tǒng) Web 應(yīng)用中依賴 Session 的認(rèn)證模式面臨重大挑戰(zhàn)。本文將以 Spring Security 為核心,構(gòu)建一套無狀態(tài)、可擴(kuò)展、支持 OAuth2、JWT、Redis 黑名單與 Refresh Token 的安全認(rèn)證體系,適用于 Serverless 應(yīng)用場景。
Serverless 應(yīng)用的安全挑戰(zhàn)
Serverless 應(yīng)用的無狀態(tài)特性決定了其認(rèn)證模型不能依賴傳統(tǒng)的會話管理。核心挑戰(zhàn)包括:
- 身份認(rèn)證用戶身份需要跨請求驗證,無 Session。
- 權(quán)限校驗如何高效識別用戶角色與權(quán)限。
- Token 生命周期管理訪問令牌的過期續(xù)簽、注銷。
- 密鑰管理JWT 簽名密鑰的安全管理。
Spring Security + JWT 構(gòu)建輕量認(rèn)證模型
我們采用如下組件構(gòu)建無狀態(tài)認(rèn)證體系:
組件 | 作用 |
JWT | 用戶身份令牌,無狀態(tài)傳遞 |
OAuth2 | 多客戶端支持與統(tǒng)一授權(quán) |
Redis | 黑名單 + RefreshToken 存儲 |
Spring Security | 安全攔截器與權(quán)限控制 |
系統(tǒng)結(jié)構(gòu)設(shè)計圖
+-----------------------------+
| 前端調(diào)用接口 |
+-----------------------------+
|
v
+-----------------------------+
| API網(wǎng)關(guān) / Serverless函數(shù) |
+-----------------------------+
|
v
+-----------------------------+
| Spring Security + Token攔截 |
+-----------------------------+
|
v
+------------+ Redis +-------------+
| JWT Token校驗 | <----> | Token黑名單 |
+------------+ +-------------+
|
v
+--------------------------+
| 用戶業(yè)務(wù)邏輯處理 |
+--------------------------+
關(guān)鍵模塊代碼實現(xiàn)
Spring Boot 啟動類
@SpringBootApplication
public class ServerlessSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(ServerlessSecurityApplication.class, args);
}
}
Security 配置類(無狀態(tài)、JWT、資源服務(wù)器)
@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<String, String> 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<Void> 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
總結(jié)
無狀態(tài) Serverless 環(huán)境下,Spring Security 可通過 JWT、OAuth2 與 Redis 輕松實現(xiàn)高效認(rèn)證體系:
- 不依賴 Session
- 支持訪問控制 + 黑名單管理
- Refresh Token 保證登錄體驗
- Redis 做 Token 生命周期緩存
今天就講到這里,如果有問題需要咨詢,大家可以直接留言或掃下方二維碼來知識星球找我,我們會盡力為你解答。