美文网首页
更友好的 Nuxt 路由权限控制

更友好的 Nuxt 路由权限控制

作者: 梧桐月明中 | 来源:发表于2020-12-21 10:11 被阅读0次

-------------------- 更新 --------------------

发现这种方案搭配 CDN 会存在问题。
CDN 会缓存服务端返回的静态资源,如果返回的页面中具有个人信息,既会浪费 CDN 资源,又会导致个人信息在 CDN 的泄露。
因此就要避免在服务端请求用户信息并渲染到页面中。

改进办法:

删掉服务端的 userInfo 请求,将服务端依据用户信息的路由控制后移到页面程序中。

在所有页面中,mounted 生命周期中,判断是否 store 中没有 userInfo 然而 cookie 中存在 token,如果是,则请求 userInfo 接口填充信息;在需要登录的页面中, mounted 生命周期中,判断 cookie 中是否存在 token,不存在则直接跳转到登录页面。

这样改造之后在需要登陆的页面刷新,会先呈现未登录状态的页面,即服务端返回的无差别页面,随后变成有用户信息的登陆态页面。虽然体验上打了折扣,但个人感觉还是比跳到登录页再跳回来的感受要好一些。

具体改进步骤如下:

  • 删掉 nuxtServerInit
  • middleware/auth 中增加 process.client 的判断,使其只在客户端路由中生效
// middleware/auth.js
export default function ({ route, redirect, store, $cookies }) {
  if (process.client) {
    const token = $cookies.get('token')
    const logout = $cookies.get('logout')
    const loggedIn = store.getters['user/loggedIn']

    // 处理其它选项卡中登陆后的情况
    if (token && !loggedIn) {
      store.commit('user/SET_TOKEN', token)
      store.dispatch('user/getInfo')
    } 
    // 处理其它选项卡中登出后的情况
    else if (!token && logout && loggedIn) { 
      store.dispatch('user/logout')
      redirect('/login')
    } 
    // 正常情况
    else if (!loggedIn) {
      $cookies.set('redirect.login', route.path)
      redirect('/login')
    }
  }
}
  • 添加 mixin,restoreUserInfo.js 添加到 layouts/default.vue 中
// mixin/restoreUserInfo.js
/**
 * 用户登录状态,刷新页面,则重新根据 token 请求用户信息填充 store
 */
const restoreUserInfoMixin = {
  mounted () {
    const token = this.$cookies.get('token')
    const loggedIn = this.$store.getters['user/loggedIn']

    // 有 token 却没用户信息,则获取用户信息
    if (accessToken && !loggedIn) {
      this.$store.commit('user/SET_TOKEN', token)
      this.$store.dispatch('user/getInfo')
    }
  }
}

export default restoreUserInfoMixin

  • 另一个 mixin 以 plugin 的形式添加,在需要登录的页面中加入 auth: true 生效
// plugins/auth.js
/**
 * 需要登录的页面,判断有没有 token
 */
import Vue from 'vue'
Vue.mixin({
  mounted () {
    if (this.$options.auth) {
      const token = this.$cookies.get('token') || this.$cookies.get('udm_access_token')

      // 没 token,则不予展示
      if (!token) {
        this.$router.push('/login')
      }
    }
  }
})

-------------------- 原文 --------------------

在项目开发中,经常会遇到需要进行路由控制的场景,比如只有在登陆的情况下,才可以访问个人中心页面。

原本引入了 @nuxtjs/auth 模块,但是应用中发现它并不完美。比如在个人中心页面,登陆的情况下,刷新页面或者新标签页打开页面时,会先跳到登陆页面再跳回当前页。参考这篇文章

文中作者认为“这是 @nuxtjs/auth 的机制,因为它要加载 vuex 状态树,并没有什么不友好”,但我依然无法忍受,再加之项目需要更加自由的路由控制,于是我决定放弃 @nuxtjs/auth 的封装,自己来实现。

解决上面问题的思路在于 nuxtServerInit,它是一个 store action,只在应用初始化时执行一次(刷新时、新标签页打开时),执行在路由中间件之前。我们可以在这里读取 cookie 中的 token,并发送请求,将请求结果填充进 store 中。

梳理下需要完成的功能:

  • 未登录状态访问需要登录的页面,跳转到登录页面
  • 登录后可以自动跳回到之前的页面
  • 登录后可以正常访问页面
  • 登录后刷新或新标签页打开需要登录的页面,不丢失登录状态
  • 在需要登录的页面点击退出登录,退出后跳回首页
  • 退出登录后其它标签页也为退出状态
  • 登录状态下接口请求自带 Authorization: 'Bearer token' 头信息

为了实现以上功能,需要:

  • 存储之 cookie 持久化
  • 存储之 vuex 状态管理
    • nuxtServerInit 进行初始化的信息填充
    • user 模块存储用户信息
  • 插件之 axios 来添加 Authorization 头
  • 中间件之 auth 进行路由权限控制
  • 插件之 mock 用来开发中接口自测

一、cookie 持久化

引入 cookie-universal-nuxt 进行 cookie 管理

npm install cookie-universal-nuxt -s

然后加入到 nuxt.config.js 的模块中

// nuxt.config.js
export default {
  // 省略其余代码
  modules: ['cookie-universal-nuxt']
}

之后就可以通过 this.$cookies 使用了

这里需要用设置的 cookie 有三个:

  • token: 登录后拿到的 token
  • redirect: 记录被重定向到 login 页面前的地址,以便登录后回跳
  • logout: 用户点击退出后标记为 true, 登录后标记为 false(加这个是为了点击退出后其它标签页也要丢失登录状态,后面会讲到)

二、状态管理

nuxtServerInit 必须写在 store/index.js 中才会被调用。

在这里判断 cookie 中有没有 token,如果有,就存储到 store 中并请求用户信息,然后把用户信息也存储到 store 中。

注意需要 await 接口请求。

// store/index.js
export default {
  actions: {
    async nuxtServerInit ({ commit }, { $cookies }) {
      const token = $cookies.get('token')
      if (token) {
        commit('user/SET_TOKEN', token)
        await this.$axios.get('/user')
          .then((response) => {
            commit('user/SET_INFO', response.data.data)
          })
          .catch((err) => {
            console.error(err)
          })
      }
    }
  }
}

然后添加一个 user 模块。

在 user 模块中存储 token 和用户信息 info。
并提供一个 getters loggedIn。
在 actions 中提供 getInfo、login、register、logout 动作,包括对应的接口请求、状态填充和 cookie 操作。

// store/user.js
/* eslint-disable no-unused-expressions */
export default {
  namespace: true,

  state: () => ({
    token: '',
    info: null
  }),

  getters: {
    loggedIn (state) {
      return !!state.info
    }
  },

  mutations: {
    SET_TOKEN: (state, payload) => {
      state.token = payload
    },
    SET_INFO: (state, payload) => {
      state.info = payload
    }
  },

  actions: {
    getInfo ({ commit }) {
      return new Promise((resolve, reject) => {
        this.$axios.get('/user')
          .then((response) => {
            commit('SET_INFO', response.data.data)
            resolve()
          })
          .catch((err) => {
            reject(err)
          })
      })
    },
    login ({ commit, dispatch }, payload) {
      return new Promise((resolve, reject) => {
        this.$axios.post('/login', payload)
          .then(async (response) => {
            commit('SET_TOKEN', response.data.data.token)
            let options = null
            if (payload.remember) {
              options = {
                maxAge: 60 * 60 * 24 * 7
              }
            }
            this.$cookies.set('token', response.data.data.token, options)
            this.$cookies.set('logout', false)
            await dispatch('getInfo')
            resolve()
          })
          .catch((err) => {
            reject(err)
          })
      })
    },
    register ({ commit, dispatch }, payload) {
      return new Promise((resolve, reject) => {
        this.$axios.post('/register', payload)
          .then(async (response) => {
            commit('SET_TOKEN', response.data.data.token)
            this.$cookies.set('token', response.data.data.token)
            this.$cookies.set('logout', false)
            await dispatch('getInfo')
            resolve()
          })
          .catch((err) => {
            reject(err)
          })
      })
    },
    logout ({ commit }) {
      return new Promise((resolve, reject) => {
        this.$axios.post('/logout', null, {
          baseURL: BASE_URL.default
        })
          .then(() => {
            commit('SET_TOKEN', '')
            commit('SET_INFO', null)
            this.$cookies.remove('token')
            this.$cookies.set('logout', true)
            const current = this.$router.history.current.path
            const pagesNeedLoggedIn = ['/my', '/me', '/market']
            const isNeedLoggedIn = pagesNeedLoggedIn.some(item => current.indexOf(item) === 0)
            if (isNeedLoggedIn) {
              this.$router.push('/')
            }
            resolve()
          })
          .catch((err) => {
            reject(err)
          })
      })
    }
  }
}

三、axios 插件

首先项目中要引入了 axios 模块,如果没有先 npm install @nuxtjs/axios -s,然后添加一个 axios 插件

// nuxt.config.js
export default {
  // 省略其余代码
  modules: ['@nuxtjs/axios'],
  plugins: ['@/plugins/axios']
}

写 plugins/axios.js 插件。
如果 cookie 中或 store 中存在 token(防止用户禁用了浏览器 cookie,所以也要查看下 store),则加入 ajax 请求头 Authorization。

// plugins/axios.js
export default function ({ $axios, $cookies, store }) {
  $axios.setBaseURL('http://api.example.com')

  $axios.onRequest((config) => {
    const cookieToken = $cookies.get('token')
    const storeToken = store.state.user.token
    const token = cookieToken || storeToken
    if (token) {
      config.headers.Authorization = 'Bearer ' + token
    }
    return config
  }}
}

四、路由中间件

然后通过一个路由中间件来进行权限控制的跳转,在需要路由控制的页面中通过middleware: 'auth'引入。

这里除了拦截访问、重定向到登录页的主要逻辑外,还要处理两种情况。

一是其它选项卡中登录后的情况。这时在当前选项卡,cookie 中有了 token,但是 store 中没有,这时应该把 token 填充到 store 并去请求用户信息。

二是其它选项卡登出后的情况。这时在当前选项卡,store 中还有 token 等数据,但是 cookie 中已经没有了 token,并且 logout 为 true,这时候应该 dispatch 下 logout,并将当前页面重定向到登录页。

这两种情况都是在再次点击链接触发路由时生效的,并非和其它选项卡实时同步。并且这两种情况都不考虑 cookie 被禁用,在禁用 cookie 时,也不存在这些联动问题了。

再说一下主要逻辑:拦截访问、重定向到登录页,这时候需要把当前地址记录到 cookie 中,用来登录后的回跳。

// middleware/auth.js
export default function ({ route, redirect, store, $cookies }) {
  const token = $cookies.get('token')
  const logout = $cookies.get('logout')
  const loggedIn = store.getters['user/loggedIn']

  // 处理其它选项卡中登陆后的情况
  if (token && !loggedIn) {
    store.commit('user/SET_TOKEN', token)
    store.dispatch('user/getInfo')
  } 
  // 处理其它选项卡中登出后的情况
  else if (!token && logout && loggedIn) { 
    store.dispatch('user/logout')
    redirect('/login')
  } 
  // 正常情况
  else if (!loggedIn) {
    $cookies.set('redirect.login', route.path)
    redirect('/login')
  }
}

五、页面

一切就绪,可以在页面中写方法进行 login、logout 了。这时基本只需要 dispatch action 就可以了。注意 login 后需要处理一下回跳,如果 cookie 中存在 redirect.login 记录,则回跳过去,并把记录清掉;如果没有,则跳到个人中心页面。

// pages/login.js
// 省略其它代码
async login () {
  this.submitting = true

  const validateResult = await this.validate()
  if (!validateResult) {
    this.submitting = false
    return
  }

  const data = {
    account: this.formData.account,
    password: this.formData.password,
    remember: this.formData.remember
  }

  this.$store.dispatch('user/login', data)
    .then(() => {
      const redirect = this.$cookies.get('redirect.login')
      if (redirect) {
        this.$router.push(redirect)
        this.$cookies.remove('redirect.login')
      } else {
        this.$router.push('/my/account')
      }
    })
    .catch(() => {
      this.$message({
        type: 'error',
        message: '发生错误,请重试'
      })
    })
    .then(() => {
      this.submitting = false
    })
}
// components/Nav.js
// 省略其它代码
logout () {
  this.$store.dispatch('user/logout')
}

六、Mock

Mock 不必多说,用来开发时模拟接口请求返回假数据。
npm install mockjs -D 然后在 plugins 中加入个 mock.js 就可以了。

// plugins/mock.js 
import Mock from 'mockjs'
// 示例
Mock.mock(/\/login/, 'post', () => {
  return {
    code: 200,
    message: 'success',
    data: {
      token: 'thisisatoken123abc'
    }
  }
})

它会自动拦截 axios 请求。但是注意只在浏览器端有效,服务端发出的不会拦截。

总结

至此我们上面所列的功能就都可以实现了。
后面可以进阶一下,看是否可以封装成类似 @nuxtjs/auth 的插件。

相关文章

网友评论

      本文标题:更友好的 Nuxt 路由权限控制

      本文链接:https://www.haomeiwen.com/subject/utwxnktx.html