对于登录与登出操作在后台项目中是一个通用的解决方案。
登录
登录具体可以分为以下几点:
- 封装
axios模块及请求接口 - 保存服务端返回的
token - 登录鉴权
这些内容就共同的组成了一套 后台登录解决方案 。
配置环境变量封装 axios 模块
在当前这个场景下,希望封装出来的 axios 模块,至少需要具备一种能力,那就是:根据当前模式的不同,设定不同的 BaseUrl ,因为通常情况下企业级项目在 开发状态 和 生产状态 下它的 baseUrl 是不同的。
首先可以在项目中创建两个文件:.env.development和.env.production,它们分别对应 开发状态 和 生产状态。
// .env.development
# base api
VUE_APP_BASE_API = '/api'
// .env.production
# base api
VUE_APP_BASE_API = '/prod-api'
创建 utils/request.js ,写入如下代码:
import axios from 'axios'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
export default service
webpack DevServer 代理
在前面配置环境变量时指定了 开发环境下,请求的 BaseUrl 为 /api ,假如发出的请求为:/api/login, 这样的一个请求会被自动键入到当前前端所在的服务中,所以我们最终就得到了 http://host:8080/api/login 这样的一个请求路径。但是这个路径肯定是没有,所以需要代理到真正的服务器上。
在 vue.config.js 中,加入以下代码:
module.exports = {
devServer: {
// 配置反向代理
proxy: {
// 当地址中有/api的时候会触发代理机制
'/api': {
// 要代理的服务器地址 这里不用写 api
target: 'https://api.xxxxxxx/',
changeOrigin: true // 是否跨域
}
}
},
...
}
本地缓存处理方案
通常情况下,在获取到 token 之后,我们会把 token 进行缓存,而缓存的方式将会分为两种:
- 本地缓存:
LocalStorage - 全局状态管理:
Vuex
保存在 LocalStorage 是为了方便实现 自动登录功能, 保存在 vuex 中是为了后面在其他位置进行使用。
LocalStorage
在 vuex 的 user 模块下,处理 token 的保存
export default {
namespaced: true,
state: () => ({
token: getItem(TOKEN) || ''
}),
mutations: {
setToken(state, token) {
state.token = token
setItem(TOKEN, token)
}
},
actions: {
login(context, userInfo) {
...
.then(data => {
this.commit('user/setToken', data.data.data.token)
resolve()
})
...
})
}
}
}
响应数据的统一处理
有一个地方比较难受,那就是在 vuex 的 user 模块 中,我们获取数据端的 token 数据,通过 data.data.data.token 的形式进行获取。一路的 data. 确实让人比较难受,如果有过 axios 拦截器处理经验的同学应该知道,对于这种问题,我们可以通过 axios 响应拦截器 进行处理。
// 响应拦截器
service.interceptors.response.use(
response => {
const { success, message, data } = response.data
// 根据success的成功与否决定下面的操作
if (success) {
return data
} else {
// 业务错误
ElMessage.error(message) // 提示错误消息
return Promise.reject(new Error(message))
}
},
error => {
// TODO: 将来处理 token 超时问题
ElMessage.error(error.message) // 提示错误信息
return Promise.reject(error)
}
)
登录鉴权解决方案
首先我们先去对 登录鉴权 进行一个定义,什么是 登录鉴权 呢?
- 当用户未登陆时,不允许进入除
login之外的其他页面。 - 用户登录后,
token未过期之前,不允许进入login页面。
而想要实现这个功能,那么最好的方式就是通过 路由守卫 来进行实现。
在 main.js 平级,创建 permission 文件
import router from './router'
import store from './store'
// 白名单
const whiteList = ['/login']
router.beforeEach(async (to, from, next) => {
if (store.getters.token) {
if (to.path === '/login') {
next('/')
} else {
next()
}
} else {
// 没有token的情况下,可以进入白名单
if (whiteList.indexOf(to.path) > -1) {
next()
} else {
next('/login')
}
}
})
登出
对于退出登录而言,它的触发时机一般有两种:
- 用户主动退出
- 用户被动退出
其中:
- 主动退出指:用户点击登录按钮之后退出
- 被动退出指:
token过期或被 其他人"顶下来"时退出
那么无论是什么退出方式,在用户退出时,所需要执行的操作都是固定的:
- 清理掉当前用户缓存数据
- 清理掉权限相关配置
- 返回到登录页
用户主动退出的对应策略
在 store/modules/user.js 中,添加对应 action
import router from '@/router'
logout() {
this.commit('user/setToken', '')
this.commit('user/setUserInfo', {})
removeAllItem()
router.push('/login')
}
用户被动退出方案解析
用户被动退出 的场景主要有两个:1. token 失效; 2. 单点登录:其他人登录该账号被"顶下来"。
那么这两种场景下,在前端对应的处理方案一共也分为两种,共分为 主动处理 、被动处理 两种 :
- 主动处理:主要应对
token失效 - 被动处理:同时应对
token失效 与 单点登录
用户被动退出解决方案之主动处理
我们知道 token 表示了一个用户的身份令牌,对 服务端 而言,它是只认令牌不认人的。所以说一旦其他人获取到了你的 token ,那么就可以伪装成你,来获取对应的敏感数据。
所以为了保证用户的信息安全,那么对于 token 而言就被制定了很多的安全策略,比如:动态刷新 token(可变 token)和 时效 token,一般的方案就是 时效 token。
对于 token 本身是拥有时效的,但是通常情况下,这个时效都是在服务端进行处理。而此时我们要在 服务端处理 token 时效的同时,在前端主动介入 token 时效的处理中。 从而保证用户信息的更加安全性。
那么对应到我们代码中的实现方案为:
- 在用户登陆时,记录当前 登录时间
- 制定一个 失效时长
- 在接口调用时,根据 当前时间 对比 登录时间 ,看是否超过了 时效时长
- 如果未超过,则正常进行后续操作
- 如果超过,则进行 退出登录 操作
创建 utils/auth.js 文件,并写入以下代码:
// 获取时间戳
export function getTimeStamp() {
return getItem(TIME_STAMP)
}
// 设置时间戳
export function setTimeStamp() {
setItem(TIME_STAMP, Date.now())
}
// 是否超时
export function isCheckTimeout() {
// 当前时间戳
var currentTime = Date.now()
// 缓存时间戳
var timeStamp = getTimeStamp()
return currentTime - timeStamp > TOKEN_TIMEOUT_VALUE
}
在用户登录成功之后去设置时间,到 store/user.js 的 login 中:
import { setTimeStamp } from '@/utils/auth'
login(context, userInfo) {
...
return new Promise((resolve, reject) => {
...
.then(data => {
...
// 保存登录时间
setTimeStamp()
resolve()
})
})
},
在 utils/request 对应的请求拦截器中进行 主动介入
import { isCheckTimeout } from '@/utils/auth'
if (store.getters.token) {
if (isCheckTimeout()) {
// 登出操作
store.dispatch('user/logout')
return Promise.reject(new Error('token 失效'))
}
...
}
至此就完成了 主动处理 对应的业务逻辑
用户被动退出解决方案之被动处理
对于token 过期
我们知道对于
token而言,本身就是具备时效的,这个是在服务端生成token时就已经确定的。而此时我们所谓的
token过期指的就是:服务端生成的
token超过 服务端指定时效 的过程
而对于 单点登录 而言,指的是:
当用户 A 登录之后,
token过期之前。用户 A 的账号在其他的设备中进行了二次登录,导致第一次登录的 A 账号被 “顶下来” 的过程。
即:同一账户仅可以在一个设备中保持在线状态
从背景中我们知道,以上的两种情况,都是在 服务端进行判断的,而对于前端而言其实是 **服务端通知前端的一个过程。**
所以说对于其业务处理,将遵循以下逻辑:
- 服务端返回数据时,会通过特定的状态码通知前端
- 当前端接收到特定状态码时,表示遇到了特定状态:
token时效 或 单点登录 - 此时进行 退出登录 处理
如果token失效,只需要后台和前台约定一个**token 失效** 状态码即可。如果需要到 单点登录 时,只需要增加一个状态码判断即可。
在 utils/request 的响应拦截器中,增加以下逻辑:
// 响应拦截器
service.interceptors.response.use(
response => {
...
},
error => {
// 处理 token 超时问题
if (
error.response &&
error.response.data &&
error.response.data.code === 401
) {
// token超时
store.dispatch('user/logout')
}
ElMessage.error(error.message) // 提示错误信息
return Promise.reject(error)
}
)
至此,我们就已经完成了 整个用户退出 方案。