-------------------- 更新 --------------------
发现这种方案搭配 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 的插件。