从浏览器存储到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 方案更适合与后端框架紧密集成的非前后端分离项目,或者后端状态管理(如购物车)非常复杂的场景。在前后端分离架构中,它的有状态特性会成为扩展瓶颈。
相关推荐
牛奔1 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌6 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX7 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法8 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端