一、什么是分布式 Session?
分布式 Session 是一种在微服务架构中,保证用户会话(Session)数据能够在多个独立的、无状态的服务实例之间共享和保持一致的机制。以下是传统单体架构中的 Session与微服务架构中的 Session之间的对比:
-
传统单体架构中的 Session:
- 单体架构 Session:应用部署在一台服务器上。用户登录后,服务器会在本地内存中创建一个 Session 对象,存储用户ID、用户名等数据,并给浏览器返回一个唯一的 Session ID(通常保存在 Cookie 中)。浏览器后续的每次请求都会带上这个 Session ID,服务器根据它找到对应的 Session 数据。一切都很简单,因为数据就在本地。
-
微服务架构中的 Session:
- 服务是无状态的:会有很多个微服务(用户服务、订单服务、商品服务......),它们可以独立部署、扩缩容。
- 请求是分布式的:用户的一个请求(比如"查看我的订单")可能会先经过网关,然后被路由到订单服务的实例 A,而这个请求又需要调用用户服务的实例 B 来验证用户信息。
二、为什么需要分布式 Session?
在传统的单体应用架构中,Session 通常由应用服务器的内存(如 Tomcat 的 Session)管理。用户的多次请求都会路由到同一台服务器,可以轻松地存取 Session 数据。
然而,在微服务架构下,服务被拆分为多个独立的、可水平扩展的实例。这就带来了问题:
- 无状态负载均衡:用户的两次请求可能会被路由到不同的服务实例上。
- 内存隔离:每个服务实例都有自己的内存空间。如果 Session 存储在实例 A 的内存中,那么实例 B 无法读取到该 Session。
因此,就需要一种机制,让所有服务实例都能访问到同一个 Session,这就是分布式 Session。
三、主流分布式 Session 实现方案
3.1 Session同步
- 原理:当一个服务实例的 Session 发生变化时,它会将这个 Session 数据同步到集群中的所有其他服务实例。
- 优点:
- 任何实例都拥有全量的 Session 数据,请求可以路由到任意实例。
- 缺点:
- 网络开销巨大:随着服务实例数量的增加,同步产生的网络流量会呈指数级增长,严重消耗带宽和性能。
- 内存消耗高:每个实例都存储全量的 Session 数据,浪费内存。
- 耦合性高:实例间存在强耦合,不利于扩展。
3.2 Session粘滞
- 原理:在负载均衡器(如 Nginx、网关)上配置规则,将同一个用户的所有请求都固定地路由到同一个后端服务实例。这样,这个用户的 Session 就始终只存在于那个实例的内存中,就像回到了单体架构。
- 优点:
- 实现简单,无需修改业务代码。
- 避免了 Session 的跨实例共享问题。
- 缺点:
- 缺乏容错性:如果某个实例宕机,那么路由到该实例的所有用户 Session 都将丢失。
- 不利于水平扩展:在扩缩容(增加或减少实例)时,哈希结果会变化,导致大量用户的 Session 失效,需要重新登录。
- 负载可能不均衡:无法根据实例的实时负载进行动态调整。
3.3 服务端集中存储
- 原理:这是最常用、最经典的方案。将 Session 数据从服务本地内存中剥离出来,集中存储在一个外部化的、高可用的数据存储中心。所有微服务实例都从这个中心读写 Session 数据。
- 常用存储介质:
- Redis(首选):基于内存,性能极高;支持数据持久化;提供丰富的数据结构和过期机制。
- Memcached:同样是高性能内存缓存,但数据结构较简单。
- 数据库(如 MySQL):不推荐用于生产环境,因为性能瓶颈明显,但易于实现和理解。
- MongoDB:文档型数据库,Schema 灵活。
- 工作流程:
- 用户登录后,服务端生成一个全局唯一的 Session ID。
- 将 Session 数据序列化后存入 Redis(Key 是 Session ID,Value 是 Session 数据)。
- 将 Session ID 通过 Cookie 返回给客户端。
- 客户端后续请求携带此 Session ID。
- 任何微服务实例收到请求后,都使用这个 Session ID 去 Redis 中查询完整的 Session 数据。
- 优点:
- 真正实现了服务的无状态化,扩展性强。
- 数据持久化,即使所有服务重启,Session 也不会丢失(取决于 Redis 配置)。
- 高性能,Redis 的读写速度极快。
- 缺点:
- 引入了外部依赖,架构变复杂,需要保证 Redis 集群的高可用。
- 多了一次网络 IO,有微小的延迟。
3.4 基于 Token(现代趋势)
- 原理:完全废除服务器端的 Session 存储。用户登录后,服务器根据用户信息生成一个签名的 Token(如 JWT - JSON Web Token),然后将这个 Token 返回给客户端。客户端在后续请求中(通常在 HTTP Header 的 Authorization 字段)携带此 Token。服务器只需验证 Token 的签名有效性即可信任其中的用户信息。
- 优点:
- 完全无状态:服务端不需要存储任何会话数据,扩展性达到极致。
- 适合跨域:非常适合现代的前后端分离、跨域访问以及单点登录(SSO)场景。
- 灵活性高:Token 可以被任何拥有秘钥的服务验证,不局限于生成它的服务。
- 缺点:
- Token 一旦签发,在有效期内无法主动使其失效(除非使用额外的黑名单机制,但这又引入了状态)。
- Token 包含信息较多:由于用户信息都在 Token 里,它通常比一个 Session ID 大,会增加每次请求的带宽开销。
- 安全性考虑:需要妥善保管签名秘钥,并防范 XSS 和 CSRF 攻击。
四、典型实现案例
4.1 基于 Spring Boot + Redis
4.1.1 项目依赖:
xml
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Session 使用 Redis 作为数据源 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
4.1.2 配置文件 application.yml:
yaml
spring:
session:
store-type: redis # 指定存储类型为 Redis
timeout: 30m # Session 过期时间,默认30分钟
redis:
host: localhost
port: 6379
# 如果有密码,在此配置 password: your-password
4.1.3 登录控制器(设置 Session):
java
@RestController
@RequestMapping("/auth")
public class AuthController {
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginRequest request, HttpServletRequest httpRequest) {
// 1. 验证用户名密码 (伪代码)
User user = userService.authenticate(request.getUsername(), request.getPassword());
if (user == null) {
return ResponseEntity.badRequest().body("Login failed");
}
// 2. 获取当前 HttpSession,Spring Session 会自动创建或获取
HttpSession session = httpRequest.getSession(true); // true 表示不存在则创建
// 3. 将用户信息存入 Session
session.setAttribute("CURRENT_USER", user);
// 也可以存其他任何需要跨请求共享的数据
session.setAttribute("LOGIN_TIME", Instant.now());
return ResponseEntity.ok("Login successful");
}
}
4.1.4 业务控制器(读取 Session):
java
@RestController
@RequestMapping("/api")
public class BusinessController {
@GetMapping("/profile")
public ResponseEntity<User> getProfile(HttpServletRequest httpRequest) {
// 1. 获取当前 Session (如果不存在,getSession(false) 返回 null)
HttpSession session = httpRequest.getSession(false);
if (session != null) {
// 2. 从 Session 中获取用户信息
User currentUser = (User) session.getAttribute("CURRENT_USER");
if (currentUser != null) {
// 3. 执行业务逻辑...
return ResponseEntity.ok(currentUser);
}
}
// 4. 未登录或 Session 过期
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
@PostMapping("/logout")
public ResponseEntity<String> logout(HttpServletRequest request) {
// 使当前 Session 失效,Spring Session 会自动从 Redis 中删除它
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return ResponseEntity.ok("Logged out");
}
}
4.2 基于 Spring Boot + JWT
4.2.1 添加依赖
xml
<dependencies>
<!-- Spring Boot Web -->
<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-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>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
4.2.2 配置文件
yaml
# application.yml
spring:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
jwt:
secret: mySecretKeyForJWTTokenGenerationInSpringBootApplication2024
expiration: 7200000 # 2小时
header: Authorization
prefix: "Bearer "
4.2.3 创建JWT工具类
java
// JwtTokenUtil.java
@Component
public class JwtTokenUtil implements Serializable {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.prefix}")
private String tokenPrefix;
// 生成 token
public String generateToken(UserSession userSession) {
Map<String, Object> claims = new HashMap<>();
claims.put("sessionId", userSession.getSessionId());
claims.put("userId", userSession.getUserId());
claims.put("username", userSession.getUsername());
claims.put("roles", userSession.getRoles());
return Jwts.builder()
.setClaims(claims)
.setSubject(userSession.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 从 token 中获取用户名
public String getUsernameFromToken(String token) {
return getAllClaimsFromToken(token).getSubject();
}
// 从 token 中获取 sessionId
public String getSessionIdFromToken(String token) {
return getAllClaimsFromToken(token).get("sessionId", String.class);
}
// 获取过期时间
public Date getExpirationDateFromToken(String token) {
return getAllClaimsFromToken(token).getExpiration();
}
// 获取所有声明
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
// 验证 token 是否过期
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// 验证 token
public Boolean validateToken(String token, UserSession userSession) {
final String username = getUsernameFromToken(token);
return (username.equals(userSession.getUsername()) && !isTokenExpired(token));
}
// 刷新 token
public String refreshToken(String token) {
Claims claims = getAllClaimsFromToken(token);
claims.setIssuedAt(new Date());
claims.setExpiration(new Date(System.currentTimeMillis() + expiration));
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 提取 token(去掉前缀)
public String extractToken(String header) {
if (header != null && header.startsWith(tokenPrefix)) {
return header.substring(tokenPrefix.length());
}
return null;
}
}
4.2.4 Session 实体类
java
// UserSession.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserSession implements Serializable {
private String sessionId;
private Long userId;
private String username;
private List<String> roles;
private Date loginTime;
private Date lastAccessTime;
private String token;
public UserSession(Long userId, String username, List<String> roles) {
this.sessionId = UUID.randomUUID().toString();
this.userId = userId;
this.username = username;
this.roles = roles;
this.loginTime = new Date();
this.lastAccessTime = new Date();
}
}
4.2.5 Redis Session 存储服务
java
// RedisSessionService.java
@Service
public class RedisSessionService {
private static final String SESSION_KEY_PREFIX = "session:";
private static final long SESSION_TIMEOUT = 7200L; // 2小时
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 保存 session
public void saveSession(UserSession userSession) {
String key = SESSION_KEY_PREFIX + userSession.getSessionId();
redisTemplate.opsForValue().set(key, userSession, Duration.ofSeconds(SESSION_TIMEOUT));
}
// 获取 session
public UserSession getSession(String sessionId) {
String key = SESSION_KEY_PREFIX + sessionId;
UserSession session = (UserSession) redisTemplate.opsForValue().get(key);
if (session != null) {
// 更新最后访问时间并延长过期时间
session.setLastAccessTime(new Date());
saveSession(session);
}
return session;
}
// 删除 session
public void deleteSession(String sessionId) {
String key = SESSION_KEY_PREFIX + sessionId;
redisTemplate.delete(key);
}
// 检查 session 是否存在
public boolean sessionExists(String sessionId) {
String key = SESSION_KEY_PREFIX + sessionId;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
// 延长 session 过期时间
public void extendSession(String sessionId) {
String key = SESSION_KEY_PREFIX + sessionId;
redisTemplate.expire(key, Duration.ofSeconds(SESSION_TIMEOUT));
}
}
4.2.6 JWT 认证过滤器
java
// JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private RedisSessionService redisSessionService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null) {
try {
String sessionId = jwtTokenUtil.getSessionIdFromToken(token);
UserSession userSession = redisSessionService.getSession(sessionId);
if (userSession != null && jwtTokenUtil.validateToken(token, userSession)) {
// 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userSession,
null,
getAuthorities(userSession.getRoles())
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置到 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
// 更新 session 最后访问时间
redisSessionService.extendSession(sessionId);
}
} catch (Exception e) {
logger.warn("JWT token 验证失败: " + e.getMessage());
}
}
chain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
private Collection<? extends GrantedAuthority> getAuthorities(List<String> roles) {
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
4.2.7 Security 配置
java
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.2.8 Redis 配置
java
// RedisConfig.java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
4.2.9 认证控制器
java
// AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private RedisSessionService redisSessionService;
@Autowired
private PasswordEncoder passwordEncoder;
// 模拟用户数据
private Map<String, User> userDatabase = new HashMap<>();
@PostConstruct
public void init() {
// 初始化测试用户
userDatabase.put("admin", new User(1L, "admin", passwordEncoder.encode("admin123"),
Arrays.asList("ROLE_ADMIN", "ROLE_USER")));
userDatabase.put("user", new User(2L, "user", passwordEncoder.encode("user123"),
Arrays.asList("ROLE_USER")));
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
User user = userDatabase.get(loginRequest.getUsername());
if (user == null || !passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error("用户名或密码错误"));
}
// 创建 session
UserSession userSession = new UserSession(
user.getId(),
user.getUsername(),
user.getRoles()
);
// 生成 token
String token = jwtTokenUtil.generateToken(userSession);
userSession.setToken(token);
// 保存 session 到 Redis
redisSessionService.saveSession(userSession);
LoginResponse response = new LoginResponse(
token,
"Bearer",
userSession.getSessionId(),
new UserInfo(user.getId(), user.getUsername(), user.getRoles())
);
return ResponseEntity.ok(ApiResponse.success("登录成功", response));
}
@PostMapping("/logout")
public ResponseEntity<?> logout(HttpServletRequest request) {
String token = jwtTokenUtil.extractToken(request.getHeader("Authorization"));
if (token != null) {
try {
String sessionId = jwtTokenUtil.getSessionIdFromToken(token);
redisSessionService.deleteSession(sessionId);
} catch (Exception e) {
// token 无效,忽略
}
}
SecurityContextHolder.clearContext();
return ResponseEntity.ok(ApiResponse.success("登出成功"));
}
@PostMapping("/refresh-token")
public ResponseEntity<?> refreshToken(HttpServletRequest request) {
String token = jwtTokenUtil.extractToken(request.getHeader("Authorization"));
if (token == null) {
return ResponseEntity.badRequest().body(ApiResponse.error("Token 不存在"));
}
try {
String sessionId = jwtTokenUtil.getSessionIdFromToken(token);
UserSession userSession = redisSessionService.getSession(sessionId);
if (userSession == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error("Session 已过期"));
}
String newToken = jwtTokenUtil.refreshToken(token);
userSession.setToken(newToken);
redisSessionService.saveSession(userSession);
return ResponseEntity.ok(ApiResponse.success("Token 刷新成功",
new RefreshTokenResponse(newToken)));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error("Token 刷新失败"));
}
}
@GetMapping("/me")
public ResponseEntity<?> getCurrentUser(@AuthenticationPrincipal UserSession userSession) {
if (userSession == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error("未登录"));
}
UserInfo userInfo = new UserInfo(
userSession.getUserId(),
userSession.getUsername(),
userSession.getRoles()
);
return ResponseEntity.ok(ApiResponse.success("获取成功", userInfo));
}
}