🔐 分布式Session共享:让用户登录态"漫游"各个服务器!

51 阅读8分钟

副标题:Redis、JWT、Cookie Domain,谁是最佳选择?🎯


🎬 开场:Session的烦恼

单机时代 vs 分布式时代

单机时代(很简单):

用户登录 → Session存在服务器内存
         → 后续请求都到这台服务器
         → Session一直有效 ✅

┌──────┐
│ 用户 │
└───┬──┘
    │
    ↓
┌───────────┐
│  服务器   │
│  Session  │
└───────────┘


分布式时代(问题来了):

用户登录 → Session存在服务器A
         → 下次请求到服务器B
         → 服务器B:谁啊?没登录!❌

┌──────┐
│ 用户 │
└───┬──┘
    │
    ├──→ 第1次请求 → 服务器A (Session存在)
    │
    └──→ 第2次请求 → 服务器B (Session不存在) ❌

真实故障案例

某电商网站的惨痛经历:

10:00  用户登录成功
       Session存在服务器A
       
10:01  用户点击"加入购物车"
       负载均衡到服务器B
       提示:请先登录!❌
       
10:02  用户再次登录
       负载均衡又到服务器A
       登录成功
       
10:03  用户点击"结算"
       负载均衡又到服务器B
       又提示:请先登录!❌
       
10:05  用户崩溃,放弃购物...

损失:订单丢失,用户流失

📚 核心概念

什么是Session?

Session(会话):
服务器端存储的用户状态信息

典型内容:
├── 用户ID
├── 用户名
├── 登录时间
├── 权限信息
├── 购物车数据
└── 其他临时数据

工作流程:
1. 用户登录 → 创建Session → 生成SessionId
2. 返回SessionId给客户端(通常存在Cookie中)
3. 后续请求携带SessionId
4. 服务器根据SessionId找到Session数据

为什么需要Session共享?

问题:
多台服务器,Session数据分散

需求:
无论访问哪台服务器,都能识别用户

目标:
Session数据在所有服务器间共享

🏗️ 解决方案对比

方案复杂度性能推荐度适用场景
Session粘性⭐⭐⭐⭐⭐⭐小规模应用
Session复制⭐⭐⭐⭐极小规模
Redis集中存储⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐大部分场景
JWT无状态⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐微服务

🔧 方案1:Session粘性(Sticky Session)

原理

负载均衡器:记住用户和服务器的映射关系

用户A → 第1次 → 服务器1
      → 第2次 → 服务器1 (粘性)
      → 第3次 → 服务器1 (粘性)

用户B → 第1次 → 服务器2
      → 第2次 → 服务器2 (粘性)
      → 第3次 → 服务器2 (粘性)

实现方式:
- 根据客户端IP hash
- 根据Cookie中的ServerID

Nginx配置

# nginx.conf

upstream backend {
    # IP Hash方式
    ip_hash;
    
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;
    server 192.168.1.103:8080;
}

server {
    listen 80;
    server_name www.example.com;
    
    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

或者使用Cookie方式

upstream backend {
    server 192.168.1.101:8080 route=server1;
    server 192.168.1.102:8080 route=server2;
    server 192.168.1.103:8080 route=server3;
}

server {
    listen 80;
    
    location / {
        # 根据Cookie中的route参数路由
        proxy_pass http://backend;
        proxy_cookie_path / "/; HttpOnly; Secure; SameSite=Lax";
    }
}

优缺点

优点 ✅:
- 实现简单
- 无需改造应用
- 性能好(无额外IO)

缺点 ❌:
- 服务器宕机,用户Session丢失
- 负载不均衡(某些用户可能很活跃)
- 扩展性差(新增服务器困难)
- 不适合长连接场景

适用场景:
- 小规模应用
- Session不重要(丢了可以重新登录)
- 短期过渡方案

🔧 方案2:Session复制(Session Replication)

原理

所有服务器互相同步Session数据

服务器A:Session1, Session2, Session3
         ↓ 同步
服务器B:Session1, Session2, Session3
         ↓ 同步
服务器C:Session1, Session2, Session3

任何服务器的Session变化,都会同步到其他服务器

Tomcat集群配置

<!-- server.xml -->
<Server>
    <Service name="Catalina">
        <Engine name="Catalina" defaultHost="localhost">
            
            <!-- 集群配置 -->
            <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
                
                <!-- Session管理器 -->
                <Manager className="org.apache.catalina.ha.session.DeltaManager"
                         expireSessionsOnShutdown="false"
                         notifyListenersOnReplication="true"/>
                
                <!-- 多播通信 -->
                <Channel className="org.apache.catalina.tribes.group.GroupChannel">
                    <Membership className="org.apache.catalina.tribes.membership.McastService"
                                address="228.0.0.4"
                                port="45564"
                                frequency="500"
                                dropTime="3000"/>
                    
                    <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                              address="auto"
                              port="4000"
                              autoBind="100"
                              selectorTimeout="5000"
                              maxThreads="6"/>
                    
                    <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
                        <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
                    </Sender>
                </Channel>
                
                <!-- 拦截器 -->
                <Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
                       filter=""/>
                <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
                
                <!-- 部署器 -->
                <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
                          tempDir="/tmp/war-temp/"
                          deployDir="/tmp/war-deploy/"
                          watchDir="/tmp/war-listen/"
                          watchEnabled="false"/>
                
            </Cluster>
            
        </Engine>
    </Service>
</Server>

web.xml配置

<!-- web.xml -->
<web-app>
    <!-- 标记为可分布式 -->
    <distributable/>
</web-app>

优缺点

优点 ✅:
- 所有服务器都有完整Session
- 单台服务器宕机不影响

缺点 ❌:
- 同步开销大(网络IO)
- 只适合2-3台服务器
- 数据一致性问题
- 占用大量内存(每台都存全量)

适用场景:
- 极小规模集群(2-3台)
- 已弃用,不推荐 ❌

🔧 方案3:Redis集中存储 ⭐⭐⭐⭐⭐

架构图

┌─────────────────────────────────┐
│         负载均衡器              │
└────────┬────────────────────────┘
         │ 轮询/随机
    ┌────┴────┬────────┬────────┐
    │         │        │        │
┌───▼───┐ ┌──▼───┐ ┌──▼───┐ ┌──▼───┐
│服务器1│ │服务器2│ │服务器3│ │服务器4│
└───┬───┘ └──┬───┘ └──┬───┘ └──┬───┘
    │        │        │        │
    └────────┴────────┴────────┘
             │
        ┌────▼────┐
        │  Redis  │  ← 集中存储Session
        │  集群   │
        └─────────┘

Spring Session + Redis实现

1. 引入依赖

<!-- pom.xml -->
<dependencies>
    <!-- Spring Session -->
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    
    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- Lettuce连接池 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
</dependencies>

2. 配置文件

# application.yml
spring:
  # Redis配置
  redis:
    host: 127.0.0.1
    port: 6379
    password: 
    database: 0
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: 1000ms
    timeout: 3000ms
  
  # Session配置
  session:
    store-type: redis
    redis:
      namespace: spring:session  # Redis key前缀
      flush-mode: on_save        # 保存策略
    timeout: 30m                 # Session过期时间
    
server:
  servlet:
    session:
      cookie:
        name: SESSIONID
        http-only: true
        secure: false
        domain: .example.com  # 跨子域共享

3. 启用Spring Session

/**
 * Spring Session配置
 */
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)  // 30分钟
public class SessionConfig {
    
    /**
     * 自定义Cookie序列化器
     */
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        
        serializer.setCookieName("SESSIONID");
        serializer.setCookiePath("/");
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");  // 跨子域
        serializer.setUseHttpOnlyCookie(true);
        serializer.setUseSecureCookie(false);  // 生产环境改为true
        serializer.setSameSite("Lax");
        
        return serializer;
    }
    
    /**
     * 自定义Session序列化器
     */
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        // 使用JSON序列化
        return new GenericJackson2JsonRedisSerializer();
    }
}

4. 使用Session

/**
 * 用户Controller
 */
@RestController
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    /**
     * 登录
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginRequest request, HttpSession session) {
        String username = request.getUsername();
        String password = request.getPassword();
        
        // 验证用户名密码
        User user = userService.login(username, password);
        
        if (user == null) {
            return Result.fail("用户名或密码错误");
        }
        
        // 存入Session
        session.setAttribute("userId", user.getId());
        session.setAttribute("username", user.getUsername());
        session.setAttribute("roles", user.getRoles());
        
        log.info("用户登录成功: {}, sessionId={}", username, session.getId());
        
        return Result.success("登录成功");
    }
    
    /**
     * 获取当前用户信息
     */
    @GetMapping("/current")
    public Result<User> getCurrentUser(HttpSession session) {
        Long userId = (Long) session.getAttribute("userId");
        
        if (userId == null) {
            return Result.fail("未登录");
        }
        
        User user = userService.getById(userId);
        return Result.success(user);
    }
    
    /**
     * 登出
     */
    @PostMapping("/logout")
    public Result logout(HttpSession session) {
        String sessionId = session.getId();
        
        // 清除Session
        session.invalidate();
        
        log.info("用户登出: sessionId={}", sessionId);
        
        return Result.success("登出成功");
    }
}

5. Redis中的存储结构

Redis中的数据:

1. Session数据
key: spring:session:sessions:【sessionId】
value: {
  "userId": 1000,
  "username": "zhangsan",
  "roles": ["USER", "ADMIN"],
  "creationTime": 1702368000000,
  "lastAccessedTime": 1702371600000,
  "maxInactiveInterval": 1800
}
type: Hash
ttl: 18002. 过期时间索引
key: spring:session:sessions:expires:【sessionId】
value: ""
type: String
ttl: 18003. 用户Session索引(可选)
key: spring:session:index:【principalName】:【sessionId】
value: ""
type: String

自定义Session操作

/**
 * 自定义Session服务
 */
@Service
public class CustomSessionService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String SESSION_PREFIX = "spring:session:sessions:";
    
    /**
     * 获取所有在线用户
     */
    public List<OnlineUser> getOnlineUsers() {
        List<OnlineUser> onlineUsers = new ArrayList<>();
        
        // 扫描所有Session key
        Set<String> keys = redisTemplate.keys(SESSION_PREFIX + "*");
        
        if (keys != null) {
            for (String key : keys) {
                Map<Object, Object> session = redisTemplate.opsForHash().entries(key);
                
                if (!session.isEmpty()) {
                    OnlineUser user = new OnlineUser();
                    user.setUserId((Long) session.get("userId"));
                    user.setUsername((String) session.get("username"));
                    user.setLoginTime((Long) session.get("creationTime"));
                    
                    onlineUsers.add(user);
                }
            }
        }
        
        return onlineUsers;
    }
    
    /**
     * 踢出用户(强制下线)
     */
    public void kickOutUser(String sessionId) {
        String key = SESSION_PREFIX + sessionId;
        redisTemplate.delete(key);
        
        log.info("用户被踢出: sessionId={}", sessionId);
    }
    
    /**
     * 获取用户的所有Session(多地登录)
     */
    public List<String> getUserSessions(Long userId) {
        // 这需要额外维护用户ID到Session的映射
        String indexKey = "user:sessions:" + userId;
        Set<Object> sessionIds = redisTemplate.opsForSet().members(indexKey);
        
        return sessionIds != null ? 
            sessionIds.stream()
                .map(Object::toString)
                .collect(Collectors.toList()) :
            Collections.emptyList();
    }
    
    /**
     * 限制单用户登录(踢出旧Session)
     */
    public void limitSingleLogin(Long userId, String newSessionId) {
        List<String> oldSessions = getUserSessions(userId);
        
        // 删除旧Session
        for (String oldSessionId : oldSessions) {
            if (!oldSessionId.equals(newSessionId)) {
                kickOutUser(oldSessionId);
            }
        }
        
        // 记录新Session
        String indexKey = "user:sessions:" + userId;
        redisTemplate.delete(indexKey);
        redisTemplate.opsForSet().add(indexKey, newSessionId);
        redisTemplate.expire(indexKey, 30, TimeUnit.MINUTES);
    }
}

优缺点

优点 ✅:
- 集中管理,易于维护
- 高性能(Redis内存存储)
- 高可用(Redis集群)
- 扩展性好(无状态服务器)
- 支持Session共享

缺点 ❌:
- 依赖Redis
- 网络IO开销
- Redis故障影响登录

适用场景:
- 大部分分布式应用
- 推荐方案 ⭐⭐⭐⭐⭐

🔧 方案4:JWT无状态 ⭐⭐⭐⭐⭐

原理

传统Session:
客户端 → 发送SessionId → 服务器查询Session数据

JWT方案:
客户端 → 发送JWT Token → 服务器解析Token(无需查询)

JWT Token包含所有用户信息:
{
  "userId": 1000,
  "username": "zhangsan",
  "roles": ["USER", "ADMIN"],
  "exp": 1702371600  // 过期时间
}

优势:
- 无需存储Session
- 服务器完全无状态
- 天然支持分布式

JWT结构

JWT = Header.Payload.Signature

Header(头部):
{
  "alg": "HS256",      // 签名算法
  "typ": "JWT"         // Token类型
}

Payload(负载):
{
  "sub": "1000",       // 主题(用户ID)
  "name": "zhangsan",  // 用户名
  "role": "admin",     // 角色
  "iat": 1702368000,   // 签发时间
  "exp": 1702371600    // 过期时间
}

Signature(签名):
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

完整JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMDAwIiwibmFtZSI6InpoYW5nc2FuIiwicm9sZSI6ImFkbWluIn0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

实现代码

1. 引入依赖

<!-- pom.xml -->
<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>

2. JWT工具类

/**
 * JWT工具类
 */
@Component
public class JwtUtil {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.expiration}")
    private Long expiration;  // 毫秒
    
    private Key key;
    
    @PostConstruct
    public void init() {
        // 初始化密钥
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }
    
    /**
     * 生成Token
     */
    public String generateToken(User user) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);
        
        return Jwts.builder()
            .setSubject(String.valueOf(user.getId()))  // 用户ID
            .claim("username", user.getUsername())     // 用户名
            .claim("roles", user.getRoles())           // 角色
            .setIssuedAt(now)                          // 签发时间
            .setExpiration(expiryDate)                 // 过期时间
            .signWith(key, SignatureAlgorithm.HS256)   // 签名
            .compact();
    }
    
    /**
     * 解析Token
     */
    public Claims parseToken(String token) {
        try {
            return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
            
        } catch (ExpiredJwtException e) {
            throw new AuthException("Token已过期");
        } catch (JwtException e) {
            throw new AuthException("Token无效");
        }
    }
    
    /**
     * 从Token获取用户ID
     */
    public Long getUserIdFromToken(String token) {
        Claims claims = parseToken(token);
        return Long.valueOf(claims.getSubject());
    }
    
    /**
     * 从Token获取用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get("username", String.class);
    }
    
    /**
     * 从Token获取角色
     */
    @SuppressWarnings("unchecked")
    public List<String> getRolesFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get("roles", List.class);
    }
    
    /**
     * 验证Token是否有效
     */
    public boolean validateToken(String token) {
        try {
            parseToken(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    /**
     * 刷新Token
     */
    public String refreshToken(String oldToken) {
        Claims claims = parseToken(oldToken);
        
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);
        
        return Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(key, SignatureAlgorithm.HS256)
            .compact();
    }
}

3. 登录接口

/**
 * 认证Controller
 */
@RestController
@RequestMapping("/auth")
public class AuthController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private JwtUtil jwtUtil;
    
    /**
     * 登录
     */
    @PostMapping("/login")
    public Result<LoginResponse> login(@RequestBody LoginRequest request) {
        String username = request.getUsername();
        String password = request.getPassword();
        
        // 验证用户名密码
        User user = userService.login(username, password);
        
        if (user == null) {
            return Result.fail("用户名或密码错误");
        }
        
        // 生成JWT Token
        String token = jwtUtil.generateToken(user);
        
        // 返回Token
        LoginResponse response = new LoginResponse();
        response.setToken(token);
        response.setTokenType("Bearer");
        response.setExpiresIn(1800000L);  // 30分钟(毫秒)
        response.setUser(user);
        
        log.info("用户登录成功: {}", username);
        
        return Result.success(response);
    }
    
    /**
     * 刷新Token
     */
    @PostMapping("/refresh")
    public Result<String> refreshToken(@RequestHeader("Authorization") String authHeader) {
        String token = extractToken(authHeader);
        
        if (token == null || !jwtUtil.validateToken(token)) {
            return Result.fail("Token无效");
        }
        
        // 刷新Token
        String newToken = jwtUtil.refreshToken(token);
        
        return Result.success(newToken);
    }
    
    /**
     * 登出(JWT本身是无状态的,可以在这里加入黑名单)
     */
    @PostMapping("/logout")
    public Result logout(@RequestHeader("Authorization") String authHeader) {
        String token = extractToken(authHeader);
        
        if (token != null) {
            // 可选:将Token加入黑名单
            blacklistService.addToBlacklist(token);
            
            log.info("用户登出");
        }
        
        return Result.success("登出成功");
    }
    
    private String extractToken(String authHeader) {
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        return null;
    }
}

4. JWT拦截器

/**
 * JWT拦截器
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain filterChain) 
            throws ServletException, IOException {
        
        // 获取Token
        String token = extractToken(request);
        
        if (token != null && jwtUtil.validateToken(token)) {
            // 解析Token
            Long userId = jwtUtil.getUserIdFromToken(token);
            String username = jwtUtil.getUsernameFromToken(token);
            List<String> roles = jwtUtil.getRolesFromToken(token);
            
            // 设置认证信息到上下文
            UserContext userContext = new UserContext();
            userContext.setUserId(userId);
            userContext.setUsername(username);
            userContext.setRoles(roles);
            
            UserContextHolder.setContext(userContext);
            
            log.debug("JWT认证成功: userId={}, username={}", userId, username);
        }
        
        try {
            filterChain.doFilter(request, response);
        } finally {
            // 清理上下文
            UserContextHolder.clear();
        }
    }
    
    private String extractToken(HttpServletRequest request) {
        String authHeader = request.getHeader("Authorization");
        
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        
        return null;
    }
}

/**
 * 用户上下文
 */
public class UserContextHolder {
    
    private static final ThreadLocal<UserContext> contextHolder = new ThreadLocal<>();
    
    public static void setContext(UserContext context) {
        contextHolder.set(context);
    }
    
    public static UserContext getContext() {
        return contextHolder.get();
    }
    
    public static void clear() {
        contextHolder.remove();
    }
}

5. 前端使用

/**
 * 前端JWT使用
 */
class AuthService {
    
    /**
     * 登录
     */
    async login(username, password) {
        const response = await fetch('/auth/login', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ username, password })
        });
        
        const data = await response.json();
        
        if (data.code === 200) {
            // 保存Token到localStorage
            localStorage.setItem('token', data.data.token);
            localStorage.setItem('user', JSON.stringify(data.data.user));
            
            return true;
        } else {
            alert(data.message);
            return false;
        }
    }
    
    /**
     * 发起请求时携带Token
     */
    async request(url, options = {}) {
        const token = localStorage.getItem('token');
        
        if (token) {
            options.headers = options.headers || {};
            options.headers['Authorization'] = 'Bearer ' + token;
        }
        
        const response = await fetch(url, options);
        
        // Token过期,自动刷新
        if (response.status === 401) {
            const refreshed = await this.refreshToken();
            
            if (refreshed) {
                // 重试请求
                return this.request(url, options);
            } else {
                // 跳转到登录页
                window.location.href = '/login';
            }
        }
        
        return response;
    }
    
    /**
     * 刷新Token
     */
    async refreshToken() {
        const token = localStorage.getItem('token');
        
        if (!token) {
            return false;
        }
        
        try {
            const response = await fetch('/auth/refresh', {
                method: 'POST',
                headers: {
                    'Authorization': 'Bearer ' + token
                }
            });
            
            const data = await response.json();
            
            if (data.code === 200) {
                localStorage.setItem('token', data.data);
                return true;
            }
            
        } catch (error) {
            console.error('刷新Token失败', error);
        }
        
        return false;
    }
    
    /**
     * 登出
     */
    logout() {
        localStorage.removeItem('token');
        localStorage.removeItem('user');
        window.location.href = '/login';
    }
}

JWT黑名单(解决无法主动失效的问题)

/**
 * JWT黑名单服务
 */
@Service
public class JwtBlacklistService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 加入黑名单
     */
    public void addToBlacklist(String token) {
        // 解析Token获取过期时间
        Claims claims = jwtUtil.parseToken(token);
        Date expiration = claims.getExpiration();
        
        // 计算剩余有效时间
        long ttl = expiration.getTime() - System.currentTimeMillis();
        
        if (ttl > 0) {
            String key = "jwt:blacklist:" + token;
            redisTemplate.opsForValue().set(key, "1", ttl, TimeUnit.MILLISECONDS);
            
            log.info("Token加入黑名单: ttl={}ms", ttl);
        }
    }
    
    /**
     * 检查是否在黑名单
     */
    public boolean isBlacklisted(String token) {
        String key = "jwt:blacklist:" + token;
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
}

优缺点

优点 ✅:
- 完全无状态
- 无需存储Session
- 天然支持分布式
- 跨域友好
- 移动端友好

缺点 ❌:
- Token较大(携带所有信息)
- 无法主动失效(需要黑名单)
- 敏感信息不能放入Payload
- Token泄露风险

适用场景:
- 微服务架构
- 移动APP
- 跨域场景
- 推荐方案 ⭐⭐⭐⭐⭐

🎯 方案选择

决策树

是否微服务架构?
├─ 是 → JWT ✅
└─ 否
    ├─ 是否需要跨域?
    │   ├─ 是 → JWT ✅
    │   └─ 否
    │       ├─ 服务器数量?
    │       │   ├─ ≤ 3台 → Session粘性
    │       │   └─ > 3台 → Redis Session ✅
    │       └─
    └─

混合方案

/**
 * 混合方案:短期用JWT,长期用Refresh Token(存Redis)
 */
@Service
public class HybridAuthService {
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 登录:返回Access Token + Refresh Token
     */
    public TokenPair login(User user) {
        // 1. 生成短期Access Token(30分钟)
        String accessToken = jwtUtil.generateToken(user, 1800000L);
        
        // 2. 生成长期Refresh Token(7天)
        String refreshToken = UUID.randomUUID().toString().replace("-", "");
        
        // 3. Refresh Token存入Redis
        String key = "refresh:token:" + refreshToken;
        Map<String, Object> data = new HashMap<>();
        data.put("userId", user.getId());
        data.put("username", user.getUsername());
        data.put("createTime", System.currentTimeMillis());
        
        redisTemplate.opsForHash().putAll(key, data);
        redisTemplate.expire(key, 7, TimeUnit.DAYS);
        
        TokenPair tokens = new TokenPair();
        tokens.setAccessToken(accessToken);
        tokens.setRefreshToken(refreshToken);
        tokens.setExpiresIn(1800);  // 秒
        
        return tokens;
    }
    
    /**
     * 刷新Access Token
     */
    public String refresh(String refreshToken) {
        String key = "refresh:token:" + refreshToken;
        
        Map<Object, Object> data = redisTemplate.opsForHash().entries(key);
        
        if (data.isEmpty()) {
            throw new AuthException("Refresh Token无效");
        }
        
        Long userId = Long.valueOf(data.get("userId").toString());
        User user = userService.getById(userId);
        
        // 生成新的Access Token
        return jwtUtil.generateToken(user, 1800000L);
    }
}

🎉 总结

对比总结

维度Session粘性Session复制Redis SessionJWT
实现难度⭐⭐⭐⭐⭐⭐⭐⭐⭐
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
扩展性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
可靠性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
推荐度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

记忆口诀

分布式Session要共享,
四种方案各有优缺。

Session粘性最简单,
IP Hash绑服务器。
适合小规模临时用,
宕机丢失是缺点。

Session复制全同步,
所有服务器都有份。
只适二三台服务器,
扩展性差已淘汰。

Redis集中是主流,
Spring Session很方便。
高性能又高可用,
大部分场景都推荐。

JWT无状态最先进,
Token包含用户信息。
微服务跨域都适合,
注意黑名单和安全!

实际选择看场景,
单体Redis就够用。
微服务首选JWT,
混合方案更灵活!

愿你的用户永远在线,Session永不丢失! 🔐✨