Code前端首页关于Code前端联系我们

1.Vue Router 和 JWT 各自是干啥的?

terry 3小时前 阅读数 5 #Vue
文章标签 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 咋配合?

用户点登录按钮后,流程大概是这样:

  1. 前端发登录请求:把用户名、密码传给后端接口(/api/login)。
  2. 后端返回 JWT:验证成功后,生成 accessToken(用于接口鉴权)和 refreshToken(用于刷新过期的 accessToken),一起返回给前端。
  3. 前端存 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,得用后端接口间接处理,适合前后端同域场景)。
  4. 跳转目标页面:登录成功后,用 Vue Router 跳转到用户想访问的页面,比如从登录页跳主页:
    router.push({ name: 'Home' })

这里有个细节:如果用户没登录时直接输 URL 访问需要权限的页面(/user),得让 Vue Router 把用户踢回登录页,这就需要路由守卫配合,后面会讲。

路由守卫咋用 JWT 拦非法访问?

Vue Router 提供了 router.beforeEach 这类导航守卫,在路由跳转前拦截,判断用户有没有权限,结合 JWT 的逻辑大概这样:

  1. 判断路由是否需要登录:给需要权限的路由加 meta.requiresAuth 标记,比如路由配置:
    const routes = [
      {
        path: '/user',
        name: 'User',
        component: User,
        meta: { requiresAuth: true } // 需要登录才能访问
      }
    ]
  2. 在全局守卫里检查 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:

  1. 响应拦截器抓 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)
      }
    )
  2. 处理并发请求的刷新冲突:如果同时多个请求都返回 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 注入风险。
  • CSRF 攻击:token 存在 Cookie 里,且没设 SameSite,攻击者能伪造请求让用户浏览器自动发 Cookie,对策:设置 Cookie 的 SameSite=Strict/Lax,再配合 Secure(只在 HTTPS 下传)。
  • token 明文存储:JWT 本身是 Base64 编码(不是加密),payload 信息能被解析,所以别在 token 里存密码、手机号这些敏感信息,只存用户 ID、角色这类必要信息。

多角色场景下,咋做细粒度权限控制?

比如系统有 admin(能删数据)、editor(能改数据)、viewer(只能看)三种角色,不同角色能访问的页面不一样,结合 Vue Router 和 JWT 可以这么玩:

  1. JWT 里存角色信息:后端生成 JWT 时,把用户角色("role": "admin")塞到 payload 里,前端解析后能拿到。
  2. 路由元信息配角色权限:给路由加 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 能进
        }
      }
    ]
  3. 路由守卫里判断角色:在 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()
      }
    })
  4. 动态加载路由(进阶):如果角色权限由后端配置(比如不同企业的 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前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门