在浏览器中,缓存/存储数据的方式主要有以下几种,每种方式适用于不同场景,具备不同的特性,比如容量大小、持久性、安全性、作用范围等。
浏览器中的常见本地存储方式汇总
存储方式 | 大致容量 | 生命周期 | 是否可跨页面 | 是否随请求发送 | 可否被 JS 访问 | 简述用途 |
---|---|---|---|---|---|---|
document.cookie |
~4KB/每条 | 自定义(expires ) |
✅ 是 | ✅ 会随请求发送 | ✅ 可访问 | 早期数据存储方式,兼容好,常用于登录态 |
cookieStore |
同 cookie | 同上 | ✅ 是 | ✅ | ✅(非 HttpOnly) | 更现代的 Cookie 异步 API |
localStorage |
~5MB | 永久(除非手动清除) | ✅ 是 | ❌ 不随请求发送 | ✅ 可访问 | 持久存储,适合本地缓存、配置等 |
sessionStorage |
~5MB | 当前标签页关闭即失效 | ❌ 不可 | ❌ 不随请求发送 | ✅ 可访问 | 短期存储,适合会话数据 |
IndexedDB |
上百 MB | 永久(除非清除) | ✅ 是 | ❌ | ✅ 可访问 | 大容量结构化数据(如离线缓存) |
WebSQL (已废弃) |
~5MB | 永久 | ✅ 是 | ❌ | ✅ 可访问 | SQLite 接口,已弃用,不推荐使用 |
Cache API (Service Worker) |
上百 MB | 持久/可控 | ✅ 是 | ❌ | ✅ 可访问 | 请求/响应缓存,适合离线网页或 PWA |
每种方式的重点说明
cookie
cookieStore
和 document.cookie
都可以用于在前端操作浏览器中的 Cookie,但它们在 使用方式、能力、兼容性、安全性 等方面存在明显差异,
- 它的常用属性如下,实际浏览器貌似都不支持设置domain,默认为当前域
属性 | 说明 |
---|---|
name=value |
键值对 |
Domain |
指定哪些域名可以访问该 Cookie |
Path |
限制访问该 Cookie 的 URL 路径 |
Expires / Max-Age |
Cookie 有效期,过期后请求就不会携带了 |
Secure |
只在 HTTPS 下发送 |
HttpOnly |
禁止 JS 访问(防止 XSS) |
SameSite |
限制第三方请求时是否携带 Cookie |
-
重点介绍一下cookie的scope
Cookie 的 作用范围 主要由两个属性(Domain和path)控制,没有
port
(端口)这个作用范围的概念,这跟localStorage
、sessionStorage
不同,它们的作用范围包含端口(严格的 origin:协议 + 域 + 端口)-
Domain(域)
默认 行为:不设置
Domain
时,Cookie 只对当前域名有效(包括子路径)--当前域名下的所有路径,不能被子域访问设置 Domain后:如果设置为主域,例如
example.com
,则该域及其所有子域 (如www.example.com
、api.example.com
)都可以访问这个 Cookie不能跨主域:
a.com
设置的 Cookie,b.com
是无法访问的;localhost
和127.0.0.1
被认为是不同域。设置 Cookie 的
Domain
为顶级域名是无效的,例如.com、.net、.org、.cn等。浏览器内部可能维护了一套顶级域名后缀列表 -
Path(路径)
默认行为:默认是当前路径及其子路径(如
/user/
)。设置 Path 后:可以精细控制在哪些 URL 路径及其子路径下会自动发送该 Cookie。
示例:
javascriptdocument.cookie = "name=abc; Path=/user";
只有访问
/user
或/user/profile
才会带上该 Cookie。不会被非子路径访问:
/user
的 Cookie 不会自动发送到/admin
。
-
-
cookieStore和document.cookie的区别
特性 cookieStore
document.cookie
API 风格 异步(Promise-based) 同步字符串读写 可读性 数据结构清晰,类似 JSON 是一个拼接的字符串,解析麻烦 监听功能 ✅ 支持监听 cookie 改变( cookiechange
事件)❌ 不支持 可读性限制 可读不可为 HttpOnly
的 cookie同样无法访问 HttpOnly
cookie跨路径支持 ✅ 可以设置特定路径 ✅ 也可以设置特定路径 API 粒度控制 ✅ 支持字段化读写(如 name, value, domain) ❌ 只能读写整个 cookie 字符串 浏览器支持 目前仅部分浏览器支持(Chrome 96+) 所有浏览器广泛支持 适合的场景 更现代的 Cookie 操作(监听/清晰管理) 兼容性优先场景 -
cookie的唯一性
浏览器判断两个 Cookie 是否"同名",主要从
name + Domain + Path
这3个字段组合判断,例如浏览器的cookie会存在如下两个cookie。设置cookie的时候:同名会覆盖,不同名会追加initoken=abc123; Path=/ token=xyz456; Path=/user
删除某个cookie的最佳做法是重新设置同名 的cookie的expires=过去
或
max-age=0
localStorage和sessionStorage
-
localstorage和sessionStorage都是同源策略(协议+域名+端口),三者完全一致,才能访问同一个 localStorage 或 sessionStorage。
-
localstorage是永久存储,不随页面刷新或关闭而失效,
但注意:"永久"只是相对的,它仍然可以被用户手动清除,或受到浏览器存储策略的限制(比如隐私模式下不持久)。
-
sessionStorage是仅在当前标签页中有效,标签页关闭即清除
-
两种存储都遵循
Storage
接口,有setItem、getItem、removeItem、clear的api
前后端分离项目中的鉴权
背景知识:什么是 Token 授权
在前后端分离的 Web 应用中,用户登录成功后,后端通常会签发一个 Token(令牌) 来表示用户身份:
- 最常见的 Token 是 JWT(JSON Web Token)
- Token 本质是一个字符串,前端拿着它访问接口时,后端校验身份
常见的鉴权策略
- 单Token策略:使用一个JWT token进行认证,存储localStorage、sessionStorage、cookie、cookie httpOnly
- 双Token策略 :Access Token + Refresh Token
- Access Token存localStorage + Refresh Token存HttpOnly Cookie
- 双Token都存储在HttpOnly Cookie
- Cookie-based认证:传统的基于Cookie的会话认证
策略 | 安全性 | 易用性 (前端) | 跨域支持 | 服务端 | 推荐场景 |
---|---|---|---|---|---|
1. 单Token (localStorage) | ⭐⭐ (易受XSS攻击) | ⭐⭐⭐⭐⭐ (简单直观) | ⭐⭐⭐⭐⭐ (极佳) | 无状态,易扩展 | 内部系统,安全性要求不高的场景 |
2. 双Token (Access存前端, Refresh存Cookie) | ⭐⭐⭐⭐ (较好平衡) | ⭐⭐⭐ (中等复杂) | ⭐⭐⭐⭐ (良好) | 无状态,易扩展 | 通用Web应用,推荐方案 |
3. 双Token (都存HttpOnly Cookie) | ⭐⭐⭐⭐⭐ (高) | ⭐⭐⭐⭐ (前端简单) | ⭐⭐⭐ (需处理CSRF) | 无状态,易扩展 | 安全性要求极高的金融、政务类应用 |
4. 传统 Cookie-Session | ⭐⭐⭐⭐ (HttpOnly防XSS) | ⭐⭐⭐⭐ (前端简单) | ⭐⭐ (需额外配置) | 有状态,扩展性差 | 简单应用,或从传统架构迁移的项目 |
1. 单Token策略 (Single JWT Token)
这是最简单直接的无状态认证方式。
核心思路
- 登录:用户提供凭证,后端验证通过后,生成一个包含用户信息和过期时间的JWT Token,返回给前端。
- 存储 :前端将此Token存储在某个地方(
localStorage
最常见)。 - 请求 :之后的所有请求,前端都必须在HTTP请求头
Authorization
中携带这个Token。 - 验证 :后端通过一个拦截器(Interceptor)或过滤器(Filter)来验证每个请求的Token。如果Token有效,则处理请求;如果无效或过期,则返回
401 Unauthorized
。 - 登出:前端删除本地存储的Token即可。
存储方式对比
- localStorage/sessionStorage : 最常用。通过JS可以方便地读写。最大缺点是容易受到XSS攻击,一旦网站有XSS漏洞,攻击者的脚本可以轻易获取Token。
- Cookie: JS可以读写,同样有XSS风险。同时,如果未做特殊处理,请求时会自动携带,可能受CSRF攻击影响。
- Cookie (HttpOnly) : JS无法读取,可以有效防止XSS攻击。但它会被浏览器自动附加到所有同域请求上,必须做严格的CSRF防护 (如
SameSite
属性、Anti-CSRF Token)。
重点伪代码
前端 (Vue + Axios)
JavaScript
ini
// 1. Axios请求拦截器:自动添加Token
axios.interceptors.request.use(config => {
const token = localStorage.getItem('jwt_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 2. 登录后存储Token
async function login(username, password) {
const response = await axios.post('/api/login', { username, password });
const token = response.data.token;
localStorage.setItem('jwt_token', token);
}
// 3. 收到401后跳转登录页 (响应拦截器)
axios.interceptors.response.use(response => response, error => {
if (error.response.status === 401) {
localStorage.removeItem('jwt_token');
// router.push('/login');
console.error("认证失败,请重新登录!");
}
return Promise.reject(error);
});
后端 (Spring Boot)
Java
scala
// Spring Security Filter 或 自定义 Interceptor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
final String jwt = authHeader.substring(7);
try {
if (jwtUtil.validateToken(jwt)) {
// Token有效,设置认证信息到SecurityContext
UsernamePasswordAuthenticationToken authentication = jwtUtil.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
// Token无效(过期、签名错误等)
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid Token");
return;
}
filterChain.doFilter(request, response);
}
}
2. 双Token策略 (Access Token + Refresh Token)
这是目前Web应用中最主流、最推荐的方案,它在安全性和用户体验之间取得了很好的平衡。
核心思路
- Access Token: 用于访问业务接口,生命周期很短(如15分钟 - 2小时)。
- Refresh Token: 用于获取新的Access Token,生命周期很长(如7天 - 30天)。
策略2a: Access Token存localStorage
+ Refresh Token存HttpOnly Cookie
- 登录 :后端生成
accessToken
和refreshToken
。accessToken
通过响应体返回,refreshToken
通过Set-Cookie
头设置到HttpOnly
的Cookie中。 - 请求 :前端将
accessToken
(从localStorage
读取)放在Authorization
头中去请求API。 - Access Token过期 :当API返回
401
时,说明accessToken
过期。 - 刷新Token :前端"静默"地调用刷新接口(如
/api/refresh
)。这个请求不需要Authorization
头,但浏览器会自动带上refreshToken
的Cookie。 - 后端处理刷新 :后端验证
refreshToken
的有效性。如果有效,则生成一个新的accessToken
(有时也会生成一个新的refreshToken
来做轮换),返回给前端。 - 重试 :前端拿到新的
accessToken
后,更新localStorage
,然后重新发起刚才失败的那个API请求。
重点伪代码
前端 (Vue + Axios)
JavaScript
ini
// 增强的Axios响应拦截器,实现无感刷新
let isRefreshing = false;
let failedQueue = []; // 存储因token过期而失败的请求
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
axios.interceptors.response.use(response => response, async (error) => {
const originalRequest = error.config;
// 401错误且不是刷新请求本身
if (error.response.status === 401 && originalRequest.url !== '/api/refresh') {
if (isRefreshing) {
// 如果正在刷新,则将失败的请求存入队列
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers['Authorization'] = 'Bearer ' + token;
return axios(originalRequest); // 重新执行请求
});
}
originalRequest._retry = true; // 标记为已重试
isRefreshing = true;
try {
// refreshToken 是存在HttpOnly Cookie里的,所以这里不需要传
const rs = await axios.post('/api/refresh');
const newAccessToken = rs.data.accessToken;
localStorage.setItem('jwt_token', newAccessToken);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + newAccessToken;
processQueue(null, newAccessToken); // 执行队列中的请求
return axios(originalRequest); // 重试当前请求
} catch (err) {
processQueue(err, null); // 刷新失败,清空队列
// 导航到登录页
// router.push('/login');
return Promise.reject(err);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
});
后端 (Spring Boot)
Java
less
// LoginController.java
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
// ... 验证用户 ...
String accessToken = jwtUtil.generateAccessToken(user);
String refreshToken = jwtUtil.generateRefreshToken(user);
// 将RefreshToken存入HttpOnly Cookie
ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", refreshToken)
.httpOnly(true)
.secure(true) // 生产环境应为true
.path("/api/refresh") // 只在刷新时发送
.maxAge(Duration.ofDays(7))
.sameSite("Strict") // 严格的CSRF防护
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString())
.body(new AuthResponse(accessToken)); // AccessToken在响应体中
}
// RefreshController.java
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@CookieValue(name = "refreshToken") String refreshToken) {
// ... 验证 refreshToken 是否有效 ...
// 如果有效,生成新的 accessToken
String newAccessToken = jwtUtil.refreshAccessToken(refreshToken);
return ResponseEntity.ok(new AuthResponse(newAccessToken));
}
策略2b: 双Token都存储在HttpOnly Cookie
这种方式对前端最友好,但对后端的CSRF防护要求最高。
- 思路 : 登录后,
accessToken
和refreshToken
都通过Set-Cookie
设置为HttpOnly
。前端代码几乎不用关心Token的存在。 - 请求 : 浏览器自动携带两个Cookie。后端拦截器优先验证
accessToken
。 - 刷新 : 如果
accessToken
过期,拦截器内部直接检查refreshToken
。如果refreshToken
有效,则在同一个请求-响应周期内 ,生成新双Token,设置到响应的Set-Cookie
头中,然后继续处理原业务逻辑。这个过程对前端完全透明。 - 安全性 : 这是防XSS的最好方式。但所有
POST/PUT/DELETE
等修改性操作的接口都必须有CSRF防护。
3. Cookie-based认证 (传统Session)
虽然是"传统"方式,但在某些场景下依然适用,尤其是与Spring Security等框架结合时,实现非常简单。
核心思路
-
登录 :用户登录成功后,服务器创建一个
Session
对象(可以存在内存、数据库或Redis中),并生成一个唯一的Session ID
。 -
设置Cookie :服务器通过
Set-Cookie
将Session ID
(通常是JSESSIONID
)返回给浏览器,并设置为HttpOnly
。 -
请求 :浏览器之后的每个请求都会自动带上这个
Session ID
Cookie。 -
验证 :服务器根据
Session ID
找到对应的Session
对象,从而获取用户状态。 -
跨域问题
: 这种方式天然存在跨域问题。前端(如
vue.a.com
)请求后端(
api.b.com
)时,浏览器默认不会发送
cssb.com
的Cookie。需要:
- 后端设置CORS响应头
Access-Control-Allow-Credentials: true
。 - 前端Axios设置
axios.defaults.withCredentials = true;
。
- 后端设置CORS响应头
重点伪代码
前端 (Vue + Axios)
JavaScript
csharp
// 全局设置,允许跨域携带Cookie
axios.defaults.withCredentials = true;
// 登录和普通请求,前端无需特殊处理token
async function login(username, password) {
// 登录成功后,浏览器会自动存储服务器返回的SESSIONID Cookie
await axios.post('https://api.yourdomain.com/login', { username, password });
}
async function getUserData() {
// 请求时,浏览器会自动带上Cookie
const response = await axios.get('https://api.yourdomain.com/api/user');
return response.data;
}
后端 (Spring Boot)
Java
scala
// Spring Security配置,它会为你自动处理Session
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ... 配置 userDetailsService, passwordEncoder ...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 开启CSRF防护
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin(); // 或自定义登录端点
}
// CORS配置,允许凭证
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080")); // 前端地址
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowCredentials(true); // 关键!
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
总结
- 对于绝大多数新项目 ,双Token策略(Access存
localStorage
,Refresh存HttpOnly Cookie
) 是最佳实践。它在安全性、用户体验和实现复杂度上取得了最佳平衡。 - 如果你的应用对安全性要求极高 (例如金融级应用),并且你对后端的CSRF防护有十足的信心和经验,可以采用 双Token都存
HttpOnly Cookie
的方式。 - 单Token策略 适用于快速开发、内部系统或对安全要求不那么敏感的项目。
- 传统Session 方案更适合与后端框架紧密集成的非前后端分离项目,或者后端状态管理(如购物车)非常复杂的场景。在前后端分离架构中,它的有状态特性会成为扩展瓶颈。