You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Expo Web中用localStorage存储JWT是否安全?存在哪些潜在风险?

问题描述

我使用Expo Secure Store存储来自Supabase的JWT凭证,对应的适配器代码如下:

const ExpoSecureStoreAdapter = {
  getItem: (key: string) => {
      return SecureStore.getItemAsync(key);
  },
  setItem: (key: string, value: string) => {
      SecureStore.setItemAsync(key, value);
  },
  removeItem: (key: string) => {
      SecureStore.deleteItemAsync(key);
  },
};

该方案在iOS/Android端运行良好,但Expo Web并不支持。因此我改用localStorage作为Web客户端的存储方案,对应的适配器代码如下:

const WebLocalStorageAdapter = {
  getItem: async (key: string) => {
    return localStorage.getItem(key);
  },
  setItem: async (key: string, value: string) => {
    return localStorage.setItem(key, value);
  },
  removeItem: async (key: string) => {
    return localStorage.removeItem(key);
  },
}

随后通过以下代码根据平台选择对应的适配器:

const isWeb = Platform.OS === 'web';
const storageAdapter = isWeb ? WebLocalStorageAdapter : ExpoSecureStoreAdapter;

请问这种做法是否可取?是否存在我未察觉的攻击向量或其他问题?

回答

这种跨平台适配的思路是可行的,但Web端用localStorage存储JWT确实存在几个需要重点关注的风险和细节问题:

核心风险点

  • XSS攻击隐患:localStorage的内容可被同源下的任意JavaScript直接读取,如果你的站点存在XSS漏洞,攻击者注入的恶意脚本能轻松窃取JWT凭证,进而冒充用户执行操作。而原生端的Expo Secure Store是系统级安全存储,普通应用内脚本无法随意访问,安全性高出不少。
  • 无自动过期机制:localStorage不会主动清理存储内容,除非手动删除或用户清除浏览器缓存。即便JWT本身带过期时间,你也得额外实现逻辑去校验并移除过期凭证,否则会出现用失效token发起请求的情况。
  • 跨标签页状态不同步:当用户打开多个站点标签页时,localStorage的修改会同步到所有同源标签页。如果其中一个标签页登出并删除了JWT,其他标签页需要监听storage事件来同步状态,否则会出现用户已登出但其他标签页仍保持登录状态的不一致问题。

优化建议

  • Web端优先用HttpOnly Cookie存JWT:如果Supabase后端支持,把JWT存入带有HttpOnly、Secure、SameSite属性的Cookie中,这样JavaScript无法读取该Cookie,能从根源上避免XSS攻击带来的凭证泄露问题。不过这种方式需要处理跨域请求的Cookie传递逻辑。
  • 必须用localStorage时加防护:如果业务场景限制只能用localStorage,要确保站点没有XSS漏洞——比如严格过滤用户输入、配置内容安全策略(CSP)限制脚本加载、禁用eval等危险API。同时给JWT设置较短的过期时间,配合刷新token机制,降低凭证泄露后的风险范围。
  • 统一适配器的异步签名:注意到ExpoSecureStoreAdapter的方法返回Promise但未显式标记async,而Web适配器标记了。虽然不会直接报错,但统一加上async能让代码风格更一致,避免潜在的类型校验问题:
const ExpoSecureStoreAdapter = {
  getItem: async (key: string) => {
      return SecureStore.getItemAsync(key);
  },
  setItem: async (key: string, value: string) => {
      return SecureStore.setItemAsync(key, value);
  },
  removeItem: async (key: string) => {
      return SecureStore.deleteItemAsync(key);
  },
};

内容的提问来源于stack exchange,提问作者alyx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 00:02:53