VueJS中存储用户token的最佳方案是什么(不使用localStorage和cookie)
方案选型结论
如果要求完全不使用localStorage和cookie,没有100%和本地存储等价的永久持久化方案,可选的最优替代方案有两类,可根据你的业务场景选择:
- 方案1:IndexedDB加密存储 + 内存缓存(安全性优于localStorage,基本满足常规持久化需求)
- 方案2:纯内存存储 + 服务端静默授权(安全性最高,无本地存储泄露风险,需要服务端配合)
方案1:IndexedDB加密存储
核心设计思路:对存储的token做对称加密,加密密钥仅保存在运行时内存中,就算XSS攻击读取到IndexedDB的加密数据,没有内存中的密钥也无法还原有效token,安全等级远高于明文存localStorage。
1. 加密工具函数示例
// 加密密钥在应用启动时生成,仅存在内存中,页面刷新后自动重置 const AES_KEY = crypto.getRandomValues(new Uint8Array(16)); // 128位密钥 // 加密函数 async function encrypt(data) { const encoder = new TextEncoder(); const dataBuf = encoder.encode(data); const iv = crypto.getRandomValues(new Uint8Array(12)); const key = await crypto.subtle.importKey('raw', AES_KEY, { name: 'AES-GCM' }, false, ['encrypt']); const encryptedBuf = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, dataBuf); return btoa(JSON.stringify({ iv: Array.from(iv), data: Array.from(new Uint8Array(encryptedBuf)) })); } // 解密函数 async function decrypt(encryptedStr) { const { iv: ivArr, data: dataArr } = JSON.parse(atob(encryptedStr)); const iv = new Uint8Array(ivArr); const dataBuf = new Uint8Array(dataArr); const key = await crypto.subtle.importKey('raw', AES_KEY, { name: 'AES-GCM' }, false, ['decrypt']); const decryptedBuf = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, key, dataBuf); return new TextDecoder().decode(decryptedBuf); }
2. IndexedDB简易封装示例
class TokenDB { constructor() { this.dbName = 'AppTokenDB'; this.storeName = 'tokenStore'; this.db = null; } init() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, 1); request.onupgradeneeded = (e) => { const db = e.target.result; if (!db.objectStoreNames.contains(this.storeName)) { db.createObjectStore(this.storeName, { keyPath: 'id' }); } }; request.onsuccess = (e) => { this.db = e.target.result; resolve(); }; request.onerror = (e) => reject(e.target.error); }); } async saveToken(token) { const encryptedToken = await encrypt(token); const transaction = this.db.transaction(this.storeName, 'readwrite'); const store = transaction.objectStore(this.storeName); store.put({ id: 'accessToken', value: encryptedToken }); } async getToken() { return new Promise((resolve, reject) => { const transaction = this.db.transaction(this.storeName, 'readonly'); const store = transaction.objectStore(this.storeName); const request = store.get('accessToken'); request.onsuccess = async (e) => { if (e.target.result) { try { const token = await decrypt(e.target.result.value); resolve(token); } catch (err) { // 解密失败说明密钥失效,清除无效数据 this.clearToken(); resolve(null); } } else { resolve(null); } }; request.onerror = (e) => reject(e.target.error); }); } clearToken() { const transaction = this.db.transaction(this.storeName, 'readwrite'); const store = transaction.objectStore(this.storeName); store.delete('accessToken'); } }
3. Vuex集成示例
// store/index.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const tokenDB = new TokenDB(); // 应用启动时初始化DB tokenDB.init(); export default new Vuex.Store({ state: { token: null }, mutations: { SET_TOKEN(state, token) { state.token = token; } }, actions: { // 登录成功后存储token async saveTokenAfterLogin({ commit }, token) { commit('SET_TOKEN', token); await tokenDB.saveToken(token); }, // 应用初始化时读取token async initToken({ commit }) { const token = await tokenDB.getToken(); if (token) commit('SET_TOKEN', token); }, // 退出登录时清除token async logout({ commit }) { commit('SET_TOKEN', null); tokenDB.clearToken(); } } })
4. 应用入口初始化
// main.js import store from './store' async function initApp() { await store.dispatch('initToken'); new Vue({ store, render: h => h(App) }).$mount('#app') } initApp();
方案2:纯内存存储 + 静默授权
完全不在本地存储任何token数据,安全性最高,适合对安全要求极高的业务场景,需要服务端配合支持静默授权流程。
流程说明
- 登录成功后,服务端返回token的同时,种下一个仅HttpOnly、Secure、SameSite=Strict的会话标记cookie(该cookie仅用作静默授权凭证,不会存token本身,HttpOnly属性可防止XSS窃取,不属于前端可操作的cookie范畴)
- token仅存在Vuex的内存中,页面刷新后,前端主动调用服务端的静默授权接口,服务端验证会话标记cookie有效后,直接返回新的token,前端再存入Vuex即可,整个过程用户无感知
- 退出登录时,调用服务端接口销毁会话标记,同时清空Vuex里的token
代码示例
// store/index.js import Vue from 'vue' import Vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) export default new Vuex.Store({ state: { token: null, isAuthed: false }, mutations: { SET_TOKEN(state, token) { state.token = token; state.isAuthed = !!token; } }, actions: { // 登录逻辑 async login({ commit }, loginForm) { const res = await axios.post('/api/login', loginForm); commit('SET_TOKEN', res.data.token); }, // 静默授权逻辑 async silentAuth({ commit }) { try { const res = await axios.post('/api/silent-auth'); commit('SET_TOKEN', res.data.token); return true; } catch (err) { commit('SET_TOKEN', null); return false; } }, // 退出登录 async logout({ commit }) { await axios.post('/api/logout'); commit('SET_TOKEN', null); } } })
应用入口初始化
// main.js import store from './store' import router from './router' async function initApp() { const isAuthed = await store.dispatch('silentAuth'); // 未授权且访问需鉴权页面时跳转到登录页 if (!isAuthed && router.currentRoute.meta.requireAuth) { router.push('/login'); } new Vue({ store, router, render: h => h(App) }).$mount('#app') } initApp();
注意事项
- 方案1的加密密钥每次页面刷新都会重新生成,所以如果用户完全关闭浏览器再打开,IndexedDB里的加密token会解密失败,需要重新登录,属于刻意设计的安全策略,避免设备丢失后token被窃取
- 如果你完全不能接受任何cookie,只能选择方案1
内容的提问来源于stack exchange,提问作者Peter Van de Put
相关产品推荐
相关产品推荐

