后台管理系统之登录与登出

1,002 阅读7分钟

对于登录与登出操作在后台项目中是一个通用的解决方案。

登录

登录具体可以分为以下几点:

  1. 封装 axios 模块及请求接口
  2. 保存服务端返回的 token
  3. 登录鉴权

这些内容就共同的组成了一套 后台登录解决方案 。

配置环境变量封装 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 进行缓存,而缓存的方式将会分为两种:

  1. 本地缓存:LocalStorage
  2. 全局状态管理: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')
        }
      }
})

登出

对于退出登录而言,它的触发时机一般有两种:

  1. 用户主动退出
  2. 用户被动退出

其中:

  1. 主动退出指:用户点击登录按钮之后退出
  2. 被动退出指:token 过期或被 其他人"顶下来"时退出

那么无论是什么退出方式,在用户退出时,所需要执行的操作都是固定的:

  1. 清理掉当前用户缓存数据
  2. 清理掉权限相关配置
  3. 返回到登录页

用户主动退出的对应策略

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. 单点登录:其他人登录该账号被"顶下来"。

那么这两种场景下,在前端对应的处理方案一共也分为两种,共分为 主动处理被动处理 两种 :

  1. 主动处理:主要应对 token 失效
  2. 被动处理:同时应对 token 失效 与 单点登录

用户被动退出解决方案之主动处理

我们知道 token 表示了一个用户的身份令牌,对 服务端 而言,它是只认令牌不认人的。所以说一旦其他人获取到了你的 token ,那么就可以伪装成你,来获取对应的敏感数据。

所以为了保证用户的信息安全,那么对于 token 而言就被制定了很多的安全策略,比如:动态刷新 token(可变 token)和 时效 token,一般的方案就是 时效 token

对于 token 本身是拥有时效的,但是通常情况下,这个时效都是在服务端进行处理。而此时我们要在 服务端处理 token 时效的同时,在前端主动介入 token 时效的处理中。 从而保证用户信息的更加安全性。

那么对应到我们代码中的实现方案为:

  1. 在用户登陆时,记录当前 登录时间
  2. 制定一个 失效时长
  3. 在接口调用时,根据 当前时间 对比 登录时间 ,看是否超过了 时效时长
    1. 如果未超过,则正常进行后续操作
    2. 如果超过,则进行 退出登录 操作

创建 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.jslogin 中:

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 账号被 “顶下来” 的过程。

即:同一账户仅可以在一个设备中保持在线状态

从背景中我们知道,以上的两种情况,都是在 服务端进行判断的,而对于前端而言其实是 **服务端通知前端的一个过程。**

所以说对于其业务处理,将遵循以下逻辑:

  1. 服务端返回数据时,会通过特定的状态码通知前端
  2. 当前端接收到特定状态码时,表示遇到了特定状态:token 时效单点登录
  3. 此时进行 退出登录 处理

如果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)
  }
)

至此,我们就已经完成了 整个用户退出 方案。