从浏览器存储到web项目中鉴权的简单分析

在浏览器中,缓存/存储数据的方式主要有以下几种,每种方式适用于不同场景,具备不同的特性,比如容量大小、持久性、安全性、作用范围等。

浏览器中的常见本地存储方式汇总

存储方式 大致容量 生命周期 是否可跨页面 是否随请求发送 可否被 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

每种方式的重点说明

cookieStoredocument.cookie 都可以用于在前端操作浏览器中的 Cookie,但它们在 使用方式、能力、兼容性、安全性 等方面存在明显差异,

  1. 它的常用属性如下,实际浏览器貌似都不支持设置domain,默认为当前域
属性 说明
name=value 键值对
Domain 指定哪些域名可以访问该 Cookie
Path 限制访问该 Cookie 的 URL 路径
Expires / Max-Age Cookie 有效期,过期后请求就不会携带了
Secure 只在 HTTPS 下发送
HttpOnly 禁止 JS 访问(防止 XSS)
SameSite 限制第三方请求时是否携带 Cookie
  1. 重点介绍一下cookie的scope

    Cookie 的 作用范围 主要由两个属性(Domain和path)控制,没有 port(端口)这个作用范围的概念,这跟 localStoragesessionStorage 不同,它们的作用范围包含端口(严格的 origin:协议 + 域 + 端口

    • Domain(域)

      默认 行为:不设置 Domain 时,Cookie 只对当前域名有效(包括子路径)--当前域名下的所有路径,不能被子域访问

      设置 Domain后:如果设置为主域,例如 example.com,则该域及其所有子域 (如 www.example.comapi.example.com)都可以访问这个 Cookie

      不能跨主域:a.com 设置的 Cookie,b.com 是无法访问的;localhost127.0.0.1 被认为是不同域

      设置 Cookie 的 Domain 为顶级域名是无效的,例如.com、.net、.org、.cn等。浏览器内部可能维护了一套顶级域名后缀列表

    • Path(路径)

      默认行为:默认是当前路径及其子路径(如 /user/)。

      设置 Path 后:可以精细控制在哪些 URL 路径及其子路径下会自动发送该 Cookie。

      示例:

      javascript 复制代码
      document.cookie = "name=abc; Path=/user";

      只有访问 /user/user/profile 才会带上该 Cookie。

      不会被非子路径访问:/user 的 Cookie 不会自动发送到 /admin

  2. cookieStore和document.cookie的区别

    特性 cookieStore document.cookie
    API 风格 异步(Promise-based) 同步字符串读写
    可读性 数据结构清晰,类似 JSON 是一个拼接的字符串,解析麻烦
    监听功能 ✅ 支持监听 cookie 改变(cookiechange 事件) ❌ 不支持
    可读性限制 可读不可为 HttpOnly 的 cookie 同样无法访问 HttpOnly cookie
    跨路径支持 ✅ 可以设置特定路径 ✅ 也可以设置特定路径
    API 粒度控制 ✅ 支持字段化读写(如 name, value, domain) ❌ 只能读写整个 cookie 字符串
    浏览器支持 目前仅部分浏览器支持(Chrome 96+) 所有浏览器广泛支持
    适合的场景 更现代的 Cookie 操作(监听/清晰管理) 兼容性优先场景
  3. cookie的唯一性

    浏览器判断两个 Cookie 是否"同名",主要从name + Domain + Path这3个字段组合判断,例如浏览器的cookie会存在如下两个cookie。设置cookie的时候:同名会覆盖,不同名会追加

    ini 复制代码
    token=abc123; Path=/
    token=xyz456; Path=/user

    删除某个cookie的最佳做法是重新设置同名 的cookie的expires=过去max-age=0

localStorage和sessionStorage

  1. localstorage和sessionStorage都是同源策略(协议+域名+端口),三者完全一致,才能访问同一个 localStorage 或 sessionStorage。

  2. localstorage是永久存储,不随页面刷新或关闭而失效,

    但注意:"永久"只是相对的,它仍然可以被用户手动清除,或受到浏览器存储策略的限制(比如隐私模式下不持久)。

  3. sessionStorage是仅在当前标签页中有效,标签页关闭即清除

  4. 两种存储都遵循 Storage 接口,有setItem、getItem、removeItem、clear的api

前后端分离项目中的鉴权

背景知识:什么是 Token 授权

在前后端分离的 Web 应用中,用户登录成功后,后端通常会签发一个 Token(令牌) 来表示用户身份:

  • 最常见的 Token 是 JWT(JSON Web Token)
  • Token 本质是一个字符串,前端拿着它访问接口时,后端校验身份

常见的鉴权策略

  1. 单Token策略:使用一个JWT token进行认证,存储localStorage、sessionStorage、cookie、cookie httpOnly
  2. 双Token策略 :Access Token + Refresh Token
    • Access Token存localStorage + Refresh Token存HttpOnly Cookie
    • 双Token都存储在HttpOnly Cookie
  3. 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)

这是最简单直接的无状态认证方式。

核心思路

  1. 登录:用户提供凭证,后端验证通过后,生成一个包含用户信息和过期时间的JWT Token,返回给前端。
  2. 存储 :前端将此Token存储在某个地方(localStorage最常见)。
  3. 请求 :之后的所有请求,前端都必须在HTTP请求头 Authorization 中携带这个Token。
  4. 验证 :后端通过一个拦截器(Interceptor)或过滤器(Filter)来验证每个请求的Token。如果Token有效,则处理请求;如果无效或过期,则返回401 Unauthorized
  5. 登出:前端删除本地存储的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天)。
  1. 登录 :后端生成accessTokenrefreshTokenaccessToken通过响应体返回,refreshToken通过Set-Cookie头设置到HttpOnly的Cookie中。
  2. 请求 :前端将accessToken(从localStorage读取)放在Authorization头中去请求API。
  3. Access Token过期 :当API返回401时,说明accessToken过期。
  4. 刷新Token :前端"静默"地调用刷新接口(如/api/refresh)。这个请求不需要Authorization头,但浏览器会自动带上refreshToken的Cookie。
  5. 后端处理刷新 :后端验证refreshToken的有效性。如果有效,则生成一个新的accessToken(有时也会生成一个新的refreshToken来做轮换),返回给前端。
  6. 重试 :前端拿到新的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));
}

这种方式对前端最友好,但对后端的CSRF防护要求最高。

  • 思路 : 登录后,accessTokenrefreshToken都通过Set-Cookie设置为HttpOnly。前端代码几乎不用关心Token的存在。
  • 请求 : 浏览器自动携带两个Cookie。后端拦截器优先验证accessToken
  • 刷新 : 如果accessToken过期,拦截器内部直接检查refreshToken。如果refreshToken有效,则在同一个请求-响应周期内 ,生成新双Token,设置到响应的Set-Cookie头中,然后继续处理原业务逻辑。这个过程对前端完全透明。
  • 安全性 : 这是防XSS的最好方式。但所有POST/PUT/DELETE等修改性操作的接口都必须有CSRF防护

虽然是"传统"方式,但在某些场景下依然适用,尤其是与Spring Security等框架结合时,实现非常简单。

核心思路

  1. 登录 :用户登录成功后,服务器创建一个Session对象(可以存在内存、数据库或Redis中),并生成一个唯一的Session ID

  2. 设置Cookie :服务器通过Set-CookieSession ID(通常是JSESSIONID)返回给浏览器,并设置为HttpOnly

  3. 请求 :浏览器之后的每个请求都会自动带上这个Session ID Cookie。

  4. 验证 :服务器根据Session ID找到对应的Session对象,从而获取用户状态。

  5. 跨域问题

    : 这种方式天然存在跨域问题。前端(如

    复制代码
    vue.a.com

    )请求后端(

    复制代码
    api.b.com

    )时,浏览器默认不会发送

    css 复制代码
    b.com

    的Cookie。需要:

    • 后端设置CORS响应头 Access-Control-Allow-Credentials: true
    • 前端Axios设置 axios.defaults.withCredentials = true;

重点伪代码

前端 (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 方案更适合与后端框架紧密集成的非前后端分离项目,或者后端状态管理(如购物车)非常复杂的场景。在前后端分离架构中,它的有状态特性会成为扩展瓶颈。
相关推荐
南雨北斗3 分钟前
高版本mysql数据导入低版本mysql数据出错解决方案
后端
无奈何杨4 分钟前
CoolGuard风控系统基于“会话”构建可信任的验证降级机制(理论篇)
后端
Deng9452013144 分钟前
快捷订餐系统
前端·css·css3
十五_在努力7 分钟前
参透JavaScript —— 花十分钟搞懂作用域
前端·javascript
前端小巷子11 分钟前
前端网络性能优化
前端·javascript·面试
晓得迷路了22 分钟前
栗子前端技术周刊第 89 期 - TypeScript 5.9 Beta、VSCode v1.102、Angular 20.1...
前端·javascript·typescript
Kagol28 分钟前
2025年中总结:我想我克服公众演讲的恐惧了,一个社恐分子突破自我的故事
前端·开源
江城开朗的豌豆32 分钟前
多个组件库混用导致JS爆炸?看我如何瘦身70%!
前端·javascript·vue.js
江城开朗的豌豆36 分钟前
Vue懒加载全揭秘:从2.x到3.0,我是这样优化首屏速度的!
前端·javascript·vue.js
江城开朗的豌豆38 分钟前
不用Vue,手搓一个数据双向绑定?教你用原生JS造轮子!
前端·javascript·vue.js