当前位置: 首页 > news >正文

分布式Session会话实现方案

一、什么是分布式 Session?

分布式 Session 是一种在微服务架构中,保证用户会话(Session)数据能够在多个独立的、无状态的服务实例之间共享和保持一致的机制。以下是传统单体架构中的 Session与微服务架构中的 Session之间的对比:

  • 传统单体架构中的 Session:

    • 单体架构 Session:应用部署在一台服务器上。用户登录后,服务器会在本地内存中创建一个 Session 对象,存储用户ID、用户名等数据,并给浏览器返回一个唯一的 Session ID(通常保存在 Cookie 中)。浏览器后续的每次请求都会带上这个 Session ID,服务器根据它找到对应的 Session 数据。一切都很简单,因为数据就在本地。
  • 微服务架构中的 Session:

    • 服务是无状态的:会有很多个微服务(用户服务、订单服务、商品服务……),它们可以独立部署、扩缩容。
    • 请求是分布式的:用户的一个请求(比如“查看我的订单”)可能会先经过网关,然后被路由到订单服务的实例 A,而这个请求又需要调用用户服务的实例 B 来验证用户信息。

二、为什么需要分布式 Session?

在传统的单体应用架构中,Session 通常由应用服务器的内存(如 Tomcat 的 Session)管理。用户的多次请求都会路由到同一台服务器,可以轻松地存取 Session 数据。

然而,在微服务架构下,服务被拆分为多个独立的、可水平扩展的实例。这就带来了问题:

  1. 无状态负载均衡:用户的两次请求可能会被路由到不同的服务实例上。
  2. 内存隔离:每个服务实例都有自己的内存空间。如果 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 灵活。
  • 工作流程:
    1. 用户登录后,服务端生成一个全局唯一的 Session ID。
    2. 将 Session 数据序列化后存入 Redis(Key 是 Session ID,Value 是 Session 数据)。
    3. 将 Session ID 通过 Cookie 返回给客户端。
    4. 客户端后续请求携带此 Session ID。
    5. 任何微服务实例收到请求后,都使用这个 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 项目依赖:
<!-- 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:
spring:session:store-type: redis # 指定存储类型为 Redistimeout: 30m     # Session 过期时间,默认30分钟redis:host: localhostport: 6379# 如果有密码,在此配置 password: your-password
4.1.3 登录控制器(设置 Session):
@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. 将用户信息存入 Sessionsession.setAttribute("CURRENT_USER", user);// 也可以存其他任何需要跨请求共享的数据session.setAttribute("LOGIN_TIME", Instant.now());return ResponseEntity.ok("Login successful");}
}
4.1.4 业务控制器(读取 Session):
@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 添加依赖
<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 配置文件
# application.yml
spring:redis:host: localhostport: 6379password: database: 0timeout: 2000mslettuce:pool:max-active: 8max-wait: -1msmax-idle: 8min-idle: 0jwt:secret: mySecretKeyForJWTTokenGenerationInSpringBootApplication2024expiration: 7200000  # 2小时header: Authorizationprefix: "Bearer "
4.2.3 创建JWT工具类
// 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;// 生成 tokenpublic 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 中获取 sessionIdpublic 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());}// 验证 tokenpublic Boolean validateToken(String token, UserSession userSession) {final String username = getUsernameFromToken(token);return (username.equals(userSession.getUsername()) && !isTokenExpired(token));}// 刷新 tokenpublic 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 实体类
// 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 存储服务
// RedisSessionService.java
@Service
public class RedisSessionService {private static final String SESSION_KEY_PREFIX = "session:";private static final long SESSION_TIMEOUT = 7200L; // 2小时@Autowiredprivate RedisTemplate<String, Object> redisTemplate;// 保存 sessionpublic void saveSession(UserSession userSession) {String key = SESSION_KEY_PREFIX + userSession.getSessionId();redisTemplate.opsForValue().set(key, userSession, Duration.ofSeconds(SESSION_TIMEOUT));}// 获取 sessionpublic 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;}// 删除 sessionpublic 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 认证过滤器
// JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Autowiredprivate RedisSessionService redisSessionService;@Overrideprotected 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));// 设置到 SecurityContextSecurityContextHolder.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 配置
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;@Beanpublic 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();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}
4.2.8 Redis 配置
// RedisConfig.java
@Configuration
public class RedisConfig {@Beanpublic 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 认证控制器
// AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Autowiredprivate RedisSessionService redisSessionService;@Autowiredprivate PasswordEncoder passwordEncoder;// 模拟用户数据private Map<String, User> userDatabase = new HashMap<>();@PostConstructpublic 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("用户名或密码错误"));}// 创建 sessionUserSession userSession = new UserSession(user.getId(), user.getUsername(), user.getRoles());// 生成 tokenString token = jwtTokenUtil.generateToken(userSession);userSession.setToken(token);// 保存 session 到 RedisredisSessionService.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));}
}
http://www.dtcms.com/a/544658.html

相关文章:

  • Java创建【线程池】的方法
  • 相机直播,HDMI线怎么选择
  • 做外贸哪些国外网站可以推广上海中学地址
  • HFSS微带线仿真
  • 推荐常州微信网站建设网站友链怎么做
  • 多模态的大模型文本分类模型代码(二)——模型初步运行
  • 强化特权用户监控,守护Active Directory核心安全
  • Kafka Consumer 消费流程详解
  • 安全守护者:防爆外壳在气体传感器领域的关键应用
  • 【JavaEE初阶】网络经典面试题小小结
  • 以太网多参量传感器:构筑工业安全与环境稳定的“数据堡垒”
  • pinia-storeToRefs方法
  • 基于用户的协同过滤算法理解
  • jsp书城网站开发中国建设银行重庆网站首页
  • 郑州网站建设公司排名湖南省城乡住房建设厅网站
  • 蓝牙钥匙 第4次 蓝牙协议栈深度剖析:从物理层到应用层的完整架构解析
  • 口腔健康系统|口腔医疗|基于java和小程序的口腔健康系统小程序设计与实现(源码+数据库+文档)
  • FANUC发那科焊接机器人薄板焊接节气
  • 如何加强网站信息管理建设个人网站设计步骤
  • 调用API历史和未来气象数据获取
  • 机器人从设计到仿真到落地
  • 战略合作 | 深信科创携手北极雄芯、灵猴机器人共推国产智能机器人规模化落地
  • Rust 闭包的定义与捕获:从理论到实践的深度探索
  • 公司网站建设分录哪里的赣州网站建设
  • 各级院建设网站的通知网站建设的结论
  • 四种编程语言字符串函数及方法对比(python、Java、C#、C++)
  • 亲测好用:Chrome/Chromedriver一键下载工具(免费无广)
  • 基于Chrome140的TK账号自动化(关键词浏览)——脚本撰写(二)
  • C# SelectMany 完全指南:从入门到精通
  • 卡片式设计网站制作婚庆网站建设需求分析