一、分布式會話的背景
在微服務架構或集群部署環境下,請求可能落在不同的服務器節點,無法再依賴本地內存來維護用戶 Session。因此,需要一種跨節點共享 Session 的機制,這就是 分布式會話管理的核心目標。
二、分布式會話的演進歷程
1. 單節點會話(原始方案)
- 會話存儲在內存中,如 Tomcat 的 HttpSession。
- 缺點:不可擴展、節點故障丟失會話。
2. Session Sticky(會話綁定)
-
做法:使用負載均衡策略(如 Nginx 的
ip_hash
)將同一用戶綁定到固定節點。 -
優點:不用共享 Session。
-
缺點:
- 容錯差:節點掛了,Session 丟失。
- 擴展性差:負載不均衡。
3. Session 復制(共享內存同步)
-
如 Tomcat 集群通過
DeltaManager
或BackupManager
實現 Session 同步。 -
缺點:
- 性能開銷大(尤其寫操作多時)。
- 網絡帶寬壓力高。
4. 集中式 Session 存儲(核心階段)
-
統一存儲于 Redis、Memcached、數據庫等。
-
優點:
- 節點無狀態,擴容方便。
- 容錯好,支持持久化。
-
常見技術棧:
- Spring Session + Redis
- Shiro + Redis
- 自定義 Filter 攔截 + Redis
-
缺點:
- 讀寫 Redis 有延遲,需優化緩存與連接池。
5. Token 模式(Stateless Session)
-
不再使用服務器記錄狀態。會話狀態由客戶端持有,常見如 JWT(JSON Web Token)。
-
特點:
- 完全無狀態,易于擴展。
- 鑒權速度快,無需訪問服務器。
-
缺點:
- JWT 不支持撤銷。
- Payload 泄漏風險需加密或簽名。
- 會話失效處理復雜(需結合 Redis 存 token 黑名單等)。
6. 混合模式(最佳實踐)
-
使用 JWT 攜帶身份信息 + Redis 存儲服務端狀態(權限、Session 信息等)。
-
優點兼得:
- 前端無狀態便于認證傳輸。
- 服務端掌握可控狀態,支持注銷、權限變更等。
三、關鍵技術對比
模式 | 狀態位置 | 擴展性 | 容錯性 | 性能 | 安全性 | 實時性 |
---|---|---|---|---|---|---|
Sticky Session | 服務端 | 差 | 差 | 好 | 中 | 高 |
Session 復制 | 多服務端 | 差 | 中 | 差 | 中 | 高 |
Redis集中存儲 | Redis | 好 | 好 | 中 | 好 | 中 |
JWT | 客戶端 | 最好 | 最好 | 高 | 中(需加密) | 高 |
混合方案 | 客戶端+Redis | 最佳 | 最佳 | 中上 | 最佳 | 高 |
四、最佳事件(Best Practices)
? 實際生產中推薦:
1. Spring Boot 項目:使用 Spring Session + Redis
- 簡單集成,Spring 自動替換原生 Session。
- 可配置 session TTL,支持自動刷新、分布式環境可控。
2. 高并發微服務:使用 JWT + Redis 雙模式
- JWT 保證 stateless 高性能登錄認證。
- Redis 存儲 session 黑名單、權限信息,便于集中管理。
3. 安全性要求高:JWT 簽名 + 加密 + Redis 控制權限變更
- 對 token 加簽、加密(如 RSA)防篡改、防泄露。
- Redis 控制 Token 生命周期、用戶禁用、權限升級等事件。
4. 高可用部署:Redis 使用 Sentinel / Cluster + 本地緩存
- Redis 配合本地 Caffeine/Guava 緩存,減輕訪問壓力。
- Redis 節點使用 Sentinel 做主從切換,確保高可用。
5. 重要業務審計:使用 Session ID 記錄登錄軌跡、權限行為
- 配合 Kafka/Logstash 進行操作軌跡分析。
五、總結
分布式會話的演進反映了分布式系統對性能、可用性、安全性和擴展性的不斷追求。最佳實踐通常采用混合模式,兼顧無狀態特性與業務控制能力:
推薦組合:JWT(身份標識)+ Redis(狀態控制)+ 本地緩存(性能優化)+ Spring Security/Spring Session 集成
六、分布式會話最佳實踐之代碼實現
Spring Boot + Spring Security + JWT + Redis 的完整分布式會話控制實現
🧱 一、整體架構圖
[前端] ? [Spring Boot 接口層 (JWT Auth Filter)] ? [JWT 驗簽 + Redis 校驗] ? [業務接口]
📦 二、Maven 依賴
<!-- Spring Boot -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><!-- Spring Security -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency><!-- JWT -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency><!-- Redis -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
🔐 三、JWT 工具類
@Component
public class JwtUtil {private final String secretKey = "mySecretKey"; // 建議使用 RSA 非對稱密鑰private final long expiration = 60 * 60 * 1000; // 1小時public String generateToken(String username) {return Jwts.builder().setSubject(username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + expiration)).signWith(SignatureAlgorithm.HS512, secretKey).compact();}public String getUsername(String token) {return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();}public boolean isTokenValid(String token) {try {Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();return claims.getExpiration().after(new Date());} catch (Exception e) {return false;}}
}
🔁 四、登錄接口
@RestController
@RequestMapping("/auth")
public class AuthController {@Autowired private JwtUtil jwtUtil;@Autowired private RedissonClient redissonClient;@PostMapping("/login")public ResponseEntity<?> login(@RequestBody Map<String, String> login) {String username = login.get("username");String password = login.get("password");//這里要寫業務代碼查詢用戶進行數據校驗 //獲取userId if ("admin".equals(username) && "123456".equals(password)) {String userId= "用戶ID";String token = jwtUtil.generateToken(username);RBucket<String> bucket = redissonClient.getBucket("TOKEN:" + userId);bucket.set(token, 30, TimeUnit.MINUTES);return ResponseEntity.ok(Map.of("token", token));}return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");}@PostMapping("/logout")public ResponseEntity<?> logout(HttpServletRequest request) {String token = request.getHeader("Authorization");if (token != null && token.startsWith("Bearer ")) {String jwt = token.substring(7);String userId= jwtUtil.getUsername(jwt);redissonClient.getBucket("TOKEN:" + userId).delete();}return ResponseEntity.ok("Logged out");}
}
🛡? 五、JWT 過濾器(攔截器)
@Component
public class JwtAuthFilter extends OncePerRequestFilter {@Autowired private JwtUtil jwtUtil;@Autowired private RedissonClient redissonClient;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException {String authHeader = request.getHeader("Authorization");if (authHeader != null && authHeader.startsWith("Bearer ")) {String token = authHeader.substring(7);if (jwtUtil.isTokenValid(token)) {String userId = jwtUtil.getUsername(token); RBucket<String> bucket = redissonClient.getBucket("TOKEN:" + userId);String redisToken= bucket.get();boolean exists = bucket.isExists();if (exists && token.equals(redisToken)) {UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(username, null, List.of());SecurityContextHolder.getContext().setAuthentication(authentication);}}}chain.doFilter(request, response);}
}
?? 六、Spring Security 配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {@Autowired private JwtAuthFilter jwtAuthFilter;@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/auth/**").permitAll().anyRequest().authenticated().and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}
}
🧪 七、測試方式
-
登錄
- POST
/auth/login
,body:{ "username": "admin", "password": "123456" }
- 返回
token
- POST
-
請求業務接口
- GET
/some/api
,在 Header 中加Authorization: Bearer <token>
- GET
-
退出登錄
- POST
/auth/logout
,將當前 token 放入 Header
- POST
🧠 八、擴展建議(進階后續會不斷填坑)
功能需求 | 建議方案 |
---|---|
多端登錄互踢 | Redis 中存儲設備ID、時間戳,舊設備 token 作廢 |
Token 黑名單機制 | Redis 設置黑名單,配合 JWT ID(jti) 做驗證 |
細粒度權限控制 | 搭配 Spring Security 的 @PreAuthorize 、@Secured 注解使用 |
Token 刷新機制 | 定期發起 refresh token 請求,更新主 token |
Redis 持久化或 Cluster | 開啟持久化,使用 Sentinel/Cluster 高可用 |
收工