在 Vue + TypeScript 项目中,登录状态的存储核心是客户端存储标识 + 服务端验证,结合 Vue 生态的最佳实践,我会分「存储方案选择」「TS 类型封装」「实际代码实现」三部分讲清楚,保证类型安全且符合前端工程化规范。
一、核心存储方案选择(按场景)
1. 首选:Cookie(推荐登录态)
- 存储位置:浏览器 Cookie(通过 js-cookie 库操作)
- 优势: 支持 HttpOnly/Secure 配置,防 XSS 攻击(服务端设置更安全); 自动随请求携带,无需手动拼接,适配后端 Session 验证; 可设置过期时间,天然支持登录态超时。
- 适用:服务端维护 Session(Redis 存储)的场景,前端只需存 Session ID。
2. 次选:LocalStorage/SessionStorage(适配 JWT)
- LocalStorage:永久存储(除非手动删除),适合长期登录态;
- SessionStorage:会话级存储(关闭标签页清空),适合临时登录态;
- 优势:存储容量大(5MB),可存更多用户信息;
- 注意:易受 XSS 攻击,不要存敏感信息(如 token 需加密 / 服务端验证)。
3. 补充:Vue Pinia(内存缓存)
- 登录状态同步到 Pinia 全局状态,解决页面刷新后状态丢失问题(需结合 Cookie/LocalStorage 持久化);
- 优势:响应式,组件间共享登录态更便捷,TS 类型支持完善。
二、完整实现步骤(Vue3 + TS + Pinia + Cookie)
前置依赖安装
bash
运行
# 操作 Cookie 的库(TS 友好)npm install js-cookie @types/js-cookie# Pinia(Vue 官方状态管理,替代 Vuex)npm install pinia步骤 1:封装 TS 类型(保证类型安全)
新建 src/types/user.ts,定义用户和登录状态类型:
typescript
运行
// 用户信息类型export interface UserInfo { userId: number; username: string; token: string; // 或 Session ID roles: string[]; // 权限角色}// 登录请求参数类型export interface LoginParams { username: string; password: string;}步骤 2:封装存储工具(Cookie/LocalStorage)
新建 src/utils/storage.ts,统一处理存储逻辑,避免重复代码:
typescript
运行
import Cookies from 'js-cookie';import { UserInfo } from '@/types/user';// 存储键名(统一管理,避免硬编码)const STORAGE_KEYS = { TOKEN: 'USER_TOKEN', USER_INFO: 'USER_INFO', LOGIN_STATUS: 'IS_LOGIN'};// ==================== Cookie 操作(推荐存储 Token/Session ID) ====================/** * 设置 Token(Cookie) * @param token 令牌 * @param expires 过期时间(天),默认 7 天 */export const setToken = (token: string, expires = 7) => { Cookies.set(STORAGE_KEYS.TOKEN, token, { expires, path: '/', // 全站有效 // 生产环境开启(仅 HTTPS 传输) // secure: process.env.NODE_ENV === 'production', // 防 XSS(需服务端设置,前端设置无效) // httpOnly: true });};/** * 获取 Token(Cookie) */export const getToken = (): string | undefined => { return Cookies.get(STORAGE_KEYS.TOKEN);};/** * 删除 Token(Cookie) */export const removeToken = () => { Cookies.remove(STORAGE_KEYS.TOKEN);};// ==================== LocalStorage 操作(存储用户信息) ====================/** * 设置用户信息(LocalStorage,TS 类型校验) */export const setUserInfo = (userInfo: UserInfo) => { localStorage.setItem(STORAGE_KEYS.USER_INFO, JSON.stringify(userInfo));};/** * 获取用户信息(LocalStorage,TS 类型转换) */export const getUserInfo = (): UserInfo | null => { const info = localStorage.getItem(STORAGE_KEYS.USER_INFO); if (info) { try { return JSON.parse(info) as UserInfo; } catch (e) { console.error('解析用户信息失败', e); return null; } } return null;};/** * 删除用户信息 */export const removeUserInfo = () => { localStorage.removeItem(STORAGE_KEYS.USER_INFO);};// ==================== 登录状态快捷操作 ====================/** * 清空所有登录态 */export const clearLoginState = () => { removeToken(); removeUserInfo();};步骤 3:Pinia 全局状态管理(响应式登录态)
新建 src/stores/user.ts,封装登录 / 登出 / 状态同步逻辑:
typescript
运行
import { defineStore } from 'pinia';import { ref, computed } from 'vue';import { UserInfo, LoginParams } from '@/types/user';import { setToken, getToken, setUserInfo, getUserInfo, clearLoginState } from '@/utils/storage';// 假设你有封装的请求库import { request } from '@/utils/request';// 定义 Pinia Store,保证 TS 类型提示export const useUserStore = defineStore('user', () => { // 响应式状态:用户信息 const userInfo = ref(null); // 响应式状态:是否登录 const isLogin = computed(() => !!userInfo.value && !!getToken()); // 初始化:页面刷新后从本地存储恢复登录态 const initUserState = () => { const info = getUserInfo(); if (info && getToken()) { userInfo.value = info; } }; // 登录操作(异步,TS 类型校验) const login = async (params: LoginParams) => { try { // 调用登录接口,获取 Token 和用户信息 const res = await request.post<{ token: string; userInfo: UserInfo }>('/api/login', params); // 1. 存储 Token 到 Cookie setToken(res.token); // 2. 存储用户信息到 LocalStorage setUserInfo(res.userInfo); // 3. 更新 Pinia 状态(响应式) userInfo.value = res.userInfo; return true; } catch (error) { console.error('登录失败', error); return false; } }; // 登出操作 const logout = () => { // 1. 清空本地存储 clearLoginState(); // 2. 清空 Pinia 状态 userInfo.value = null; // 3. 可选:调用后端登出接口(销毁服务端 Session/Token) request.post('/api/logout'); }; return { userInfo, isLogin, initUserState, login, logout };}); 步骤 4:全局初始化登录态(App.vue)
页面刷新后恢复登录状态,保证状态不丢失:
vue
[xss_clean]import { onMounted } from 'vue';import { useUserStore } from '@/stores/user';const userStore = useUserStore();// 页面挂载时初始化登录态onMounted(() => { userStore.initUserState();});[xss_clean] 步骤 5:登录组件使用示例(Login.vue)
vue
[xss_clean]import { ref } from 'vue';import { useUserStore } from '@/stores/user';import { useRouter } from 'vue-router';import type { LoginParams } from '@/types/user';const userStore = useUserStore();const router = useRouter();// 表单数据(TS 类型约束)const form = ref({ username: '', password: ''});// 登录按钮点击事件const handleLogin = async () => { const success = await userStore.login(form.value); if (success) { // 登录成功跳转到首页 router.push('/home'); }};[xss_clean] 步骤 6:请求拦截器(自动携带 Token)
新建 src/utils/request.ts,封装 Axios 并自动携带 Token,实现登录态验证:
typescript
运行
import axios from 'axios';import { getToken } from '@/utils/storage';import { useUserStore } from '@/stores/user';// 创建 Axios 实例const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 5000});// 请求拦截器:添加 Tokenrequest.interceptors.request.use( (config) => { const token = getToken(); if (token) { // 把 Token 放到请求头(或依赖 Cookie 自动携带) config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error));// 响应拦截器:处理 Token 过期request.interceptors.response.use( (response) => response.data, (error) => { // 401 表示 Token 过期/未登录 if (error.response?.status === 401) { const userStore = useUserStore(); userStore.logout(); // 清空登录态 // 跳转到登录页 _window.location.href = '/login'; } return Promise.reject(error); });export { request };三、关键注意事项(TS + 安全)
- 类型约束全覆盖:所有存储 / 请求 / 状态都要定义 TS 接口,避免 any 类型,提升代码可维护性;
- 敏感信息不裸存: Token/Session ID 优先存在 Cookie(开启 HttpOnly/Secure); LocalStorage 只存非敏感用户信息(如用户名、角色),不存密码 / 完整 Token;
- 登录态双重验证:前端存储的只是「标识」,最终需通过后端接口验证(如每次请求携带 Token,后端查 Redis 校验);
- Token 过期处理:响应拦截器捕获 401 状态码,自动登出并跳转登录页;
- 登出需清全量:既要清空前端存储(Cookie/LocalStorage),也要调用后端接口销毁服务端登录态。
总结(核心要点)
- 存储组合:Cookie 存 Token/Session ID + LocalStorage 存用户信息 + Pinia 做响应式状态管理;
- TS 核心:通过接口定义 UserInfo/LoginParams,保证所有操作的类型安全;
- 安全原则:敏感信息用 Cookie(带安全配置),前端只存标识,最终验证依赖后端。
如果需要适配 JWT 场景(如 Token 存在 LocalStorage + 刷新 Token 逻辑),或补充路由守卫(未登录拦截)的代码,可以告诉我,我会针对性补充。
vue typescript,如何验证cookie过期,是在后端服务器,还是前端服务器?
在 Vue + TypeScript 项目中,Cookie 过期的核心验证逻辑必须在后端服务器,前端仅做「辅助判断」和「友好处理」—— 这是保障登录态安全的核心原则。下面分「核心结论」「前后端分工」「TS 实战代码」三部分讲清楚,新手能直接落地。
一、核心结论
表格
职责 | 后端服务器(必做) | 前端(Vue + TS,辅助) |
Cookie 过期验证 | ✅ 校验 Token/Session ID 是否有效、是否过期 | ❌ 不做核心验证(前端可被篡改) |
过期结果返回 | ✅ 返回明确状态码(如 401 Unauthorized) | ✅ 接收状态码,执行登出 / 跳转等操作 |
过期时间管理 | ✅ 控制 Cookie 的 Expires/Max-Age 配置 | ✅ 可选:读取 Cookie 过期时间做前端提示 |
简单说:后端是「裁判」,前端是「执行者」 —— 前端永远不能自己判断「登录是否有效」,必须以后端的校验结果为准。
二、前后端分工详解(结合 TS 实战)
1. 后端服务器:Cookie 过期的核心验证(关键)
Cookie 本质是「客户端存储的标识」(如 Session ID/Token),真正的过期逻辑在后端:
- Session 模式:后端 Redis 中存储 Session ID 对应的过期时间,每次请求时校验: 如果 Redis 中 Session 不存在 / 已过期 → 返回 401 状态码; 如果有效 → 正常返回数据。
- JWT 模式:后端解析 Token 时校验签名和过期时间(exp 字段),过期则返回 401。
- Cookie 配置:后端设置 Cookie 时,会指定 Expires/Max-Age(如 2 小时),浏览器会自动在过期后删除 Cookie,但前端删除不代表后端状态失效(仍需后端校验)。
2. 前端(Vue + TS):处理过期结果(核心代码)
前端不直接验证 Cookie 是否过期,而是通过「拦截后端响应」处理过期场景,以下是完整 TS 实现:
步骤 1:封装 Axios 响应拦截器(捕获 401 过期状态)
新建 src/utils/request.ts,这是前端处理 Cookie 过期的核心入口:
typescript
运行
import axios from 'axios';import Cookies from 'js-cookie';import { useUserStore } from '@/stores/user';import { ElMessage } from 'element-plus'; // 可选:UI 库提示// 创建 Axios 实例const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 5000, withCredentials: true // 关键:允许携带 Cookie(跨域时必须)});// 响应拦截器:处理后端返回的过期状态request.interceptors.response.use( (response) => { // 后端返回正常数据 → 直接返回 return response.data; }, async (error) => { const { response } = error; // 核心:捕获后端返回的 401 状态码(表示 Token/Session 过期/无效) if (response && response.status === 401) { const userStore = useUserStore(); // 1. 清空前端所有登录态(Cookie/LocalStorage/Pinia) userStore.logout(); // 2. 友好提示 ElMessage.error('登录状态已过期,请重新登录'); // 3. 跳转登录页(避免死循环:排除登录页本身) if _(window.location.pathname !== '/login') { _window.location.href = '/login'; } } // 其他错误(如 500)正常抛出 return Promise.reject(error); });export { request };步骤 2:Pinia 中封装 logout 方法(清空前端 Cookie)
在之前的 src/stores/user.ts 中完善 logout 逻辑,确保前端 Cookie 被清理:
typescript
运行
import { defineStore } from 'pinia';import { ref, computed } from 'vue';import Cookies from 'js-cookie';import { request } from '@/utils/request';import type { UserInfo, LoginParams } from '@/types/user';export const useUserStore = defineStore('user', () => { // ... 其他状态(省略) // 登出:清空前端 Cookie + 通知后端 const logout = async () => { try { // 1. 调用后端登出接口(让后端销毁 Session/Token) await request.post('/api/logout'); } finally { // 2. 清空前端 Cookie(关键:不管后端是否成功,前端都要清) Cookies.remove('USER_TOKEN'); // 对应后端设置的 Cookie 键名 // 3. 清空 LocalStorage 和 Pinia 状态 localStorage.removeItem('USER_INFO'); userInfo.value = null; } }; return { userInfo, isLogin, login, logout };});步骤 3:可选:前端读取 Cookie 过期时间(仅做提示)
如果需要在前端「提前提示」Cookie 即将过期(非核心验证),可通过 TS 解析 Cookie 的过期时间:
typescript
运行
// src/utils/cookie.tsimport Cookies from 'js-cookie';/** * 读取 Cookie 的过期时间(仅做前端提示,不做验证) * @param key Cookie 键名 * @returns 过期时间(Date)或 null */export const getCookieExpires = (key: string): Date | null => { // 注意:js-cookie 无法直接获取过期时间,需解析 [xss_clean] const cookies = [xss_clean].split('; '); for (const cookie of cookies) { const [name, value] = cookie.split('='); if (name === key) { // 后端设置 Cookie 时需返回 Expires 信息(或前端自己记录) // 示例:假设后端返回的 Cookie 包含过期时间,或前端存储了过期时间 const expiresStr = Cookies.get(`${key}_expires`); // 需后端配合 if (expiresStr) { return new Date(expiresStr); } return null; } } return null;};/** * 检查 Cookie 是否即将过期(仅提示,不做验证) * @param key Cookie 键名 * @param minutes 提前提示的分钟数 * @returns 是否即将过期 */export const isCookieAboutToExpire = (key: string, minutes = 10): boolean => { const expires = getCookieExpires(key); if (!expires) return false; // 计算剩余时间(毫秒) const remaining = expires.getTime() - Date.now(); // 剩余时间 < 设定的提前时间(分钟转毫秒)→ 提示 return remaining < minutes * 60 * 1000;};在组件中使用(仅做提示,不影响登录态):
vue
[xss_clean]import { onMounted } from 'vue';import { isCookieAboutToExpire } from '@/utils/cookie';import { ElMessage } from 'element-plus';onMounted(() => { // 检查 Token Cookie 是否即将过期(提前 10 分钟提示) if (isCookieAboutToExpire('USER_TOKEN', 10)) { ElMessage.warning('登录状态即将过期,请刷新页面或重新登录'); }});[xss_clean]3. 后端关键配置(补充,帮助你理解)
以 Node.js/Express 为例,后端设置 Cookie 并校验过期:
javascript

运行
// 1. 设置 Cookie 时指定过期时间res.cookie('USER_TOKEN', sessionId, { httpOnly: true, // 防 XSS(前端无法通过 JS 读取) secure: process.env.NODE_ENV === 'production', // 仅 HTTPS 传输 maxAge: 2 * 60 * 60 * 1000, // 2 小时过期(毫秒) path: '/'});// 2. 校验 Cookie 过期(核心)app.use((req, res, next) => { const sessionId = req.cookies.USER_TOKEN; // 从 Redis 中查 Session 是否存在/过期 redis.get(`session:${sessionId}`, (err, data) => { if (!data) { // Session 过期/无效 → 返回 401 return res.status(401).json({ msg: '登录状态已过期' }); } next(); // 有效则继续 });});三、关键注意事项(Vue + TS)
- 前端绝不信任本地判断:即使前端读取到 Cookie 没过期,也必须以后端响应为准(比如用户手动修改了本地时间,或 Cookie 被篡改);
- withCredentials 必须开启:跨域请求时,Axios 需设置 withCredentials: true,否则浏览器不会携带 Cookie 到后端;
- 401 处理要防死循环:跳转登录页时,要排除登录页本身(否则会无限跳转);
- HttpOnly Cookie:后端设置 httpOnly: true 后,前端无法通过 JS 读取 Cookie —— 这是安全的,前端只需通过后端状态码判断即可。
总结(核心要点)
- 验证主体:Cookie 过期的核心验证在后端服务器(校验 Session/Token 有效性);
- 前端职责:通过 Axios 拦截器捕获后端 401 状态码,执行「清空登录态 + 跳转登录页」;
- 安全原则:前端仅做「辅助提示」(如即将过期),绝不做「是否登录有效」的核心判断。