登录方案分析:
-
封装
axios
模块 (环境变量配置) -
封装 接口请求 模块
-
封装登录请求动作
-
保存服务端返回的
token
-
登录鉴权 (白名单、判断token、退出登录方案)
封装 axios
模块
1. 创建axios实例:定义请求baseURL、请求超时时间
2.请求、响应拦截
import store from '@/store'
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { isCheckTimeout } from '@/utils/auth'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 6000
})
// 请求拦截
service.interceptors.request.use(
(config) => {
if (store.getters.token) {
// 判断前端定时的token是否超时
if (!isCheckTimeout) {
store.dispatch('user/logout')
return Promise.reject(new Error('token 失效'))
}
存在token 注入请求头
config.headers.Authorization = `Bearer ${store.getters.token}`
}
// 配置接口国际化
config.headers['Accept-Language'] = store.getters.language
return config
},
(error) => {
// 判断服务器的token是否超时,超时返回code=401
if (
error.response &&
error.response.data &&
error.response.data.code === 401
) {
store.dispatch('user/logout')
}
ElMessage.error(error.message) // 提示错误信息
return Promise.reject(error)
}
)
// 响应拦截
service.interceptors.response.use(
(response) => {
const { success, message, data } = response.data
if (success) {
return data
} else {
ElMessage.error(message)
return Promise.reject(new Error(message))
}
},
(error) => {
ElMessage.error(error.message)
return Promise.reject(error)
}
)
export default service
baseURL 获取的环境变量
创建两个变量 .env.development 、.env.production
ENV = 'production'
VUE_APP_BASE_API = '/prod-api'
================================
ENV = 'development'
VUE_APP_BASE_API = '/api'
axios模块中获取环境变量
process.env.VUE_APP_BASE_API
封装请求动作
创建 api
文件夹,创建 sys.js
:
import request from '@/utils/request'
/**
* 登录
*/
export const login = data => {
return request({
url: '/sys/login',
method: 'POST',
data
})
}
封装登录请求动作:
该动作我们期望把它封装到 vuex
的 action
中
在 store
下创建 modules
文件夹,创建 user.js
模块,用于处理所有和 用户相关 的内容(此处需要使用第三方包 md5
)
import { login } from '@/api/sys'
import md5 from 'md5'
export default {
namespaced: true,
state: () => ({}),
mutations: {},
actions: {
login(context, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({
username,
password: md5(password)
})
.then(data => {
resolve()
})
.catch(err => {
reject(err)
})
})
}
}
}
在 store/index
中完成注册
import { createStore } from 'vuex'
import user from './modules/user.js'
export default createStore({
modules: {
user
}
})
登录触发动作
在 login
中,触发定义的 action
<template>
<el-button
type="primary"
style="width: 100%; margin-bottom: 30px"
:loading="loading"
@click="handleLogin"
>登录</el-button
>
</template>
<script setup>
import { ref } from 'vue'
import { validatePassword } from './rules'
import { useStore } from 'vuex'
...
// 登录动作处理
const loading = ref(false)
const loginFromRef = ref(null)
const store = useStore()
const handleLogin = () => {
loginFromRef.value.validate(valid => {
if (!valid) return
loading.value = true
store
.dispatch('user/login', loginForm.value)
.then(() => {
loading.value = false
// TODO: 登录后操作
})
.catch(err => {
console.log(err)
loading.value = false
})
})
}
</script>
前面配置环境变量时指定了 开发环境下,请求的 BaseUrl
为 /api
,所以我们真实发出的请求为:/api/sys/login
。
这样的一个请求会被自动键入到当前前端所在的服务中,所以我们最终就得到了 http://192.168.18.42:8081/api/sys/login
这样的一个请求路径。
而想要处理这个问题,那么可以通过指定 webpack DevServer 代理 的形式,代理当前的 url
请求。
而指定这个代理非常简单,是一种近乎固定的配置方案。
在 vue.config.js
中,加入以下代码:
module.exports = {
devServer: {
// 配置反向代理
proxy: {
// 当地址中有/api的时候会触发代理机制
'/api': {
// 要代理的服务器地址 这里不用写 api
target: 'https://api.imooc-admin.lgdsunday.club/',
changeOrigin: true // 是否跨域
}
}
},
...
}
保存服务端返回的 token
1. 在 vuex
的 user
模块下,处理 token
的保存
import { login } from '@/api/sys'
import md5 from 'md5'
import { setItem, getItem } from '@/utils/storage'
import { TOKEN } from '@/constant'
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.token)
resolve()
})
...
})
}
}
}
2. 响应数据统一处理 (获取token数据简化操作,请求、响应拦截器配置)
在 utils/request.js
中实现以下代码
import axios from 'axios'
import { ElMessage } from 'element-plus'
...
// 响应拦截器
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)
}
)
export default service
登录鉴权
在 main.js
平级,创建 permission
文件
import router from './router'
import store from './store'
// 白名单
const whiteList = ['/login']
/**
* 路由前置守卫
*/
router.beforeEach(async (to, from, next) => {
// 存在 token ,进入主页
// if (store.state.user.token) {
// 快捷访问
if (store.getters.token) {
if (to.path === '/login') {
next('/')
} else {
next()
}
} else {
// 没有token的情况下,可以进入白名单
if (whiteList.indexOf(to.path) > -1) {
next()
} else {
next('/login')
}
}
})
在此处我们使用到了 vuex 中的 getters
,此时的 getters
被当作 快捷访问 的形式进行访问
所以我们需要声明对应的模块,创建 store/getters
const getters = {
token: state => state.user.token
}
export default getters
在 store/index
中进行导入:
import getters from './getters'
export default createStore({
getters,
...
})
退出登录方案
-
用户主动退出
点击退出,清空token -
用户被动退出
token
过期或被 其他人”顶下来“ 时退出
1. 主动方案
2. 被动方案
核心:
-
清理掉当前用户缓存数据
-
清理掉权限相关配置
-
返回到登录页
被动退出的主动方案之前端主动介入 token
时效的处理
-
在用户登陆时,记录当前 登录时间
-
制定一个 失效时长
-
在接口调用时,根据 当前时间 对比 登录时间 ,看是否超过了 时效时长
-
如果未超过,则正常进行后续操作
-
如果超过,则进行 退出登录 操作
-
创建 utils/auth.js
文件,并写入以下代码
import { TIME_STAMP, TOKEN_TIMEOUT_VALUE } from '@/constant'
import { setItem, getItem } from '@/utils/storage'
/**
* 获取时间戳
*/
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
}
在 constant
中声明对应常量
// token 时间戳
export const TIME_STAMP = 'timeStamp'
// 超时时长(毫秒) 两小时
export const TOKEN_TIMEOUT_VALUE = 2 * 3600 * 1000
在用户登录成功之后去设置时间,到 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
过期 -
单用户登录
服务器返回状态
核心:在请求时,服务器返回的状态码辨认
在 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)
}
)