1.Vue Router 和 JWT 各自是干啥的?
做前端项目时,不少同学碰到“Vue 单页应用里咋用 JWT 管控页面权限、维持登录状态”这类问题,毕竟 Vue Router 管路由跳转,JWT 负责身份验证,把这俩结合好才能让页面访问权限、用户登录态逻辑更流畅,下面通过几个关键问题,把 Vue Router + JWT 的核心逻辑、踩坑点唠明白。
先把基础掰扯清楚,不然后面逻辑理解起来费劲,Vue Router 是 Vue 生态里管前端路由的工具,比如用户点“个人中心”跳转到 /user 页面,不同 URL 对应不同组件渲染,这些规则都是 Vue Router 配置的。
JWT(JSON Web Token)是跨域身份验证的方案,用户登录成功后,后端生成一个加密的 token 发前端,这个 token 里能塞用户 ID、角色、过期时间这些信息,之后前端发请求时把 token 带上,后端验证 token 合法性,确认用户身份和权限,简单说,Vue Router 管“页面能不能跳”,JWT 管“用户有没有权跳”。
登录流程里,JWT 和 Vue Router 咋配合?
用户点登录按钮后,流程大概是这样:
- 前端发登录请求:把用户名、密码传给后端接口(/api/login)。
- 后端返回 JWT:验证成功后,生成 accessToken(用于接口鉴权)和 refreshToken(用于刷新过期的 accessToken),一起返回给前端。
- 前端存 token:得选个存储方式,常见的有 localStorage、sessionStorage、cookie,举个例子,用 localStorage 存:
localStorage.setItem('accessToken', res.data.accessToken) localStorage.setItem('refreshToken', res.data.refreshToken)
但要注意安全!localStorage 容易被 XSS 攻击偷 token,要是对安全要求高,也可以让后端把 token 放 HttpOnly Cookie 里(但前端拿不到 Cookie 里的 token,得用后端接口间接处理,适合前后端同域场景)。 - 跳转目标页面:登录成功后,用 Vue Router 跳转到用户想访问的页面,比如从登录页跳主页:
router.push({ name: 'Home' })
这里有个细节:如果用户没登录时直接输 URL 访问需要权限的页面(/user),得让 Vue Router 把用户踢回登录页,这就需要路由守卫配合,后面会讲。
路由守卫咋用 JWT 拦非法访问?
Vue Router 提供了 router.beforeEach
这类导航守卫,在路由跳转前拦截,判断用户有没有权限,结合 JWT 的逻辑大概这样:
- 判断路由是否需要登录:给需要权限的路由加
meta.requiresAuth
标记,比如路由配置:const routes = [ { path: '/user', name: 'User', component: User, meta: { requiresAuth: true } // 需要登录才能访问 } ]
- 在全局守卫里检查 token:
router.beforeEach(async (to, from, next) => { // 第一步:判断当前路由是否需要登录 if (to.meta.requiresAuth) { // 第二步:拿存储的 accessToken const accessToken = localStorage.getItem('accessToken') if (!accessToken) { // 没 token,跳登录页 next({ name: 'Login' }) } else { // 第三步:检查 token 是否过期(前端临时判断,最终以后端为准) try { const payload = JSON.parse(atob(accessToken.split('.')[1])) const now = Date.now() / 1000 if (payload.exp < now) { // token 过期了,用 refreshToken 换新的 const refreshToken = localStorage.getItem('refreshToken') const res = await axios.post('/api/refresh', { refreshToken }) // 换新 token 后存起来 localStorage.setItem('accessToken', res.data.accessToken) next() // 换成功,继续跳目标路由 } else { // token 没过期,直接放行 next() } } catch (error) { // 解析 payload 失败,或者 refresh 失败,清空 token 跳登录 localStorage.removeItem('accessToken') localStorage.removeItem('refreshToken') next({ name: 'Login' }) } } } else { // 不需要登录的路由,直接放行 next() } })
这里要注意:前端解析 JWT 里的过期时间(exp)只是“临时判断”,因为 token 可能被篡改,最终得靠后端接口验证,所以即使前端判断没过期,后端也可能返回 401(token 被吊销),这时候得在 axios 响应拦截器里再处理(后面讲)。
JWT 过期后,刷新令牌咋玩?
accessToken 有过期时间(1 小时),过期后得用 refreshToken 换新的 accessToken,不然用户得重新登录,体验差,这时候要结合axios 拦截器和 Vue Router:
- 响应拦截器抓 401 错误:当接口返回 401(Unauthorized),说明 accessToken 过期了,这时候发 refresh 请求:
axios.interceptors.response.use( response => response, async error => { if (error.response.status === 401) { // 尝试刷新 token const refreshToken = localStorage.getItem('refreshToken') try { const res = await axios.post('/api/refresh', { refreshToken }) // 存新的 accessToken localStorage.setItem('accessToken', res.data.accessToken) // 重试刚才失败的请求 return axios(error.config) } catch (refreshError) { // 刷新失败,清空 token 跳登录 localStorage.removeItem('accessToken') localStorage.removeItem('refreshToken') router.push({ name: 'Login' }) return Promise.reject(refreshError) } } return Promise.reject(error) } )
- 处理并发请求的刷新冲突:如果同时多个请求都返回 401,会触发多次 refresh 请求,得加个“正在刷新”的标记,避免重复请求:
let isRefreshing = false let refreshQueue = [] <p>axios.interceptors.response.use(..., async error => { if (error.response.status === 401) { if (!isRefreshing) { isRefreshing = true // 发 refresh 请求... // 刷新成功后,执行队列里的请求 refreshQueue.forEach(cb => cb(res.data.accessToken)) refreshQueue = [] isRefreshing = false } else { // 正在刷新,把请求加入队列 return new Promise((resolve) => { refreshQueue.push((newToken) => { error.config.headers.Authorization = <code>Bearer ${newToken}</code> resolve(axios(error.config)) }) }) } } ... })
这样能保证同一时间只有一个 refresh 请求,其他请求等刷新完再重试。
前端存 JWT 有啥安全坑?咋避?
很多同学只顾功能,忽略安全,最后项目被攻击,常见风险和对策:
- XSS 攻击偷 token:如果用 localStorage/sessionStorage 存 token,恶意脚本(比如注入的 XSS 代码)能通过
localStorage.getItem('accessToken')
把 token 偷走,对策:- 重要系统优先用 HttpOnly Cookie 存 token(后端设置
Set-Cookie: accessToken=xxx; HttpOnly; Secure; SameSite=Strict
),这样前端 JS 拿不到 Cookie,XSS 偷不了;但前端发请求时,浏览器会自动带 Cookie,后端得处理跨域 Cookie 问题(CORS 配置withCredentials: true
)。 - 如果前端必须拿到 token(比如要放请求头里),那得用 localStorage,但要加 Content Security Policy(CSP) 限制页面只能加载信任的脚本,减少 XSS 注入风险。
- 重要系统优先用 HttpOnly Cookie 存 token(后端设置
- CSRF 攻击:token 存在 Cookie 里,且没设
SameSite
,攻击者能伪造请求让用户浏览器自动发 Cookie,对策:设置 Cookie 的SameSite=Strict/Lax
,再配合Secure
(只在 HTTPS 下传)。 - token 明文存储:JWT 本身是 Base64 编码(不是加密),payload 信息能被解析,所以别在 token 里存密码、手机号这些敏感信息,只存用户 ID、角色这类必要信息。
多角色场景下,咋做细粒度权限控制?
比如系统有 admin(能删数据)、editor(能改数据)、viewer(只能看)三种角色,不同角色能访问的页面不一样,结合 Vue Router 和 JWT 可以这么玩:
- JWT 里存角色信息:后端生成 JWT 时,把用户角色(
"role": "admin"
)塞到 payload 里,前端解析后能拿到。 - 路由元信息配角色权限:给路由加
meta.roles
,指定哪些角色能访问:const routes = [ { path: '/admin', name: 'Admin', component: Admin, meta: { requiresAuth: true, roles: ['admin'] // 只有 admin 能进 } }, { path: '/editor', name: 'Editor', component: Editor, meta: { requiresAuth: true, roles: ['admin', 'editor'] // admin 和 editor 能进 } } ]
- 路由守卫里判断角色:在
router.beforeEach
里,除了检查 token,还要对比用户角色和路由要求的角色:router.beforeEach(async (to, from, next) => { if (to.meta.requiresAuth) { const accessToken = localStorage.getItem('accessToken') if (!accessToken) { next({ name: 'Login' }) return } // 解析角色 const payload = JSON.parse(atob(accessToken.split('.')[1])) const userRole = payload.role // 检查角色是否匹配 if (to.meta.roles && !to.meta.roles.includes(userRole)) { // 角色不匹配,跳权限不足页面 next({ name: 'Forbidden' }) return } // 其他逻辑(比如检查 token 过期)... next() } else { next() } })
- 动态加载路由(进阶):如果角色权限由后端配置(比如不同企业的 admin 权限不同),可以让后端返回用户能访问的路由列表,前端动态添加路由:
// 后端返回的可访问路由列表:[{ path: '/xxx', component: 'Xxx' }, ...] const accessibleRoutes = await axios.get('/api/routes') accessibleRoutes.forEach(route => { router.addRoute(route) // 动态添加路由 })
这样能实现更灵活的权限控制,但要注意路由重复和加载顺序问题。
刷新页面时,JWT 咋不丢?
Vue 单页应用是前端路由,刷新页面时会重新加载 JS,内存里的变量会丢失,但存在 localStorage/sessionStorage 或 Cookie 里的 token 不会丢,所以要把 token 存在持久化存储里(localStorage),而不是只存在 Vuex 的 state 里(刷新就没了)。
举个例子,用 Vuex 存 token 时,要在页面加载时从 localStorage 里读出来:
// store/index.js const store = createStore({ state: { accessToken: localStorage.getItem('accessToken') || '' }, mutations: { setToken(state, token) { state.accessToken = token localStorage.setItem('accessToken', token) } } })
这样刷新页面时,Vuex 能从 localStorage 里重新拿到 token,路由守卫也能正常判断权限。
把 Vue Router 和 JWT 结合好,核心是“路由跳转前拦一下,token 过期时换一下,不同角色时筛一下,存储 token 时安全一下”,实际项目里,得根据业务场景(比如对内系统 vs 公开平台)选存储方式、权限颗粒度,多测测刷新页面、多标签页登录、token 过期这些边界情况,才能让权限和登录态逻辑更稳~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。