公司动态
Session、JWT与Redis Token:Web登录凭证方案全解析与选型指南
在实际 Web 开发中登录凭证是构建用户身份认证与授权体系的核心。无论是传统的单体应用还是现代的微服务、前后端分离架构开发者都需要在 Session、Token 以及基于 Redis 的 Token 方案之间做出选择。很多开发者对 JWT 或 Token 的概念有所了解但往往知其然而不知其所以然不清楚在何种场景下该选择哪种方案也不清楚每种方案背后需要处理的安全、性能和运维细节。本文将深入解析三种主流的登录凭证方案传统的 Session-Cookie 方案、无状态的 JWT Token 方案以及结合了状态管理与分布式优势的 Redis Token 方案。我们会从工作原理、实现流程、安全考量、适用场景和常见陷阱等多个维度进行对比并给出具体的代码示例和配置建议。无论你是正在设计一个新系统的登录模块还是试图优化现有系统的认证流程理解这三种方案的差异和实现细节都能帮助你做出更合适的技术选型并有效规避开发与生产环境中的常见问题。1. 登录凭证的核心概念与挑战在深入具体方案之前我们首先需要明确登录凭证要解决的根本问题以及不同方案所面临的共同挑战。1.1 认证与授权的基石登录凭证登录凭证的本质是服务器颁发给客户端的一个“身份证明”。在用户成功提供用户名和密码或其他认证方式后服务器需要一种机制来记住“这个用户已经登录了”以便在后续的请求中识别其身份而无需用户反复提交密码。这个“身份证明”就是登录凭证。凭证必须满足几个基本要求可验证性服务器必须能快速、可靠地验证客户端提交的凭证是否有效、是否由自己签发。安全性凭证本身不能被轻易伪造或篡改传输过程需要防止被窃听通常依赖 HTTPS。时效性凭证应有生命周期过期后需要重新认证以降低凭证泄露带来的风险。上下文关联性凭证需要能够关联到具体的用户身份和会话信息如用户ID、角色、权限等。不同的方案正是在如何存储、传输和验证这份“身份证明”上做出了不同的技术选择。1.2 核心挑战状态管理与分布式环境所有登录凭证方案都需要回答一个关键问题会话状态存储在哪里服务器内存最简单但无法扩展服务器重启即丢失。集中式存储如数据库、Redis解决了扩展和持久化问题但引入了外部依赖和网络开销。客户端如 Token 的 Payload实现了无状态服务器无需存储但带来了令牌管理如注销、续签的复杂性。在单体应用时代Session 存储在服务器内存是常见做法。但在微服务、集群化部署的今天请求可能被负载均衡到任何一台服务器这就要求会话状态必须能被集群内所有节点访问从而催生了集中式 Session 存储和完全无状态的 Token 方案。另一个挑战是安全性。无论哪种方案都需要妥善处理凭证的生成、传输、存储和销毁防范诸如会话固定、跨站请求伪造CSRF、跨站脚本XSS盗取凭证等常见攻击。2. 方案一传统的 Session-Cookie 方案这是最经典、最广为人知的方案其核心思想是服务器有状态。2.1 工作原理与流程用户登录客户端提交用户名和密码。创建会话服务器验证凭据通过后在服务器端内存或数据库创建一个 Session 对象用于存储用户信息如 userId, username。同时生成一个唯一的、复杂的 Session ID。返回凭证服务器将 Session ID 通过 HTTP 响应头Set-Cookie发送给客户端。通常这个 Cookie 会被设置为HttpOnly防止 JavaScript 访问和Secure仅通过 HTTPS 传输。后续请求客户端浏览器会自动在后续请求的Cookie头中携带此 Session ID。验证身份服务器接收到请求后从 Cookie 中取出 Session ID去自己的 Session 存储区查找对应的 Session 对象。如果找到且未过期则认为用户已认证可以从 Session 中取出用户信息进行业务处理。sequenceDiagram participant C as Client (Browser) participant S as Server (with Session Store) C-S: POST /login (username, password) S-S: 验证凭据创建Session对象 S-S: 生成唯一Session ID S--C: HTTP Response: Set-Cookie: SESSION_IDabc123 Note over C: 浏览器自动存储Cookie C-S: GET /api/data (Cookie: SESSION_IDabc123) S-S: 根据“abc123”查找Session S-S: 找到Session获取用户信息 S--C: HTTP 200 (with data)2.2 实现示例以 Spring Boot 为例Spring Security 和 Servlet 容器对 Session-Cookie 有内置支持配置简单。依赖(pom.xml):dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency控制器示例:RestController public class LoginController { PostMapping(/login) public ResponseEntityString login(RequestBody LoginRequest request, HttpSession session) { // 1. 模拟用户验证 (实际应从数据库查询) if (admin.equals(request.getUsername()) 123456.equals(request.getPassword())) { // 2. 将用户信息存入Session (服务器端) session.setAttribute(USER_ID, 1001); session.setAttribute(USERNAME, request.getUsername()); // 3. Session ID 会自动通过Set-Cookie返回给客户端 return ResponseEntity.ok(登录成功); } return ResponseEntity.status(401).body(用户名或密码错误); } GetMapping(/profile) public ResponseEntityString getProfile(HttpSession session) { // 4. 后续请求中从Session获取用户信息 Integer userId (Integer) session.getAttribute(USER_ID); if (userId null) { return ResponseEntity.status(401).body(未登录); } return ResponseEntity.ok(用户ID: userId); } }关键点HttpSession对象由 Servlet 容器如 Tomcat管理。默认情况下Session 存储在服务器内存中Session ID 通过名为JSESSIONID的 Cookie 传递。Spring Security 默认会启用 CSRF 保护在表单登录场景下需要处理 CSRF Token对于纯 API 项目可以考虑禁用或使用 Token 方式处理。2.3 优点与缺点分析优点成熟简单技术栈原生支持开发速度快。默认安全HttpOnly和SecureCookie 能有效防御 XSS 盗取和中间人攻击。服务端完全控制可以随时让某个 Session 失效强制下线管理能力强。缺点扩展性差默认内存存储无法跨多台服务器共享会话不适用于集群。性能瓶颈随着用户量增长Session 存储可能成为瓶颈即使使用外部存储如 Redis也多了网络 IO。对移动端/非浏览器客户端不友好Cookie 是浏览器标准但原生 App、小程序等需要手动处理。可能引发 CSRF 攻击因为认证信息自动随 Cookie 发送需要额外机制如 CSRF Token来防御。2.4 常见问题与排查问题现象可能原因检查方式处理建议登录后下次请求依然提示未登录1. Cookie 未成功设置或发送。2. 跨域请求未携带 Cookie。3. 服务器 Session 存储失效如重启。1. 浏览器开发者工具查看Application-Cookies。2. 检查请求头是否包含Cookie: JSESSIONIDxxx。3. 检查后端 Session 配置和存储。1. 确保响应头有Set-Cookie。2. 前端跨域请求需设置withCredentials: true。3. 考虑使用外部存储如 Redis持久化 Session。出现Invalid CSRF Token错误Spring Security 默认启用 CSRF 保护但请求未提供 CSRF Token。查看请求是否包含X-CSRF-TOKEN头或_csrf参数。对于纯 API 项目可在 Security 配置中.csrf().disable()。或按照规范实现 CSRF Token 的提交。会话很快过期Session 默认过期时间如30分钟太短。检查服务器配置如server.servlet.session.timeout。在application.properties中调整server.servlet.session.timeout3600(单位秒)。3. 方案二无状态的 JWT Token 方案JWTJSON Web Token是一种流行的无状态令牌方案。其核心思想是将用户信息直接编码到令牌中服务器无需存储会话状态仅通过验证令牌的签名来判断其有效性。3.1 JWT 的结构与工作原理一个 JWT 令牌由三部分组成以点.分隔Header.Payload.Signature。Header通常包含令牌类型typ: “JWT”和所使用的签名算法alg: “HS256”或RS256。{ alg: HS256, typ: JWT }Payload存放声明Claims即需要传递的信息。包含标准声明如iss签发者exp过期时间sub主题和自定义声明如userId,username。{ sub: 1234567890, name: John Doe, userId: 1001, iat: 1516239022, exp: 1516242622 }Signature对编码后的 Header 和 Payload使用一个密钥Secret通过指定算法如 HMAC SHA256进行签名用于验证消息在传输过程中未被篡改。HMACSHA256( base64UrlEncode(header) . base64UrlEncode(payload), secret)工作流程用户登录服务器验证成功。服务器使用密钥生成一个包含用户信息的 JWT返回给客户端通常放在响应体如{“token”: “xxx.yyy.zzz”}。客户端保存此 Token通常存于localStorage或sessionStorage。后续请求客户端在Authorization请求头中携带 TokenAuthorization: Bearer token。服务器收到请求从Authorization头取出 Token使用相同的密钥验证签名。如果签名有效且未过期则信任 Payload 中的用户信息完成认证。sequenceDiagram participant C as Client participant S as Server (Stateless) C-S: POST /login (credentials) S-S: 验证凭据生成JWT (含用户信息签名) S--C: HTTP 200 {“token”: “jwt_string”} Note over C: 客户端存储Token C-S: GET /api/data (Header: Authorization: Bearer jwt_string) S-S: 验证JWT签名和有效期 S-S: 从JWT Payload解析用户信息 S--C: HTTP 200 (with data)3.2 实现示例使用jjwt库依赖(pom.xml):dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependencyJWT 工具类:import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import javax.crypto.SecretKey; import java.util.Date; import java.util.HashMap; import java.util.Map; public class JwtUtil { // 密钥必须足够长且保密生产环境应从配置中心读取。 private static final SecretKey SECRET_KEY Keys.hmacShaKeyFor(YourSuperLongAndSecureSecretKeyHereAtLeast32Bytes.getBytes()); // 令牌有效期例如 2 小时 private static final long EXPIRATION 7200 * 1000; public static String generateToken(Integer userId, String username) { MapString, Object claims new HashMap(); claims.put(userId, userId); claims.put(username, username); // 也可以放入角色、权限等信息但注意Payload不宜过大 return Jwts.builder() .setClaims(claims) // 设置自定义声明 .setSubject(String.valueOf(userId)) // 标准声明主题 .setIssuedAt(new Date()) // 签发时间 .setExpiration(new Date(System.currentTimeMillis() EXPIRATION)) // 过期时间 .signWith(SECRET_KEY, SignatureAlgorithm.HS256) // 签名算法和密钥 .compact(); } public static Claims parseToken(String token) { try { return Jwts.parserBuilder() .setSigningKey(SECRET_KEY) .build() .parseClaimsJws(token) .getBody(); } catch (ExpiredJwtException e) { throw new RuntimeException(令牌已过期, e); } catch (JwtException e) { throw new RuntimeException(无效令牌, e); } } public static boolean validateToken(String token) { try { parseToken(token); return true; } catch (RuntimeException e) { return false; } } }登录与验证过滤器: 在 Spring Security 中通常需要实现一个JwtAuthenticationFilter将其添加到过滤器链中用于拦截请求并解析 JWT。这里给出一个简化的 Controller 示例RestController public class AuthController { PostMapping(/auth/login) public ResponseEntityMapString, String login(RequestBody LoginRequest request) { // 验证用户... if (admin.equals(request.getUsername()) 123456.equals(request.getPassword())) { String token JwtUtil.generateToken(1001, request.getUsername()); MapString, String response new HashMap(); response.put(token, token); return ResponseEntity.ok(response); } return ResponseEntity.status(401).build(); } GetMapping(/api/protected) public ResponseEntityString protectedApi(RequestHeader(Authorization) String authHeader) { if (authHeader null || !authHeader.startsWith(Bearer )) { return ResponseEntity.status(401).body(缺少Token); } String token authHeader.substring(7); try { Claims claims JwtUtil.parseToken(token); Integer userId claims.get(userId, Integer.class); // 基于claims中的信息进行授权判断... return ResponseEntity.ok(访问成功用户ID: userId); } catch (RuntimeException e) { return ResponseEntity.status(401).body(Token无效或过期: e.getMessage()); } } }3.3 优点与缺点分析优点无状态扩展性强服务器无需存储会话信息天生适合分布式和微服务架构。任何服务节点只要持有密钥即可验证 Token。自包含Payload 可携带用户基本信息减少了对用户服务或数据库的查询次数。多端友好不依赖 Cookie适用于浏览器、移动端、桌面客户端等各种场景。性能潜力避免了集中式会话存储的查询开销。缺点令牌无法主动失效这是 JWT 最著名的缺点。一旦签发在到期前始终有效。实现“登出”或“强制下线”功能困难需要借助黑名单等额外机制。令牌大小Payload 信息越多Token 越长每次请求都会增加网络开销。密钥管理签名密钥的保管和轮换是关键一旦泄露后果严重。性能陷阱将过多信息放入 Payload并每次解析可能比查询一次缓存如 Redis更耗时。3.4 常见问题与安全实践问题现象可能原因检查方式处理建议Signature verification failed1. 用于验证的密钥与签发密钥不一致。2. Token 被篡改。对比服务器使用的密钥。检查 Token 三部分是否完整。确保签发和验证使用相同的密钥。密钥应从安全配置源获取。JWT expiredToken 已超过exp声明的时间。解析 Token 查看exp字段。客户端需在 Token 过期前使用 Refresh Token 获取新 Token或引导用户重新登录。如何实现登出JWT 无状态服务端无法直接作废未过期的 Token。-实现方案1. 客户端主动删除存储的 Token。2. 服务端维护一个短期的 Token 黑名单需存储。3. 使用短有效期 Token Refresh Token 机制。安全实践清单1.使用强算法和足够长的密钥推荐HS256或RS256密钥长度至少 32 字节HS256。2.Token 存放位置浏览器端建议存于HttpOnlyCookie防 XSS但需注意 CSRF 防护或存于内存变量页面关闭即丢失。避免长期存于localStorage。3.设置合理的有效期Access Token 有效期宜短如 15-30 分钟通过 Refresh Token 续期。4.启用 HTTPS防止 Token 在传输中被窃听。5.不要在 Payload 中存放敏感信息如密码、密钥等。Payload 是 Base64 编码可被解码查看。4. 方案三基于 Redis 的 Token 方案这是一种折中方案结合了 Session 的可控性和 Token 的灵活性。其核心思想是服务器颁发一个随机 Token如 UUID作为键将用户会话信息存储在 Redis 中Token 本身无意义仅作为查找会话信息的钥匙。4.1 工作原理与流程用户登录客户端提交凭据。生成 Token 并存储服务器验证成功后生成一个唯一的随机字符串作为 Token例如 UUID。然后以该 Token 为 key将用户信息序列化为 JSON 或特定格式作为 value存入 Redis并设置一个过期时间TTL。返回 Token服务器将 Token 返回给客户端可通过响应体也可通过 Cookie。后续请求客户端在请求头如Authorization: Bearer token或 Cookie 中携带此 Token。验证身份服务器收到 Token 后用其作为 key 去 Redis 查询。如果查到对应的值且未过期则认证成功并反序列化出用户信息。同时可以很方便地“续期”本次会话刷新 Redis 中该 key 的 TTL。sequenceDiagram participant C as Client participant S as Server participant R as Redis C-S: POST /login (credentials) S-S: 验证凭据生成随机Token (e.g., UUID) S-R: SET token:uuid123 {userInfo} EX 3600 S--C: HTTP 200 {“token”: “uuid123”} C-S: GET /api/data (Header: Authorization: Bearer uuid123) S-R: GET token:uuid123 R--S: {userInfo} S-S: 反序列化userInfo完成认证 S--C: HTTP 200 (with data)4.2 实现示例Spring Boot Spring Data Redis依赖(pom.xml):dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency !-- 使用Jackson进行序列化 -- dependency groupIdcom.fasterxml.jackson.datatype/groupId artifactIdjackson-datatype-jsr310/artifactId /dependencyRedis 配置(application.yml):spring: redis: host: localhost port: 6379 password: # 如果有密码 database: 0 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0Token 服务类:import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.UUID; import java.util.concurrent.TimeUnit; Service public class RedisTokenService { Autowired private StringRedisTemplate redisTemplate; Autowired private ObjectMapper objectMapper; // Jackson ObjectMapper private static final String TOKEN_PREFIX auth:token:; private static final long EXPIRATION_HOURS 2; /** * 创建Token并存储用户信息 */ public String createToken(UserInfo userInfo) throws Exception { String token UUID.randomUUID().toString(); String key TOKEN_PREFIX token; String value objectMapper.writeValueAsString(userInfo); // 序列化用户信息 redisTemplate.opsForValue().set(key, value, EXPIRATION_HOURS, TimeUnit.HOURS); return token; } /** * 验证Token并获取用户信息 */ public UserInfo validateToken(String token) throws Exception { String key TOKEN_PREFIX token; String value redisTemplate.opsForValue().get(key); if (value null) { return null; // Token不存在或已过期 } // 可选每次验证成功刷新Token有效期滑动过期 redisTemplate.expire(key, EXPIRATION_HOURS, TimeUnit.HOURS); return objectMapper.readValue(value, UserInfo.class); // 反序列化 } /** * 使Token失效登出 */ public void invalidateToken(String token) { String key TOKEN_PREFIX token; redisTemplate.delete(key); } } // 简单的用户信息对象 class UserInfo { private Integer userId; private String username; private ListString roles; // getters and setters ... }控制器示例:RestController RequestMapping(/auth) public class AuthController { Autowired private RedisTokenService tokenService; PostMapping(/login) public ResponseEntityMapString, String login(RequestBody LoginRequest request) throws Exception { // 1. 验证用户 (略) UserInfo userInfo new UserInfo(1001, request.getUsername(), Arrays.asList(USER)); // 2. 创建Token String token tokenService.createToken(userInfo); MapString, String response new HashMap(); response.put(token, token); return ResponseEntity.ok(response); } GetMapping(/profile) public ResponseEntity? getProfile(RequestHeader(Authorization) String authHeader) throws Exception { if (authHeader null || !authHeader.startsWith(Bearer )) { return ResponseEntity.status(401).build(); } String token authHeader.substring(7); UserInfo userInfo tokenService.validateToken(token); if (userInfo null) { return ResponseEntity.status(401).body(Token无效或已过期); } // 3. 使用userInfo进行业务处理 return ResponseEntity.ok(欢迎, userInfo.getUsername()); } PostMapping(/logout) public ResponseEntityVoid logout(RequestHeader(Authorization) String authHeader) { if (authHeader ! null authHeader.startsWith(Bearer )) { String token authHeader.substring(7); tokenService.invalidateToken(token); // 4. 主动使Token失效 } return ResponseEntity.ok().build(); } }4.3 优点与缺点分析优点集中控制可主动管理服务端存储会话信息可以轻松实现登出、强制下线、查看在线用户等功能。信息灵活Redis 中可存储结构更复杂、信息量更大的会话数据。性能优异Redis 作为内存数据库读写速度极快适合高频的会话验证操作。天然支持分布式所有服务节点访问同一个 Redis会话自然共享。安全性较好Token 是随机值无法被解码出信息。即使 Redis 数据泄露只要密钥不同也无法伪造有效 Token。缺点引入外部依赖系统可用性依赖于 Redis 的可用性需要保障 Redis 集群的高可用。网络开销每次验证都需要一次 Redis 查询尽管很快。复杂度增加需要设计 Redis 键的命名空间、序列化方式、过期策略和内存管理。4.4 部署与优化建议Redis 高可用生产环境务必使用 Redis 哨兵Sentinel或集群Cluster模式避免单点故障。内存优化合理设置会话过期时间TTL避免无用数据堆积。对存储的会话信息进行精简不要存入过多业务数据。监控 Redis 内存使用情况设置maxmemory-policy如allkeys-lru。键设计使用清晰的命名空间如auth:token:token、auth:user:userId:tokens可用于存储用户的所有活跃 Token实现“踢掉其他设备”功能。序列化选择推荐使用 JSON如 Jackson进行序列化可读性好兼容性强。避免 Java 原生序列化。连接池配置合适的 Lettuce 或 Jedis 连接池参数避免连接瓶颈。5. 三种方案对比与选型指南为了更直观地对比我们将三种方案的核心特性总结如下特性维度Session-Cookie (服务器内存)JWT Token (无状态)Redis Token (有状态)状态存储服务器内存默认客户端 Token 中集中式存储 (Redis)扩展性差需 Session 共享优秀无状态好依赖 Redis 扩展性能好内存访问好本地验证很好Redis 内存访问主动注销支持移除 Session不支持需黑名单支持删除 Redis Key多端支持弱依赖 Cookie优秀好需手动处理 Token传输方式自动Cookie手动Header手动Header/Cookie数据承载灵活服务器对象有限Token 不宜过大灵活Redis Value安全性好HttpOnly Cookie中依赖存储和传输安全好随机 Token复杂度低框架原生中需处理续签、注销中需维护 Redis典型场景传统单体 Web 应用分布式 API、微服务、一次性验证需要强会话管理的 Web/App、单点登录SSO选型建议选择 Session-Cookie 如果你开发的是一个传统的、非分布式的单体 Web 应用且不需要服务原生移动端。追求最简单的开发和默认的安全防护HttpOnly Cookie。选择 JWT 如果你构建的是前后端分离的 SPA、移动 App API 或微服务架构需要无状态扩展并且可以接受其“无法主动注销”的缺点或愿意通过短有效期Refresh Token黑名单等组合方案来弥补。选择 Redis Token 如果你需要对用户会话有完全的控制力如强制下线、查看在线状态系统已经是分布式架构并且愿意引入和维护 Redis 集群。这是大多数中大型 Web 系统在需要强会话管理时的折中选择。在实际项目中方案并非互斥。例如可以在网关层使用 JWT 进行初步的签名验证和路由在具体的业务服务中使用 Redis 来存储更详细的会话上下文。理解每种方案的原理和边界才能设计出最适合自己业务场景的认证授权体系。