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

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)
SessionSessionID 泄露(CSRF)、服务器存储风险定期刷新 SessionID、限制 Session 有效期、Redis 存储防丢失
JWTPayload 可解码(非加密)、无法主动吊销不存敏感信息、短期 Token + 刷新 Token、维护黑名单(Redis)
SSO认证中心被攻击、Token 泄露HTTPS 加密、Token 短期有效、多因素认证(MFA)

3. 核心区别总结

对比维度CookieSessionJWTSSO
数据大小最大 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 并非互斥关系,而是不同层级的技术:

  1. Cookie 是基础载体:用于传递 SessionID 或 JWT,是客户端状态存储的最小单位;
  2. Session 是服务器状态:单体应用的首选,需 Cookie 配合,分布式需共享存储;
  3. JWT 是无状态凭证:替代 Session 的分布式方案,客户端存储,服务器仅验证;
  4. SSO 是流程整合:基于 Session 或 JWT,解决多系统认证统一问题,是更高层的架构设计。

掌握它们的核心区别和适用场景,才能在实际项目中选择最合适的技术组合,既保证系统安全,又兼顾开发效率和扩展性。

有些网站登陆过期时间是几小时,而有些却是好几天,手机应用登陆如b站则是长久保持,即使关机也不用重新登陆。这些是使用什么方案实现的?

2.网站与 APP 登录持久化方案:从几小时到永久登录的实现揭秘

在日常使用软件时,我们常会发现登录过期策略差异巨大:网站登录可能几小时失效,B 站等 APP 却能 “永久登录”(即使关机也无需重新登录)。这些差异并非简单的 “过期时间设置”,而是基于用户体验、安全风险、设备特性设计的不同技术方案。本文将拆解从 “短期登录” 到 “永久登录” 的实现原理,结合实战代码,解析背后的技术选型逻辑。

一、核心问题:登录持久化的本质是什么?

登录持久化的核心是如何安全地存储用户身份凭证,并在凭证有效期内免密恢复登录状态。无论过期时间是几小时还是几年,技术方案都围绕以下 3 个目标设计:

  1. 安全性:防止凭证被窃取、篡改,避免账号被盗;
  2. 可用性:凭证能跨会话(如浏览器重启、APP 重装)恢复,无需频繁登录;
  3. 灵活性:支持按需失效(如用户登出、账号异常时强制失效)。

不同软件的过期策略差异,本质是安全与用户体验的权衡

  • 短期登录(几小时):金融、支付类软件(如网银、支付宝),优先保障安全;
  • 长期登录(几天到永久):内容、社交类 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、社交软件(如淘宝、微博)常设置几天到几周的登录过期时间,核心方案是 “短期访问凭证 + 长期刷新凭证”,既避免频繁登录,又降低长期凭证泄露的风险。

核心原理:双凭证机制

  1. 访问凭证(Access Token):短期有效(如 2 小时),用于接口调用,泄露风险低;
  2. 刷新凭证(Refresh Token):长期有效(如 7 天),仅用于获取新的 Access Token,泄露后可通过 “设备绑定” 限制风险;
  3. 流程:
    • 用户登录时,服务器返回 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 的 “永久登录” 并非真的 “永久”,而是通过 “长期凭证 + 设备绑定 + 静默刷新” 实现 “用户无感知的持久登录”,核心是在安全可控的前提下最大化用户体验。

核心原理:三层保障的持久化方案

  1. 长期 Refresh Token:设置超长有效期(如 1 年),存储在 APP 的 “安全存储区”(如 Android 的 Keystore、iOS 的 Keychain),避免被窃取;
  2. 设备指纹绑定:生成设备唯一指纹(如手机 IMEI + 系统版本 + APP 版本),与 Refresh Token 绑定,仅该设备可使用;
  3. 静默刷新机制:APP 启动时检查 Access Token 是否过期,若过期则自动用 Refresh Token 刷新,用户无感知;
  4. 强制失效机制:服务器端维护 “黑名单”,若用户登出、账号异常(如异地登录),立即将 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存储}
}
关键技术细节
  • 安全存储:凭证不存储在SharedPreferencesNSUserDefaults(易被 root / 越狱设备读取),而是存储在系统级安全区域(Keystore/Keychain);
  • 设备指纹稳定性:结合硬件信息(IMEI、MAC)和软件信息(系统版本、APP 版本),确保设备指纹在 “系统升级、APP 重装” 后仍能匹配;
  • 异常检测:服务器监控 Refresh Token 的使用频率(如短时间内多 IP 调用),异常时强制失效,防止账号被盗。
适用场景
  • 高频使用的 APP(如社交、内容类软件);
  • 对用户体验要求极高,且能通过设备绑定、异常检测控制安全风险的场景(如 B 站、微信、QQ)。

五、总结:不同登录时长的技术选型对比

登录时长核心方案安全措施适用场景
几小时(短期)Session-Cookie / 短期 JWT+CookieHttpOnly、Secure、SameSite、Session 超时网站、网银、支付类软件
几天到几周双凭证(Access Token+Refresh Token)设备绑定、密钥分离、刷新限制电商 APP、微博、淘宝
长期 / 永久长期 Refresh Token + 安全存储 + 静默刷新安全存储(Keystore/Keychain)、设备指纹、黑名单社交 APP、内容 APP(B 站、微信、QQ)

选型核心逻辑

  1. 安全优先:金融、支付类软件优先选择短期登录,避免凭证泄露风险;
  2. 体验优先:高频使用的 APP 选择长期 / 永久登录,通过设备绑定、安全存储平衡风险;
  3. 分布式适配:多服务架构优先选择 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. 配置生效与验证

  1. 重启 Redissystemctl restart redis(Linux)或 redis-server redis.conf(Windows);
  2. 验证持久化开启:
    • 执行redis-cli config get appendonly,返回1表示 AOF 已开启;
    • 执行redis-cli config get aof-use-rdb-preamble,返回yes表示混合持久化已开启;
  3. 验证数据持久化:
    • 执行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 后端技术并非孤立存在,而是相互协同支撑业务场景,以 “高并发电商订单系统” 为例:

  1. 并发控制:用 ReentrantLock 保证订单状态修改的原子性,用 Semaphore 控制下单接口并发量;
  2. 线程管理:用线程池处理订单异步任务(如消息推送、日志记录),避免线程频繁创建;
  3. 内存管理:JVM 调优(设置堆大小、选择 G1 回收器)避免 OOM,保证系统稳定;
  4. 代码设计:用工厂模式创建不同类型订单(普通订单 / 秒杀订单),用责任链处理订单流程(校验→计算→落库);
  5. 认证授权:用 JWT 实现用户登录,Spring Security 控制订单接口权限(仅登录用户可下单);
  6. 登录持久化:用 Redis 存储 Refresh Token,支持 7 天免登录,服务重启后状态不丢失;
  7. 性能优化:用 Redis 缓存商品库存、用户信息,减少数据库查询,提升下单响应速度。

七、总结:Java 后端技术学习路径与核心原则

1. 学习路径

  • 基础层:Java 语法→JVM(内存 / GC)→并发编程(AQS / 线程池);
  • 框架层:Spring/Spring Boot→Spring Security→MyBatis;
  • 架构层:设计模式→分布式(微服务 / 认证授权)→缓存(Redis);
  • 实战层:问题排查(日志 / 监控)→性能调优(JVM / 缓存)→安全防护(XSS/CSRF)。

2. 核心原则

  • 解耦优先:用设计模式、AQS、缓存等技术减少模块耦合,提升可扩展性;
  • 安全与体验平衡:认证授权保证安全,登录持久化提升体验,缓存平衡性能与可靠性;
  • 实战驱动:技术选型需结合业务场景(如金融用短期登录,社交 APP 用长期登录),避免过度设计;
  • 问题导向:排查问题从日志→监控→工具(Arthas/JVM 工具),定位根因而非表面现象。

掌握这些核心技术与协同逻辑,不仅能应对面试中的各类问题,更能在实际项目中设计稳定、高效、可扩展的 Java 后端系统,从 “代码实现者” 成长为 “技术解决方案提供者”。

http://www.dtcms.com/a/488122.html

相关文章:

  • 营销网站制作皆选ls15227负责wordpress+边框插件
  • 视频网站开发需求分析外包网站开发公司
  • 个人网站制作基本步骤淄博网站备案公司
  • python单元测试 unittest.mock.patch (二)
  • 手机网站后台编辑器有哪些贵州建筑网站
  • 如果使用自己电脑做网站com是什么网站
  • 泛微 企业网站建设计划网站出现 503怎么了
  • 兰州移动端网站建设如何做好网站站内优化
  • PS基本教学(二)——认识PS软件各个基础模块以及PS基本设置
  • 大模型Agent智能体:开启人工智能新时代
  • 常备资料查询
  • 20251015给荣品的PRO-RK3566开发板在buildroot下打开ov5645【只配置编译了】
  • 淄博网站排名公司苏州网页关键词优化
  • 网站设置密码访问一建二建报考条件及专业要求
  • 青岛市住房城乡建设厅网站php网站建设个人总结
  • 濮阳网站关键词网站做下载wordpress
  • 上海站优云网络科技有限公司简单的网站怎么做
  • django网站开发实例pdfseo交流群
  • 重庆建网站搜索快忻科技html代码注释
  • 如何做公司o2o网站网站制作杭州
  • Python自定义容器完全指南:从基础实现到高级模式
  • 小程序做网站济南网站建设方案咨询
  • 介绍近期github上有名的开源项目
  • 相应式手机网站建设网站可不可以做自己的专利
  • 网站建设菜鸟教程模板建站合同
  • PyQt5 串口上位机开发笔记:如何给界面更换图标
  • 响应式购物网站模板不想让网站保存密码怎么做
  • C#:函数默认参数
  • 比较指令 CMP 解析
  • 做设计接私活的网站优化近义词