Cookie、Session、JWT、SSO,网站与 APP 登录持久化与缓存
Cookie、Session、JWT、SSO,网站与 APP 登录持久化与缓存
Cookie 和 Session ,jwt,sso这些到底是什么,有什么区别或关系,他们的使用场景有哪些(给出具体实现代码)
1.Cookie、Session、JWT、SSO:概念、区别、关系与实战代码
在 Java 后端开发中,Cookie、Session、JWT、SSO 是认证与授权领域的核心技术,但很多开发者容易混淆它们的定位和用法。本文将从核心定义、区别与关系、适用场景三个维度层层拆解,结合实战代码,帮你彻底理清这四类技术的本质,掌握在不同场景下的选型与落地。
一、核心概念:一句话讲清每个技术的定位
技术 | 核心定义 | 本质作用 |
---|---|---|
Cookie | 客户端(浏览器)存储的小型文本数据,由服务器通过Set-Cookie 头下发,请求时自动携带 | 存储少量状态(如 SessionID、Token) |
Session | 服务器端存储的用户会话状态,通过 Cookie 中的 SessionID 关联客户端与服务器 | 维护用户登录状态(如登录信息、权限) |
JWT | 无状态的 JSON 格式令牌,包含用户身份信息,通过签名保证完整性,客户端存储 | 分布式场景下的身份凭证(替代 Session) |
SSO | 单点登录系统,用户登录一次即可访问所有信任系统,整合认证流程 | 解决多系统重复登录问题 |
二、区别与关系:从技术维度对比
1. 存储位置与状态性
这是四类技术最核心的区别,直接决定了它们的分布式适配能力:
技术 | 存储位置 | 状态性 | 分布式适配能力 |
---|---|---|---|
Cookie | 客户端(浏览器) | 客户端状态 | 天然支持(无需服务器共享) |
Session | 服务器端(内存 / Redis) | 服务器状态 | 需共享存储(如 Redis),否则 Session 丢失 |
JWT | 客户端(localStorage/Cookie) | 无状态 | 天然支持(服务器无需存储状态) |
SSO | 认证中心存储用户状态 | 中心状态 | 需统一认证中心,子系统无状态 |
关系说明:
- Cookie 是 Session 的 “载体”:Session 通过 Cookie 中的 SessionID 关联客户端;
- JWT 是 Session 的 “无状态替代方案”:分布式场景下,用 JWT 避免 Session 共享的复杂度;
- SSO 是 “认证流程的整合”:可基于 Session 或 JWT 实现(如认证中心生成 JWT,子系统验证)。
2. 安全性对比
不同技术的安全风险和防护手段差异显著:
技术 | 安全风险 | 防护手段 |
---|---|---|
Cookie | 被窃取(XSS)、被伪造(CSRF) | 设置HttpOnly (防 XSS)、SameSite (防 CSRF)、Secure (仅 HTTPS) |
Session | SessionID 泄露(CSRF)、服务器存储风险 | 定期刷新 SessionID、限制 Session 有效期、Redis 存储防丢失 |
JWT | Payload 可解码(非加密)、无法主动吊销 | 不存敏感信息、短期 Token + 刷新 Token、维护黑名单(Redis) |
SSO | 认证中心被攻击、Token 泄露 | HTTPS 加密、Token 短期有效、多因素认证(MFA) |
3. 核心区别总结
对比维度 | Cookie | Session | JWT | SSO |
---|---|---|---|---|
数据大小 | 最大 4KB | 无限制(服务器资源决定) | 建议不超过 1KB | 无限制(取决于存储) |
有效期 | 可设置(Persistent) | 默认会话级(关闭浏览器失效) | 固定有效期(Payload 中) | 取决于 Token 有效期 |
服务器开销 | 无 | 高(存储状态) | 低(仅验证签名) | 中(认证中心维护状态) |
适用架构 | 所有架构 | 单体应用 | 微服务 / 前后端分离 | 多系统集群 |
三、适用场景与实战代码
1. Cookie:客户端状态存储(如 SessionID、语言偏好)
适用场景:
- 存储 SessionID(关联 Session);
- 存储用户偏好(如语言、主题);
- 记住登录状态(“记住我” 功能)。
实战代码:Spring Boot 下发与读取 Cookie
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@RestController
public class CookieController {// 1. 下发Cookie(如存储用户语言偏好)@GetMapping("/set-cookie")public String setCookie(HttpServletResponse response) {// 创建Cookie(name=language, value=zh-CN)Cookie languageCookie = new Cookie("language", "zh-CN");languageCookie.setMaxAge(30 * 24 * 60 * 60); // 有效期30天languageCookie.setPath("/"); // 所有路径可见languageCookie.setHttpOnly(false); // 允许前端读取(用于语言切换)languageCookie.setSecure(true); // 仅HTTPS传递(生产环境必须)languageCookie.setSameSite("Lax"); // 防CSRF// 下发Cookieresponse.addCookie(languageCookie);return "Cookie已下发:language=zh-CN";}// 2. 读取Cookie(获取用户语言偏好)@GetMapping("/get-cookie")public String getCookie(HttpServletRequest request) {Cookie[] cookies = request.getCookies();if (cookies == null) {return "未获取到Cookie";}// 遍历Cookie,找到languagefor (Cookie cookie : cookies) {if ("language".equals(cookie.getName())) {return "当前语言偏好:" + cookie.getValue();}}return "未找到language Cookie";}
}
2. Session:单体应用的用户状态管理
适用场景:
- 单体应用的用户登录状态维护(如管理系统);
- 存储用户临时数据(如购物车、表单临时数据);
- 无需分布式部署的小型应用。
实战代码:Spring Boot 使用 Session
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;@RestController
public class SessionController {// 1. 登录:创建Session@PostMapping("/login")public String login(@RequestParam String username,@RequestParam String password,HttpSession session) {// 模拟数据库校验(实际需查DB)if ("admin".equals(username) && "123456".equals(password)) {// 存储用户信息到Sessionsession.setAttribute("userId", 1L);session.setAttribute("username", username);session.setAttribute("role", "ADMIN");session.setMaxInactiveInterval(30 * 60); // 有效期30分钟return "登录成功,SessionID:" + session.getId();}throw new RuntimeException("账号或密码错误");}// 2. 业务接口:验证Session@GetMapping("/admin/order")public String getAdminOrder(HttpSession session) {// 检查Session是否有效Long userId = (Long) session.getAttribute("userId");String role = (String) session.getAttribute("role");if (userId == null) {return "请先登录";}// 检查权限(仅ADMIN可访问)if (!"ADMIN".equals(role)) {return "无权限访问管理员订单";}return "管理员订单列表:...";}// 3. 登出:销毁Session@GetMapping("/logout")public String logout(HttpSession session) {session.invalidate(); // 销毁Sessionreturn "登出成功";}
}
分布式适配:若单体应用扩展为多节点,需用 Spring Session+Redis 共享 Session:
<!-- 引入依赖 -->
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
# 配置Redis存储Session
spring:session:store-type: redisredis:namespace: spring:sessionmax-inactive-interval: 1800 # 30分钟redis:host: localhostport: 6379
3. JWT:分布式 / 前后端分离的无状态认证
适用场景:
- 微服务架构(无状态,无需 Session 共享);
- 前后端分离项目(Vue/React + Spring Boot);
- 第三方 API 接口(如开放平台的身份凭证)。
实战代码:Spring Boot 实现 JWT 认证
3.1 JWT 工具类(生成、验证、解析)
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 JwtUtils {// 密钥(生产环境存配置中心,32字节用于HS256)private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor("your-32-byte-secret-key-12345678".getBytes());// 有效期2小时private static final long EXPIRATION = 2 * 60 * 60 * 1000;// 生成JWTpublic static String generateToken(Long userId, String username, String role) {Map<String, Object> claims = new HashMap<>();claims.put("userId", userId);claims.put("username", username);claims.put("role", role);return Jwts.builder().setClaims(claims).setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)).signWith(SECRET_KEY, SignatureAlgorithm.HS256).compact();}// 验证JWT并解析用户信息public static Map<String, Object> validateToken(String token) {try {Jws<Claims> jws = Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token);Claims claims = jws.getBody();Map<String, Object> userInfo = new HashMap<>();userInfo.put("userId", claims.get("userId"));userInfo.put("username", claims.get("username"));userInfo.put("role", claims.get("role"));return userInfo;} catch (ExpiredJwtException e) {throw new RuntimeException("JWT已过期");} catch (MalformedJwtException | SignatureException e) {throw new RuntimeException("JWT无效或被篡改");}}
}
3.2 登录接口(生成 JWT)
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
public class JwtLoginController {@PostMapping("/jwt/login")public String login(@RequestParam String username,@RequestParam String password) {// 模拟校验if ("admin".equals(username) && "123456".equals(password)) {// 生成JWTreturn JwtUtils.generateToken(1L, username, "ADMIN");}throw new RuntimeException("账号或密码错误");}
}
3.3 拦截器(统一验证 JWT)
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;public class JwtInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 从Authorization头获取JWTString authHeader = request.getHeader("Authorization");if (authHeader == null || !authHeader.startsWith("Bearer ")) {response.setStatus(401);response.getWriter().write("未携带JWT");return false;}String token = authHeader.substring(7);Map<String, Object> userInfo;try {userInfo = JwtUtils.validateToken(token);} catch (Exception e) {response.setStatus(401);response.getWriter().write(e.getMessage());return false;}// 传递用户信息到业务接口request.setAttribute("userId", userInfo.get("userId"));request.setAttribute("role", userInfo.get("role"));return true;}
}
3.4 配置拦截器
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new JwtInterceptor()).addPathPatterns("/**").excludePathPatterns("/jwt/login"); // 登录接口无需拦截}
}
4. SSO:多系统单点登录(整合认证流程)
适用场景:
- 企业多系统(如 OA、CRM、ERP);
- 互联网产品矩阵(如京东金融、京东超市、京东国际);
- 第三方平台接入(如微信开放平台、支付宝开放平台)。
实战代码:基于 JWT 的 SSO 认证中心(简化版)
4.1 认证中心核心接口
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@RestController
@RequestMapping("/sso")
public class SsoAuthController {// 1. 登录页面(子系统重定向到这里)@GetMapping("/login-page")public String loginPage(@RequestParam String redirectUri) {// 实际返回登录HTML页面,这里简化为字符串return "请登录:<form action='/sso/login' method='post'>" +"<input name='username' placeholder='用户名'><br>" +"<input name='password' type='password' placeholder='密码'><br>" +"<input type='hidden' name='redirectUri' value='" + redirectUri + "'>" +"<button type='submit'>登录</button></form>";}// 2. 登录接口(生成JWT,重定向回子系统)@PostMapping("/login")public void login(@RequestParam String username,@RequestParam String password,@RequestParam String redirectUri,HttpServletResponse response) throws IOException {// 模拟校验if ("admin".equals(username) && "123456".equals(password)) {String jwt = JwtUtils.generateToken(1L, username, "ADMIN");// 重定向回子系统,携带JWTresponse.sendRedirect(redirectUri + "?token=" + jwt);return;}// 登录失败,重定向回登录页response.sendRedirect("/sso/login-page?redirectUri=" + redirectUri);}// 3. 验证JWT(子系统调用此接口验证Token)@GetMapping("/validate-token")public Map<String, Object> validateToken(@RequestParam String token) {return JwtUtils.validateToken(token);}
}
4.2 子系统整合 SSO(简化版)
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;@RestController
@RequestMapping("/subsystem")
public class SubsystemController {// SSO认证中心地址private static final String SSO_AUTH_URL = "http://localhost:8080/sso";// 子系统回调地址private static final String SUB_CALLBACK_URL = "http://localhost:8081/subsystem/callback";// 1. 子系统业务接口(需SSO认证)@GetMapping("/order")public void getOrder(HttpServletRequest request, HttpServletResponse response) throws IOException {// 检查是否已获取JWTString token = (String) request.getAttribute("token");if (token == null) {// 未认证,重定向到SSO登录页response.sendRedirect(SSO_AUTH_URL + "/login-page?redirectUri=" + SUB_CALLBACK_URL);return;}// 已认证,返回业务数据response.getWriter().write("子系统订单列表(已认证)");}// 2. 回调接口(接收SSO返回的JWT)@GetMapping("/callback")public void callback(@RequestParam String token, HttpServletRequest request, HttpServletResponse response) throws IOException {// 调用SSO接口验证JWTMap<String, Object> userInfo = restTemplate.getForObject(SSO_AUTH_URL + "/validate-token?token=" + token,Map.class);if (userInfo == null) {// 验证失败,重定向到登录页response.sendRedirect(SSO_AUTH_URL + "/login-page?redirectUri=" + SUB_CALLBACK_URL);return;}// 验证成功,存储JWT并跳转到业务接口request.setAttribute("token", token);response.sendRedirect("/subsystem/order");}
}
四、选型指南:不同场景下的技术选择
项目架构 | 推荐技术组合 | 理由 |
---|---|---|
单体应用(如管理系统) | Session + Cookie | 开发简单,无需分布式适配,Session 维护状态便捷 |
前后端分离(Vue+Spring Boot) | JWT + localStorage | 无状态,适配前端独立部署,避免 Cookie 的 CSRF 风险 |
微服务架构 | JWT + API 网关 | 网关统一验证 JWT,微服务无状态,扩展性强 |
企业多系统 | SSO(基于 JWT)+ 子系统验证 | 统一认证入口,避免多系统重复登录,JWT 简化子系统验证流程 |
开放平台(第三方 API) | JWT + 刷新 Token | 无状态便于第三方接入,刷新 Token 避免频繁登录 |
五、总结
Cookie、Session、JWT、SSO 并非互斥关系,而是不同层级的技术:
- Cookie 是基础载体:用于传递 SessionID 或 JWT,是客户端状态存储的最小单位;
- Session 是服务器状态:单体应用的首选,需 Cookie 配合,分布式需共享存储;
- JWT 是无状态凭证:替代 Session 的分布式方案,客户端存储,服务器仅验证;
- SSO 是流程整合:基于 Session 或 JWT,解决多系统认证统一问题,是更高层的架构设计。
掌握它们的核心区别和适用场景,才能在实际项目中选择最合适的技术组合,既保证系统安全,又兼顾开发效率和扩展性。
有些网站登陆过期时间是几小时,而有些却是好几天,手机应用登陆如b站则是长久保持,即使关机也不用重新登陆。这些是使用什么方案实现的?
2.网站与 APP 登录持久化方案:从几小时到永久登录的实现揭秘
在日常使用软件时,我们常会发现登录过期策略差异巨大:网站登录可能几小时失效,B 站等 APP 却能 “永久登录”(即使关机也无需重新登录)。这些差异并非简单的 “过期时间设置”,而是基于用户体验、安全风险、设备特性设计的不同技术方案。本文将拆解从 “短期登录” 到 “永久登录” 的实现原理,结合实战代码,解析背后的技术选型逻辑。
一、核心问题:登录持久化的本质是什么?
登录持久化的核心是如何安全地存储用户身份凭证,并在凭证有效期内免密恢复登录状态。无论过期时间是几小时还是几年,技术方案都围绕以下 3 个目标设计:
- 安全性:防止凭证被窃取、篡改,避免账号被盗;
- 可用性:凭证能跨会话(如浏览器重启、APP 重装)恢复,无需频繁登录;
- 灵活性:支持按需失效(如用户登出、账号异常时强制失效)。
不同软件的过期策略差异,本质是安全与用户体验的权衡:
- 短期登录(几小时):金融、支付类软件(如网银、支付宝),优先保障安全;
- 长期登录(几天到永久):内容、社交类 APP(如 B 站、微信),优先提升用户体验。
二、短期登录(几小时):网站的主流方案
网站(尤其是 PC 端)登录过期时间通常较短(1-24 小时),核心方案是Session-Cookie或短期 JWT+Cookie,兼顾安全性和临时会话需求。
1. 方案 1:Session-Cookie(单体网站首选)
原理
- 服务器存储用户会话(Session),通过 Cookie 将
SessionID
下发到客户端; - 客户端请求时自动携带
SessionID
,服务器验证 Session 有效性; - 过期策略:Session 设置短期有效期(如 2 小时),超时后服务器销毁 Session,用户需重新登录。
实战代码(Spring Boot)
import javax.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
public class ShortSessionLoginController {@PostMapping("/login")public String login(@RequestParam String username,@RequestParam String password,HttpSession session) {// 1. 模拟账号密码校验(实际需查数据库)if ("user123".equals(username) && "pwd123".equals(password)) {// 2. 存储用户信息到Sessionsession.setAttribute("userId", 1001L);session.setAttribute("username", username);// 3. 设置Session过期时间:2小时(单位:秒)session.setMaxInactiveInterval(2 * 60 * 60);return "登录成功,SessionID:" + session.getId();}throw new RuntimeException("账号或密码错误");}// 业务接口:验证Session有效性@PostMapping("/user/info")public String getUserInfo(HttpSession session) {Long userId = (Long) session.getAttribute("userId");if (userId == null) {throw new RuntimeException("登录已过期,请重新登录");}return "用户ID:" + userId + ",登录状态有效";}
}
安全增强:Cookie 配置(防窃取、防篡改)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.ServletContext;@Configuration
public class CookieConfig {@Beanpublic ServletContextInitializer servletContextInitializer() {return servletContext -> {// 配置Session对应的Cookie属性ServletContext.SessionCookieConfig cookieConfig = servletContext.getSessionCookieConfig();cookieConfig.setName("JSESSIONID");cookieConfig.setHttpOnly(true); // 禁止JS访问,防XSS窃取cookieConfig.setSecure(true); // 仅HTTPS传递,防中间人攻击cookieConfig.setSameSite("Lax");// 限制跨站携带,防CSRFcookieConfig.setMaxAge(2 * 60 * 60); // 与Session过期时间一致};}
}
适用场景
- 单体网站(如企业管理系统、论坛);
- 对安全性要求较高,且用户无需长期登录的场景(如电商 PC 端、网银)。
2. 方案 2:短期 JWT+Cookie(分布式网站)
原理
- 服务器验证用户身份后生成短期 JWT(如 2 小时),通过 Cookie 下发;
- 客户端请求时携带 Cookie 中的 JWT,服务器验证签名和过期时间;
- 过期策略:JWT 自身携带
exp
(过期时间)字段,超时后客户端需重新登录。
实战代码(JWT 工具类 + 登录接口)
// JWT工具类(核心方法)
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 ShortJwtUtils {// 密钥(生产环境存配置中心)private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor("your-32-byte-secret-key-here".getBytes());// 过期时间:2小时private static final long EXPIRATION = 2 * 60 * 60 * 1000;// 生成短期JWTpublic static String generateShortToken(Long userId, String username) {Map<String, Object> claims = new HashMap<>();claims.put("userId", userId);claims.put("username", username);return Jwts.builder().setClaims(claims).setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)).signWith(SECRET_KEY, SignatureAlgorithm.HS256).compact();}// 验证JWTpublic static Map<String, Object> validateToken(String token) {try {Jws<Claims> jws = Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token);Claims claims = jws.getBody();Map<String, Object> userInfo = new HashMap<>();userInfo.put("userId", claims.get("userId"));userInfo.put("username", claims.get("username"));return userInfo;} catch (ExpiredJwtException e) {throw new RuntimeException("登录已过期");} catch (SignatureException | MalformedJwtException e) {throw new RuntimeException("凭证无效");}}
}// 登录接口(下发短期JWT到Cookie)
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;@RestController
public class ShortJwtLoginController {@PostMapping("/jwt/login")public String login(@RequestParam String username,@RequestParam String password,HttpServletResponse response) {// 模拟校验if ("user123".equals(username) && "pwd123".equals(password)) {String jwt = ShortJwtUtils.generateShortToken(1001L, username);// 下发JWT到CookieCookie jwtCookie = new Cookie("short_jwt", jwt);jwtCookie.setMaxAge(2 * 60 * 60); // 2小时过期jwtCookie.setPath("/");jwtCookie.setHttpOnly(true);jwtCookie.setSecure(true);response.addCookie(jwtCookie);return "登录成功,短期JWT已下发";}throw new RuntimeException("账号或密码错误");}
}
适用场景
- 分布式网站(微服务架构);
- 无需长期登录,但需跨服务共享身份的场景(如多节点部署的电商平台)。
三、中长期登录(几天到几周):兼顾体验与安全的平衡方案
电商 APP、社交软件(如淘宝、微博)常设置几天到几周的登录过期时间,核心方案是 “短期访问凭证 + 长期刷新凭证”,既避免频繁登录,又降低长期凭证泄露的风险。
核心原理:双凭证机制
- 访问凭证(Access Token):短期有效(如 2 小时),用于接口调用,泄露风险低;
- 刷新凭证(Refresh Token):长期有效(如 7 天),仅用于获取新的 Access Token,泄露后可通过 “设备绑定” 限制风险;
- 流程:
- 用户登录时,服务器返回 Access Token 和 Refresh Token;
- Access Token 过期后,客户端用 Refresh Token 向服务器申请新的 Access Token;
- 若 Refresh Token 也过期,用户需重新登录。
实战代码(双凭证登录 + 刷新接口)
// 1. 凭证实体类
import lombok.Data;@Data
public class TokenPair {private String accessToken; // 访问凭证(2小时)private String refreshToken; // 刷新凭证(7天)private long accessExpire; // 访问凭证过期时间(时间戳)private long refreshExpire; // 刷新凭证过期时间(时间戳)
}// 2. 双凭证工具类
public class DoubleTokenUtils {// 访问凭证密钥private static final SecretKey ACCESS_KEY = Keys.hmacShaKeyFor("access-key-32-byte-secret".getBytes());// 刷新凭证密钥(与访问凭证不同,提升安全性)private static final SecretKey REFRESH_KEY = Keys.hmacShaKeyFor("refresh-key-32-byte-secret".getBytes());// 访问凭证过期时间:2小时private static final long ACCESS_EXP = 2 * 60 * 60 * 1000;// 刷新凭证过期时间:7天private static final long REFRESH_EXP = 7 * 24 * 60 * 60 * 1000;// 生成双凭证public static TokenPair generateTokenPair(Long userId, String deviceId) {long now = System.currentTimeMillis();// 生成Access Token(包含用户ID)String accessToken = Jwts.builder().claim("userId", userId).setExpiration(new Date(now + ACCESS_EXP)).signWith(ACCESS_KEY, SignatureAlgorithm.HS256).compact();// 生成Refresh Token(包含用户ID+设备ID,绑定设备防泄露)String refreshToken = Jwts.builder().claim("userId", userId).claim("deviceId", deviceId) // 绑定设备,仅该设备可使用.setExpiration(new Date(now + REFRESH_EXP)).signWith(REFRESH_KEY, SignatureAlgorithm.HS256).compact();TokenPair pair = new TokenPair();pair.setAccessToken(accessToken);pair.setRefreshToken(refreshToken);pair.setAccessExpire(now + ACCESS_EXP);pair.setRefreshExpire(now + REFRESH_EXP);return pair;}// 验证Access Tokenpublic static Map<String, Object> validateAccessToken(String token) {try {Jws<Claims> jws = Jwts.parserBuilder().setSigningKey(ACCESS_KEY).build().parseClaimsJws(token);Claims claims = jws.getBody();Map<String, Object> info = new HashMap<>();info.put("userId", claims.get("userId"));return info;} catch (ExpiredJwtException e) {throw new RuntimeException("访问凭证已过期,请刷新");}}// 用Refresh Token获取新的Access Tokenpublic static String refreshAccessToken(String refreshToken, String deviceId) {try {Jws<Claims> jws = Jwts.parserBuilder().setSigningKey(REFRESH_KEY).build().parseClaimsJws(refreshToken);Claims claims = jws.getBody();// 验证设备ID(防止Refresh Token泄露到其他设备)String storedDeviceId = claims.get("deviceId").toString();if (!deviceId.equals(storedDeviceId)) {throw new RuntimeException("设备不匹配,刷新失败");}// 生成新的Access TokenLong userId = Long.valueOf(claims.get("userId").toString());return generateTokenPair(userId, deviceId).getAccessToken();} catch (ExpiredJwtException e) {throw new RuntimeException("刷新凭证已过期,请重新登录");}}
}// 3. 登录接口(返回双凭证)
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
public class DoubleTokenLoginController {@PostMapping("/double/login")public TokenPair login(@RequestParam String username,@RequestParam String password,@RequestParam String deviceId) { // 客户端传递设备唯一标识(如手机IMEI)// 模拟校验if ("user123".equals(username) && "pwd123".equals(password)) {return DoubleTokenUtils.generateTokenPair(1001L, deviceId);}throw new RuntimeException("账号或密码错误");}// 刷新Access Token接口@PostMapping("/token/refresh")public String refreshToken(@RequestParam String refreshToken,@RequestParam String deviceId) {return DoubleTokenUtils.refreshAccessToken(refreshToken, deviceId);}
}
关键安全设计
- 设备绑定:Refresh Token 包含设备 ID,仅绑定的设备可使用,即使泄露也无法在其他设备生效;
- 密钥分离:Access Token 和 Refresh Token 使用不同密钥,降低单一密钥泄露的风险;
- 刷新限制:可对 Refresh Token 设置 “最大刷新次数”(如 7 天内最多刷新 3 次),异常刷新时强制失效。
适用场景
- 移动 APP(如电商、社交软件);
- 需中长期登录,但又需控制安全风险的场景(如淘宝、微博,过期时间 7-30 天)。
四、永久登录(如 B 站 APP):极致用户体验的方案
B 站、微信等 APP 的 “永久登录” 并非真的 “永久”,而是通过 “长期凭证 + 设备绑定 + 静默刷新” 实现 “用户无感知的持久登录”,核心是在安全可控的前提下最大化用户体验。
核心原理:三层保障的持久化方案
- 长期 Refresh Token:设置超长有效期(如 1 年),存储在 APP 的 “安全存储区”(如 Android 的 Keystore、iOS 的 Keychain),避免被窃取;
- 设备指纹绑定:生成设备唯一指纹(如手机 IMEI + 系统版本 + APP 版本),与 Refresh Token 绑定,仅该设备可使用;
- 静默刷新机制:APP 启动时检查 Access Token 是否过期,若过期则自动用 Refresh Token 刷新,用户无感知;
- 强制失效机制:服务器端维护 “黑名单”,若用户登出、账号异常(如异地登录),立即将 Refresh Token 加入黑名单,强制失效。
实战代码(永久登录核心逻辑)
// 1. 设备指纹工具类(生成唯一设备标识)
import java.security.MessageDigest;
import java.util.UUID;public class DeviceFingerprintUtils {// 生成设备指纹(结合设备硬件信息+系统信息)public static String generateFingerprint(String imei, String osVersion, String appVersion) {try {// 拼接设备信息String raw = imei + "_" + osVersion + "_" + appVersion + "_" + UUID.randomUUID().toString();// MD5哈希生成唯一指纹MessageDigest md = MessageDigest.getInstance("MD5");byte[] digest = md.digest(raw.getBytes());StringBuilder sb = new StringBuilder();for (byte b : digest) {sb.append(String.format("%02x", b));}return sb.toString();} catch (Exception e) {// 异常时返回UUID(降级方案)return UUID.randomUUID().toString();}}
}// 2. 永久登录工具类(核心逻辑)
public class PermanentLoginUtils {// 刷新凭证过期时间:1年private static final long PERMANENT_REFRESH_EXP = 365 * 24 * 60 * 60 * 1000;// 服务器端黑名单(Redis存储,key=refreshToken,value=失效时间)private static final RedisTemplate<String, Long> redisTemplate = new RedisTemplate<>();// 生成永久登录凭证(Access Token+长期Refresh Token)public static TokenPair generatePermanentToken(Long userId, String deviceFingerprint) {long now = System.currentTimeMillis();// Access Token(2小时,同前)String accessToken = generateAccessToken(userId);// 长期Refresh Token(1年,绑定设备指纹)String refreshToken = Jwts.builder().claim("userId", userId).claim("fingerprint", deviceFingerprint).setExpiration(new Date(now + PERMANENT_REFRESH_EXP)).signWith(REFRESH_KEY, SignatureAlgorithm.HS256).compact();// 存储Refresh Token到Redis(便于后续强制失效)redisTemplate.opsForValue().set("refresh_token:" + refreshToken,userId,PERMANENT_REFRESH_EXP,TimeUnit.MILLISECONDS);TokenPair pair = new TokenPair();pair.setAccessToken(accessToken);pair.setRefreshToken(refreshToken);pair.setAccessExpire(now + 2 * 60 * 60 * 1000);pair.setRefreshExpire(now + PERMANENT_REFRESH_EXP);return pair;}// 验证Refresh Token(含黑名单校验)public static String refreshPermanentToken(String refreshToken, String deviceFingerprint) {// 1. 先检查是否在黑名单if (redisTemplate.hasKey("blacklist:" + refreshToken)) {throw new RuntimeException("登录已失效,请重新登录");}try {Jws<Claims> jws = Jwts.parserBuilder().setSigningKey(REFRESH_KEY).build().parseClaimsJws(refreshToken);Claims claims = jws.getBody();// 2. 验证设备指纹String storedFingerprint = claims.get("fingerprint").toString();if (!deviceFingerprint.equals(storedFingerprint)) {// 设备不匹配,加入黑名单redisTemplate.opsForValue().set("blacklist:" + refreshToken,1L,365 * 24 * 60 * 60 * 1000,TimeUnit.MILLISECONDS);throw new RuntimeException("设备异常,登录已失效");}// 3. 生成新的Access TokenLong userId = Long.valueOf(claims.get("userId").toString());return generateAccessToken(userId);} catch (ExpiredJwtException e) {throw new RuntimeException("登录已过期,请重新登录");}}// 强制失效(登出、账号异常时调用)public static void invalidateToken(String refreshToken) {// 加入黑名单redisTemplate.opsForValue().set("blacklist:" + refreshToken,1L,365 * 24 * 60 * 60 * 1000,TimeUnit.MILLISECONDS);// 删除Redis中的有效凭证redisTemplate.delete("refresh_token:" + refreshToken);}// 生成Access Token(复用前序逻辑)private static String generateAccessToken(Long userId) {return Jwts.builder().claim("userId", userId).setExpiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)).signWith(ACCESS_KEY, SignatureAlgorithm.HS256).compact();}
}// 3. APP客户端静默刷新逻辑(伪代码)
public class AppLoginManager {private String refreshToken;private String deviceFingerprint;private String accessToken;// APP启动时调用:检查并刷新凭证public void checkAndRefreshToken() {if (isAccessTokenExpired()) {// 静默刷新Access Tokentry {accessToken = PermanentLoginUtils.refreshPermanentToken(refreshToken, deviceFingerprint);saveTokenToSecureStorage(); // 保存到安全存储区(如Keystore)} catch (Exception e) {// 刷新失败,跳转登录页jumpToLoginPage();}}}// 检查Access Token是否过期private boolean isAccessTokenExpired() {// 从安全存储区读取accessToken的过期时间,判断是否过期return System.currentTimeMillis() > getAccessTokenExpireTime();}// 保存凭证到安全存储区(避免被窃取)private void saveTokenToSecureStorage() {// Android:使用Keystore存储;iOS:使用Keychain存储}
}
关键技术细节
- 安全存储:凭证不存储在
SharedPreferences
或NSUserDefaults
(易被 root / 越狱设备读取),而是存储在系统级安全区域(Keystore/Keychain); - 设备指纹稳定性:结合硬件信息(IMEI、MAC)和软件信息(系统版本、APP 版本),确保设备指纹在 “系统升级、APP 重装” 后仍能匹配;
- 异常检测:服务器监控 Refresh Token 的使用频率(如短时间内多 IP 调用),异常时强制失效,防止账号被盗。
适用场景
- 高频使用的 APP(如社交、内容类软件);
- 对用户体验要求极高,且能通过设备绑定、异常检测控制安全风险的场景(如 B 站、微信、QQ)。
五、总结:不同登录时长的技术选型对比
登录时长 | 核心方案 | 安全措施 | 适用场景 |
---|---|---|---|
几小时(短期) | Session-Cookie / 短期 JWT+Cookie | HttpOnly、Secure、SameSite、Session 超时 | 网站、网银、支付类软件 |
几天到几周 | 双凭证(Access Token+Refresh Token) | 设备绑定、密钥分离、刷新限制 | 电商 APP、微博、淘宝 |
长期 / 永久 | 长期 Refresh Token + 安全存储 + 静默刷新 | 安全存储(Keystore/Keychain)、设备指纹、黑名单 | 社交 APP、内容 APP(B 站、微信、QQ) |
选型核心逻辑
- 安全优先:金融、支付类软件优先选择短期登录,避免凭证泄露风险;
- 体验优先:高频使用的 APP 选择长期 / 永久登录,通过设备绑定、安全存储平衡风险;
- 分布式适配:多服务架构优先选择 JWT(无状态),单体架构可选择 Session-Cookie(开发简单)。
无论选择哪种方案,核心都是 “在安全与体验之间找到平衡点”—— 短期登录并非 “不安全”,长期登录也并非 “无风险”,关键在于通过技术手段将风险控制在可接受范围,同时最大化用户体验。
那么以上这些不用到缓存吗,缓存可以持久化吗
3.登录持久化与缓存:缓存的必要性、持久化方案与实战代码
在之前讨论的登录持久化方案中,缓存(如 Redis)其实是核心支撑组件 —— 无论是短期 Session 共享、双凭证的黑名单管理,还是长期登录的状态维护,都离不开缓存的参与。同时,缓存能否持久化直接决定了登录状态在服务重启后的可用性。本文将系统解答 “登录场景中为什么需要缓存”“缓存如何持久化”,并结合实战代码展示缓存的落地细节。
一、登录持久化为什么离不开缓存?
在单体应用中,短期 Session 可以存储在 JVM 内存中,但分布式场景、长期登录场景必须依赖缓存。缓存的核心价值体现在以下 4 个方面:
1. 分布式 Session 共享(解决 “多服务 Session 不一致”)
单体应用中,Session 存储在单个服务器内存,若扩展为多节点(如 2 台 Tomcat),用户请求可能被负载均衡到不同节点,导致 Session 丢失(用户需重复登录)。
缓存的作用:将 Session 集中存储在 Redis 等缓存中,所有服务节点通过缓存读写 Session,实现 “一处存储,多处共享”。
2. 双凭证的状态管理(控制 Refresh Token 的生命周期)
中长期登录的 “双凭证机制” 中,需要维护 3 类关键状态:
-
Refresh Token 的有效性(是否已过期);
-
黑名单(登出、账号异常的 Token 需立即失效);
-
设备绑定关系(防止 Token 在其他设备使用)。
缓存的作用:用 Redis 存储这些状态,支持快速查询(如判断 Token 是否在黑名单)和过期自动清理(无需手动维护过期逻辑)。
3. 减轻数据库压力(避免高频查询)
登录场景中,“验证 Token 有效性”“查询用户权限” 是高频操作(如每一次接口调用都需验证 Token)。若直接查询数据库,会导致数据库压力激增。
缓存的作用:将高频访问的 Token、用户权限等数据缓存到 Redis,查询耗时从 “毫秒级(数据库)” 降至 “微秒级(缓存)”,大幅提升性能。
4. 长期登录的状态恢复(服务重启后不丢失登录状态)
若登录状态存储在 JVM 内存,服务重启后状态会全部丢失,用户需重新登录。
缓存的作用:缓存(如 Redis)支持持久化,服务重启后可从缓存恢复登录状态,避免用户感知服务重启。
二、缓存的持久化:什么是持久化?有哪些方案?
缓存的 “持久化” 是指将缓存中的数据(如 Session、Token、黑名单)写入磁盘,确保服务重启、缓存实例宕机后数据不丢失。不同缓存组件的持久化方案不同,以主流的Redis为例,核心持久化方案有两种:RDB 和 AOF。
1. Redis 持久化方案对比
方案 | 核心原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
RDB(快照) | 按配置的时间间隔(如 5 分钟)生成内存数据的快照文件(.rdb),写入磁盘 | 1. 文件体积小,恢复速度快;2. 对 Redis 性能影响小 | 1. 可能丢失 “快照间隔内” 的数据;2. 大内存场景下生成快照耗时 | 对数据一致性要求不高的场景(如 Session、非核心 Token) |
AOF(日志) | 记录每一条写操作命令(如 SET、DEL)到日志文件(.aof),重启时重新执行命令恢复数据 | 1. 数据一致性高(可配置 “每写必刷盘”);2. 无数据丢失风险 | 1. 日志文件体积大;2. 恢复速度慢 | 对数据一致性要求高的场景(如黑名单、长期 Refresh Token) |
2. 生产环境推荐方案:RDB+AOF 混合持久化
Redis 4.0 + 支持 “RDB+AOF 混合持久化”,结合两种方案的优点:
- 持久化时:先生成 RDB 快照,再将后续的写命令追加到 AOF 日志;
- 恢复时:先加载 RDB 快照(快速恢复大部分数据),再执行 AOF 日志中的增量命令(补全快照后的新数据);
- 优势:兼顾 “恢复速度” 和 “数据一致性”,是登录持久化场景的首选。
三、缓存持久化的实战配置(Redis)
以 Redis 6.0 为例,通过配置文件开启混合持久化,确保登录相关数据不丢失。
1. Redis 配置文件(redis.conf)关键配置
ini
# -------------------------- RDB持久化配置 --------------------------
# 900秒内有1个key变化则生成快照
save 900 1
# 300秒内有10个key变化则生成快照
save 300 10
# 60秒内有10000个key变化则生成快照
save 60 10000
# 快照文件存储路径(默认当前目录)
dir /var/lib/redis
# 快照文件名
dbfilename dump.rdb
# 生成快照失败时,是否停止Redis写操作(防止数据不一致)
stop-writes-on-bgsave-error yes# -------------------------- AOF持久化配置 --------------------------
# 开启AOF持久化
appendonly yes
# AOF文件名
appendfilename "appendonly.aof"
# AOF刷盘策略(everysec:每秒刷盘,平衡性能和一致性)
appendfsync everysec
# 开启混合持久化(Redis 4.0+支持)
aof-use-rdb-preamble yes
# AOF日志重写触发条件(避免日志文件过大)
auto-aof-rewrite-percentage 100 # 当前AOF文件是上次重写的2倍时触发
auto-aof-rewrite-min-size 64mb # AOF文件超过64MB时触发重写
2. 配置生效与验证
- 重启 Redis:
systemctl restart redis
(Linux)或redis-server redis.conf
(Windows); - 验证持久化开启:
- 执行
redis-cli config get appendonly
,返回1
表示 AOF 已开启; - 执行
redis-cli config get aof-use-rdb-preamble
,返回yes
表示混合持久化已开启;
- 执行
- 验证数据持久化:
- 执行
set user:1001 "admin"
写入数据; - 重启 Redis 后执行
get user:1001
,若返回"admin"
,说明数据已持久化。
- 执行
四、缓存在登录持久化中的实战代码
以下结合之前的登录方案,展示缓存(Redis)的具体应用,包括分布式 Session、双凭证管理、长期登录的黑名单控制。
1. 实战 1:分布式 Session(基于 Redis 缓存)
核心需求
多服务节点共享 Session,服务重启后 Session 不丢失,Session 过期时间 2 小时。
实战代码(Spring Boot + Spring Session + Redis)
1.1 引入依赖
<!-- Spring Session(整合Redis) -->
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- Redis客户端 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1.2 配置 Redis 与 Session
spring:# Redis配置(连接信息、序列化方式)redis:host: localhostport: 6379password: 123456lettuce:pool:max-active: 8 # 最大连接数# Spring Session配置(Redis存储)session:store-type: redis # Session存储到Redisredis:namespace: spring:session # Redis中Session的key前缀max-inactive-interval: 7200 # Session过期时间:2小时(秒)cookie:http-only: true # 防XSSsecure: true # 仅HTTPS传递same-site: Lax # 防CSRF
1.3 启用分布式 Session
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.stereotype.Component;// 启用Redis存储Session,过期时间与配置一致
@Component
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 7200)
public class RedisSessionConfig {
}
1.4 登录接口(Session 自动存储到 Redis)
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;@RestController
public class DistributedSessionLoginController {@PostMapping("/distributed/login")public String login(@RequestParam String username,@RequestParam String password,HttpSession session) {// 1. 模拟账号密码校验if ("admin".equals(username) && "123456".equals(password)) {// 2. 存储用户信息到Session(自动同步到Redis)session.setAttribute("userId", 1001L);session.setAttribute("username", username);// 3. 返回SessionID(Redis中的key前缀为spring:session:sessions:xxx)return "分布式登录成功,SessionID:" + session.getId();}throw new RuntimeException("账号或密码错误");}// 业务接口(从Redis读取Session)@PostMapping("/distributed/user/info")public String getUserInfo(HttpSession session) {Long userId = (Long) session.getAttribute("userId");if (userId == null) {throw new RuntimeException("登录已过期,请重新登录");}return "用户ID:" + userId + ",Session存储在Redis,多服务共享";}
}
1.5 缓存持久化效果
- 服务重启后,用户无需重新登录:Session 存储在 Redis,Redis 开启 RDB+AOF 持久化,服务重启后从 Redis 恢复 Session;
- 多服务节点共享:2 台 Tomcat 节点均从 Redis 读写 Session,用户请求到任意节点都能识别登录状态。
2. 实战 2:双凭证的黑名单管理(Redis 缓存)
核心需求
- Refresh Token 过期时间 7 天,存储在 Redis;
- 用户登出或账号异常时,将 Refresh Token 加入黑名单,立即失效;
- 黑名单数据需持久化,服务重启后仍能识别失效 Token。
实战代码
2.1 Redis 工具类(封装常用操作)
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;@Component
public class RedisUtils {@Resourceprivate RedisTemplate<String, Object> redisTemplate;// 存储数据(带过期时间)public void set(String key, Object value, long timeout, TimeUnit unit) {redisTemplate.opsForValue().set(key, value, timeout, unit);}// 获取数据public Object get(String key) {return redisTemplate.opsForValue().get(key);}// 判断key是否存在public boolean hasKey(String key) {return Boolean.TRUE.equals(redisTemplate.hasKey(key));}// 删除数据public void delete(String key) {redisTemplate.delete(key);}
}
2.2 双凭证管理(结合 Redis)
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;@Component
public class DoubleTokenManager {// 密钥(生产环境存配置中心)private static final SecretKey ACCESS_KEY = Keys.hmacShaKeyFor("access-key-32-byte-secret-123456".getBytes());private static final SecretKey REFRESH_KEY = Keys.hmacShaKeyFor("refresh-key-32-byte-secret-654321".getBytes());// 过期时间:Access Token 2小时,Refresh Token 7天private static final long ACCESS_EXP = 2 * 60 * 60 * 1000;private static final long REFRESH_EXP = 7 * 24 * 60 * 60 * 1000;// Redis key前缀private static final String REFRESH_TOKEN_KEY = "refresh_token:";private static final String BLACKLIST_KEY = "blacklist:";@Resourceprivate RedisUtils redisUtils;// 生成双凭证(存储Refresh Token到Redis)public TokenPair generateTokenPair(Long userId, String deviceId) {long now = System.currentTimeMillis();// 1. 生成Access TokenString accessToken = Jwts.builder().claim("userId", userId).setExpiration(new Date(now + ACCESS_EXP)).signWith(ACCESS_KEY, SignatureAlgorithm.HS256).compact();// 2. 生成Refresh Token(绑定设备ID)String refreshToken = Jwts.builder().claim("userId", userId).claim("deviceId", deviceId).setExpiration(new Date(now + REFRESH_EXP)).signWith(REFRESH_KEY, SignatureAlgorithm.HS256).compact();// 3. 存储Refresh Token到Redis(7天过期,与Token过期时间一致)redisUtils.set(REFRESH_TOKEN_KEY + refreshToken,userId,REFRESH_EXP,TimeUnit.MILLISECONDS);// 4. 封装返回TokenPair pair = new TokenPair();pair.setAccessToken(accessToken);pair.setRefreshToken(refreshToken);pair.setAccessExpire(now + ACCESS_EXP);pair.setRefreshExpire(now + REFRESH_EXP);return pair;}// 验证Refresh Token(检查是否在黑名单、设备是否匹配)public String refreshAccessToken(String refreshToken, String deviceId) {// 1. 先检查是否在黑名单if (redisUtils.hasKey(BLACKLIST_KEY + refreshToken)) {throw new RuntimeException("Token已失效,请重新登录");}// 2. 检查Redis中是否存在该Refresh Token(已过期会自动删除)Object userIdObj = redisUtils.get(REFRESH_TOKEN_KEY + refreshToken);if (userIdObj == null) {throw new RuntimeException("Refresh Token已过期");}Long userId = Long.valueOf(userIdObj.toString());try {// 3. 验证Token签名和设备IDJws<Claims> jws = Jwts.parserBuilder().setSignWith(REFRESH_KEY).build().parseClaimsJws(refreshToken);Claims claims = jws.getBody();String storedDeviceId = claims.get("deviceId").toString();if (!deviceId.equals(storedDeviceId)) {// 设备不匹配,加入黑名单addToBlacklist(refreshToken);throw new RuntimeException("设备异常,Token失效");}// 4. 生成新的Access Tokenreturn Jwts.builder().claim("userId", userId).setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXP)).signWith(ACCESS_KEY, SignatureAlgorithm.HS256).compact();} catch (Exception e) {addToBlacklist(refreshToken);throw new RuntimeException("Token无效");}}// 登出:将Token加入黑名单public void logout(String refreshToken) {addToBlacklist(refreshToken);// 删除Redis中的有效TokenredisUtils.delete(REFRESH_TOKEN_KEY + refreshToken);}// 加入黑名单(过期时间与Refresh Token一致,避免永久占用内存)private void addToBlacklist(String refreshToken) {redisUtils.set(BLACKLIST_KEY + refreshToken,"invalid",REFRESH_EXP,TimeUnit.MILLISECONDS);}
}
2.3 缓存持久化效果
- 服务重启后,黑名单仍有效:Redis 开启 AOF 持久化,黑名单数据写入 AOF 日志,服务重启后恢复;
- Token 过期自动清理:Redis 自动删除过期的 Refresh Token 和黑名单数据,无需手动维护。
3. 实战 3:长期登录的状态维护(Redis 缓存)
核心需求
B 站式 “永久登录”:
- Refresh Token 过期时间 1 年,存储在 Redis 并持久化;
- 服务重启后,用户无需重新登录;
- 账号异常时,可通过 Redis 黑名单强制失效 Token。
关键代码(基于实战 2 扩展)
// 长期登录的Token生成(过期时间1年)
public TokenPair generatePermanentToken(Long userId, String deviceFingerprint) {long now = System.currentTimeMillis();long permanentExp = 365 * 24 * 60 * 60 * 1000; // 1年过期// 1. 生成长期Refresh Token(绑定设备指纹)String refreshToken = Jwts.builder().claim("userId", userId).claim("fingerprint", deviceFingerprint).setExpiration(new Date(now + permanentExp)).signWith(REFRESH_KEY, SignatureAlgorithm.HS256).compact();// 2. 存储到Redis(1年过期,开启RDB+AOF持久化)redisUtils.set(REFRESH_TOKEN_KEY + refreshToken,userId + "_" + deviceFingerprint, // 存储设备指纹,避免后续查询JWTpermanentExp,TimeUnit.MILLISECONDS);// 3. 生成Access Token(2小时,同前)String accessToken = generateAccessToken(userId);TokenPair pair = new TokenPair();pair.setAccessToken(accessToken);pair.setRefreshToken(refreshToken);pair.setAccessExpire(now + 2 * 60 * 60 * 1000);pair.setRefreshExpire(now + permanentExp);return pair;
}// 验证长期Token(直接从Redis获取设备指纹,避免解析JWT,提升性能)
public String refreshPermanentToken(String refreshToken, String deviceFingerprint) {// 1. 检查黑名单if (redisUtils.hasKey(BLACKLIST_KEY + refreshToken)) {throw new RuntimeException("登录已失效");}// 2. 从Redis获取用户ID和设备指纹(避免解析JWT)String storedValue = (String) redisUtils.get(REFRESH_TOKEN_KEY + refreshToken);if (storedValue == null) {throw new RuntimeException("登录已过期");}String[] parts = storedValue.split("_");Long userId = Long.valueOf(parts[0]);String storedFingerprint = parts[1];// 3. 验证设备指纹if (!deviceFingerprint.equals(storedFingerprint)) {addToBlacklist(refreshToken);throw new RuntimeException("设备异常,登录失效");}// 4. 生成新的Access Tokenreturn generateAccessToken(userId);
}
持久化关键:
- Redis 开启混合持久化,1 年有效期的 Refresh Token 在服务重启后仍能从 Redis 恢复;
- 设备指纹存储在 Redis,避免每次验证都解析 JWT,兼顾性能和持久化。
五、总结:缓存与登录持久化的核心结论
1. 缓存是登录持久化的 “基础设施”
- 短期登录(分布式 Session):依赖 Redis 实现多服务共享;
- 中长期登录(双凭证):依赖 Redis 管理 Token 状态、黑名单;
- 长期登录(永久登录):依赖 Redis 持久化登录状态,服务重启不丢失。
2. 缓存必须持久化,否则登录状态不可靠
- 非持久化缓存(如内存缓存):服务重启、缓存宕机后登录状态全部丢失,用户需重新登录;
- 持久化方案:Redis 推荐 “RDB+AOF 混合持久化”,兼顾恢复速度和数据一致性;
- 注意点:缓存持久化需配置合理的刷盘策略(如 AOF 的
everysec
),避免影响 Redis 性能。
3. 缓存的选型建议
- 登录场景首选 Redis:支持持久化、过期自动清理、高并发读写,生态成熟;
- 避免使用本地缓存(如 Caffeine):仅适合单体、短期场景,不支持分布式共享和持久化。
通过缓存的合理使用和持久化配置,才能实现 “分布式共享、服务重启不丢失、高性能” 的登录持久化方案,这也是企业级应用的标准实践。
4.Java 后端核心技术体系总结:从并发编程到登录持久化与缓存
结合之前讨论的 AQS、JVM、设计模式、认证授权、登录持久化及缓存等核心技术,本文将从技术模块划分、核心原理、实战场景、关联关系四个维度进行系统总结,帮你构建完整的 Java 后端技术知识体系,明确各技术的定位与协同逻辑。
一、并发编程核心:AQS 与线程安全工具
并发编程是高并发系统的基础,核心围绕 “线程同步” 和 “线程安全” 展开,AQS 是整个体系的 “基础设施”。
1. AQS(抽象队列同步器)
- 核心定位:JUC 包的底层同步框架,通过 “状态变量
state
+CLH 变体队列” 实现线程排队与唤醒,支撑各类同步工具的实现。 - 核心组件:
state
:volatile 修饰的状态变量,自定义语义(如 ReentrantLock 的 “重入次数”、Semaphore 的 “许可证数量”);- CLH 队列:双向链表存储竞争失败的线程,支持自旋 + 阻塞的高效等待。
- 核心模式:
- 独占模式(ReentrantLock):同一时间仅一个线程获取资源,适合互斥场景;
- 共享模式(CountDownLatch、Semaphore):多个线程可同时获取资源,适合协作 / 限流场景。
- 子类实现:
- ReentrantLock:独占锁,支持公平 / 非公平模式,解决 synchronized 灵活性不足问题;
- CountDownLatch:倒计时器,主线程等待 N 个任务完成,适合任务汇总场景;
- Semaphore:信号量,控制并发访问线程数,适合接口限流 / 资源池控制。
2. 线程安全集合与工具
- ConcurrentHashMap:线程安全的 HashMap,JDK1.8 通过 “CAS + 节点级 synchronized” 替代 1.7 的 Segment 分段锁,锁粒度更细,并发效率更高,适合高并发缓存场景;
- ThreadLocal:线程私有变量容器,通过 “Thread 的 ThreadLocalMap” 存储变量,避免线程安全问题,适合链路追踪 ID、请求上下文传递;
- 线程池:通过 “核心线程 + 任务队列 + 非核心线程” 复用线程,控制并发强度,核心参数(corePoolSize、maximumPoolSize、workQueue)需结合任务类型(CPU 密集 / IO 密集)配置,避免线程爆炸或资源浪费。
二、JVM 核心:内存结构与垃圾回收
JVM 是 Java 程序的运行基石,核心解决 “内存管理” 和 “性能优化” 问题,直接影响系统稳定性与吞吐量。
1. 运行数据区(内存布局)
- 线程共享区域:
- 堆:存储对象实例 / 数组,分新生代(Eden+S0+S1,8:1:1)和老年代(2:1),垃圾回收的主要场所;
- 方法区(元空间):存储类信息、静态变量、运行时常量池,JDK8 用本地内存实现,避免永久代 OOM。
- 线程私有区域:
- 程序计数器:存储当前线程执行的字节码地址,唯一不抛 OOM 的区域;
- 虚拟机栈:存储方法栈帧(局部变量表、操作数栈),栈帧过多 / 过大导致 StackOverflowError;
- 本地方法栈:为 Native 方法服务,结构与虚拟机栈类似。
2. 类加载与垃圾回收
- 类加载机制:
- 流程:加载→验证→准备→解析→初始化→使用→卸载,核心是 “双亲委派模型”(父加载器优先加载,避免类重复与核心 API 篡改);
- 类加载器:启动类加载器(加载 JRE 核心类)→扩展类加载器(加载 ext 目录)→应用类加载器(加载 ClassPath)→自定义类加载器(灵活加载非标准路径类)。
- 垃圾回收(GC):
- 垃圾判断:可达性分析算法(以 GC Roots 为起点,不可达对象标记为垃圾);
- 回收算法:标记 - 清除(老年代,有碎片)、复制(新生代,无碎片)、标记 - 整理(老年代,无碎片);
- 回收器:G1(JDK9 + 默认,分区回收,兼顾吞吐与响应)、CMS(并发标记清除,低延迟)、Parallel(吞吐量优先)。
三、设计模式:代码设计的 “最佳实践”
设计模式是解决共性业务场景的成熟方案,核心目标是 “解耦、复用、可扩展”,高频模式集中在创建型和行为型。
1. 工厂模式(创建型)
- 核心定位:解耦对象创建与使用,避免硬编码 new 导致的耦合。
- 三种实现:
- 简单工厂:一个工厂创建所有产品,适合产品少、变化少场景(如工具类);
- 工厂方法:一个产品对应一个工厂,符合开闭原则,适合产品多、变化频繁场景(如支付方式创建);
- 抽象工厂:创建多维度产品族(如 “美式风味” 包含咖啡 + 甜点),适合多产品配套场景。
2. 策略模式(行为型)
- 核心定位:封装不同算法 / 行为,消除冗长 if-else,支持动态切换。
- 核心组件:抽象策略(定义接口)→具体策略(实现算法)→环境类(持有策略引用,统一调用);
- 实战场景:多方式登录(账号密码 / 短信 / 微信)、支付方式(支付宝 / 微信 / 银行卡)、优惠规则(满减 / 折扣)。
3. 责任链模式(行为型)
- 核心定位:将请求处理者连成链,请求沿链传递,避免请求发送者与多处理者耦合;
- 实战场景:订单流程(参数校验→数据填充→价格计算→落库)、过滤器(SpringMVC Filter)、审批流程(组长→主管→总裁)。
四、认证授权与登录持久化:系统安全的核心
认证授权是系统安全的入口,登录持久化是用户体验的关键,二者协同保障 “正确的人访问正确的资源”。
1. 认证与授权基础
- 认证(Authentication):验证用户身份(“你是谁”),核心是身份凭证(SessionID、JWT);
- 授权(Authorization):分配资源访问权限(“你能做什么”),核心是 RBAC 模型(用户→角色→权限);
- 核心框架:Spring Security(与 Spring 生态无缝整合,支持 OAuth2.0、RBAC)、Shiro(轻量,适合中小型项目)。
2. 登录持久化方案
登录持久化的核心是 “安全存储身份凭证”,不同场景对应不同方案,缓存是关键支撑:
- 短期登录(几小时):
- 方案:Session-Cookie(单体)、短期 JWT+Cookie(分布式);
- 缓存作用:分布式场景用 Redis 存储 Session,解决多服务 Session 不一致。
- 中长期登录(几天到几周):
- 方案:双凭证机制(Access Token 短期 + Refresh Token 长期);
- 缓存作用:Redis 存储 Refresh Token 有效性、黑名单、设备绑定关系,支持快速校验与过期自动清理。
- 长期登录(永久,如 B 站):
- 方案:长期 Refresh Token + 安全存储(Android Keystore/iOS Keychain)+ 静默刷新;
- 缓存作用:Redis 持久化存储 Token 状态,服务重启后不丢失登录状态,支持黑名单强制失效。
五、缓存:高并发系统的 “性能加速器”
缓存是高并发系统的核心组件,支撑登录持久化、数据查询等高频场景,持久化是缓存可靠性的关键。
1. 缓存的核心价值
- 性能提升:将高频访问数据(如用户信息、Token 状态)从数据库移到缓存(Redis),查询耗时从毫秒级降至微秒级;
- 分布式协同:实现 Session 共享、Token 状态同步,支撑多服务架构;
- 解耦数据库:减少数据库高频查询压力,避免数据库成为性能瓶颈。
2. 缓存持久化与选型
- 主流缓存:Redis(支持持久化、高并发、丰富数据结构,是登录 / 业务缓存的首选);
- 持久化方案:
- RDB:按间隔生成内存快照,文件小、恢复快,适合非核心数据;
- AOF:记录所有写操作,数据一致性高,适合核心数据(如黑名单);
- 混合持久化(Redis 4.0+):RDB+AOF 结合,兼顾恢复速度与数据安全性;
- 实战场景:
- 分布式 Session:Redis 存储 Session,多服务共享;
- Token 黑名单:Redis 存储登出 / 异常 Token,支持快速校验;
- 业务缓存:存储热点数据(如商品详情),减少数据库访问。
六、技术关联关系:各模块如何协同工作
Java 后端技术并非孤立存在,而是相互协同支撑业务场景,以 “高并发电商订单系统” 为例:
- 并发控制:用 ReentrantLock 保证订单状态修改的原子性,用 Semaphore 控制下单接口并发量;
- 线程管理:用线程池处理订单异步任务(如消息推送、日志记录),避免线程频繁创建;
- 内存管理:JVM 调优(设置堆大小、选择 G1 回收器)避免 OOM,保证系统稳定;
- 代码设计:用工厂模式创建不同类型订单(普通订单 / 秒杀订单),用责任链处理订单流程(校验→计算→落库);
- 认证授权:用 JWT 实现用户登录,Spring Security 控制订单接口权限(仅登录用户可下单);
- 登录持久化:用 Redis 存储 Refresh Token,支持 7 天免登录,服务重启后状态不丢失;
- 性能优化:用 Redis 缓存商品库存、用户信息,减少数据库查询,提升下单响应速度。
七、总结:Java 后端技术学习路径与核心原则
1. 学习路径
- 基础层:Java 语法→JVM(内存 / GC)→并发编程(AQS / 线程池);
- 框架层:Spring/Spring Boot→Spring Security→MyBatis;
- 架构层:设计模式→分布式(微服务 / 认证授权)→缓存(Redis);
- 实战层:问题排查(日志 / 监控)→性能调优(JVM / 缓存)→安全防护(XSS/CSRF)。
2. 核心原则
- 解耦优先:用设计模式、AQS、缓存等技术减少模块耦合,提升可扩展性;
- 安全与体验平衡:认证授权保证安全,登录持久化提升体验,缓存平衡性能与可靠性;
- 实战驱动:技术选型需结合业务场景(如金融用短期登录,社交 APP 用长期登录),避免过度设计;
- 问题导向:排查问题从日志→监控→工具(Arthas/JVM 工具),定位根因而非表面现象。
掌握这些核心技术与协同逻辑,不仅能应对面试中的各类问题,更能在实际项目中设计稳定、高效、可扩展的 Java 后端系统,从 “代码实现者” 成长为 “技术解决方案提供者”。