副标题: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: 1800秒
2. 过期时间索引
key: spring:session:sessions:expires:【sessionId】
value: ""
type: String
ttl: 1800秒
3. 用户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 Session | JWT |
|---|---|---|---|---|
| 实现难度 | ⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 性能 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 扩展性 | ⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 可靠性 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 推荐度 | ⭐⭐ | ❌ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
记忆口诀
分布式Session要共享,
四种方案各有优缺。
Session粘性最简单,
IP Hash绑服务器。
适合小规模临时用,
宕机丢失是缺点。
Session复制全同步,
所有服务器都有份。
只适二三台服务器,
扩展性差已淘汰。
Redis集中是主流,
Spring Session很方便。
高性能又高可用,
大部分场景都推荐。
JWT无状态最先进,
Token包含用户信息。
微服务跨域都适合,
注意黑名单和安全!
实际选择看场景,
单体Redis就够用。
微服务首选JWT,
混合方案更灵活!
愿你的用户永远在线,Session永不丢失! 🔐✨