实现Navbar的功能
处理完了基本的Layout架构之后,接着我们实现一下navbar中的头像菜单功能;
这功能分为3个部分:
- 获取并展示用户信息;
- element-plus中的dropdown组件的使用;
- 退出登录方案的实现;
获取并展示用户信息
获取并展示用户信息的步骤:
- 定义接口请求方法
- 定义调用接口的动作;
- 在权限拦截时触发动作;
那么接下来我们根据这三个步骤,分别进行实现:
定义接口请求方法
在api/sys.js文件中新增方法:
api/sys.js
// 获取用户信息
export const getUserInfo = () => {
return request({
url: '/project/getuser'
})
}
因为获取用户信息需要对应的token,所以我们可以利用axios的请求拦截器对token进行统一注入,在utils/request.js
中写入代码;
utils/request.js
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 在这个位置需要统一的注入token
if (store.getter.token) {
// 如果token存在,注入token
config.headers.Authorization = `Bearer ${store.getters.token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
定义调用接口的动作
在store/modules/user.js
中增加action;
store/modules/user.js
export default {
namespaced: true,
state: () => ({
token: getItem(TOKEN) || '',
userInfo: {}
}),
mutations: {
setUserInfo(state, userInfo) {
state.userInfo = userInfo
}
},
actions: {
/**
* 获取用户信息
*/
async getUserInfo(context) {
const res = await getUserInfo()
this.commit('user/setUserInfo', res)
return res
}
}
}
在权限拦截时触发动作
在store/getters.js增加一个:
store/getters.js
const getters = {
token: (state) => state.user.token,
/**
*
* @returns true 表示用户信息已存在
*/
hasUserInfo: (state) => {
return JSON.stringify(state.user.userInfo) !== '{}'
}
}
export default getters
permissions.js
/**
* 处理前置守卫
* to: 要到哪里去
* from: 从哪里来
* next: 是否要去
*/
router.beforeEach(async (to, from, next) => {
// 1. 用户已登录,则不允许进入login
// 2. 用户未登录,则不允许出login
// 用token判断是否登录
if (store.getters.token) {
if (to.path === '/login') {
next('/')
} else {
// 判断用户资料是否存在,如果不存在,则获取用户信息
if (!store.getters.hasUserInfo) {
await store.dispatch('user/getUserInfo')
}
next()
}
} else {
if (whiteList.includes(to.path)) {
next()
} else {
next('/login')
}
}
return false
})
渲染用户头像菜单
上节我们成功获取用户信息;接着可以根据数据渲染出用户头像内容;这里的头像菜单是使用element-plus中的dropdown组件和avatar;
在layout/components/navbar.js中实现:
首先完成头像展示的页面搭建;
layout/components/navbar.js
<template>
<div class="navbar">
<div class="right-menu">
<el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper">
<el-avatar
shape="square"
:size="40"
:src="$store.getters.userInfo.avatar"
></el-avatar>
<i class="el-icon-s-tools"></i>
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/"
><el-dropdown-item>主页</el-dropdown-item></router-link
>
<a target="__blank" href="#"
><el-dropdown-item>课程主页</el-dropdown-item></a
>
<router-link to="/"
><el-dropdown-item divided
>退出登录</el-dropdown-item
></router-link
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
接着来添加样式;
layout/components/navbar.js
<style lang="scss" scoped>
.navbar {
height: 50px;
position: relative;
background-color: white;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.88);
display: flex;
justify-content: center;
.left-menu {
margin-right: auto;
}
.right-menu {
display: flex;
align-items: center;
padding-right: 16px;
::v-deep .avatar-container {
cursor: pointer;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.el-avatar {
/**去掉头像的背景色 */
--el-avatar-background-color: none;
margin-right: 12px;
}
}
}
}
}
</style>
退出登录方案实现
退出登录有两种:
- 用户主动退出:用户点击退出登录按钮后退出;
- 用户被动退出:token过期或被其他人顶下来时退出;
无论哪种方式退出,所需要执行的操作都是固定的:
- 清理当前用户缓存数据;
- 清理权限相关的配置;
- 返回到登录页面;
明确好对应的方案之后,接下来咱们先来实现用户主动退出的对应策略;
用户主动退出登录
在store/modules/user.js
中,添加对应的action;
store/modules/user.js
/**
* 用户主动退出
*/
logout() {
this.commit('user/setToken', '')
this.commit('user/setUserInfo', '')
removeAll()
// TODO:还需要清理权限相关
// 返回到登录页
router.push('/login')
}
Navbar.vue
const logout = () => {
store.dispatch('user/logout')
}
用户被动退出
用户被动退出有两个场景:
- token失效;
- 单点登录:其他人登录该账号被"顶下来"
主动处理
我们知道token表示了一个用户的身份令牌,对服务端而言,它是只认令牌不认人的,所以说一旦其他人获取到了你的token,那么就可以伪装成你,来获取敏感数据; 为了保证用户的信息安全,对token而言就被指定了很多安全策略,比如:
- 动态token(可变token);
- 刷新token;
- 时效token
- ...
我们这里选择的是时效token;token本来就有时效,但是通常情况下,这个时效都是在服务端进行处理的,而此时我们要在服务端处理token时效的同时,在前端主动介入token时效的处理中。从而保证用户信息更加的安全;
那么我们如何实现呢,分为以下的步骤:
1.在用户登录时,记录当前的登录时间 ;
2.制定一个失效时长 ;
3.在接口调用时,根据当前时间 对比登录时间 ,看是否超过了失效时长;
- 如果未超过,则正常进行后续操作;
- 如果超过,则进行退出登录操作;
创建utils/auth.js文件:
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 checkTimeout() {
// 获取当前时间
const nowTime = Date.now()
// 缓存时间
const timeStamp = getTimeStamp()
const res = nowTime - timeStamp > TOKEN_TIMEOUT_VALUE
return res
}
在constant/index.js增加两个常量:
constant/index.js
// token时间戳
export const TIME_STAMP = 'timeStamp'
// 超时时长
export const TOKEN_TIMEOUT_VALUE = 2 * 3600
然后就可以去用我们的时间戳方法了;
store/modules/user.js
login方法中
// 保存登录时间
setTimeStamp()
在utils/request.js的请求拦截器中判断token是否过期;
utils/request.js
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 在这个位置需要统一的注入token
if (store.getters && store.getters.token) {
// 判断token是否过期
if (checkTimeout()) {
// 超时,执行退出操作
store.dispatch('user/logout')
return Promise.reject(new Error('token 失效了'))
}
// 如果token存在,注入token
config.headers.Authorization = `Bearer ${store.getters.token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
被动处理
被动处理有两种业务场景:
- token过期;
- 单点登录;
这里的token过期是指服务端生成的token超过服务端指定的失效过程;
单点登录是指:
- 当用户A登录之后,token过期之前;
- 用户A的账户在其他设备上进行了二次登录,导致第一次登录的A账号被"顶"下线;
- 即:同一账户仅可以在一个设备上保持在线状态;
从背景中知道,以上的两种情况,都是服务端进行判断的 ,是服务端通知前端;
对于其业务逻辑,将遵循以下逻辑:
- 服务端返回数据时,会通过特定的状态码通知前端;
- 当前端接收到特定的状态码时,表示遇到了特定状态: token失效 或单点登录;
- 此时进行退出登录处理;
这里允许多台设备登录同一账号 ,所以不会指定单点登录 状态码;仅有token失效 状态码。如果需要做单点登录时,只需要多加一个状态码判断;
在utils/request.js的响应拦截器增加:
utils/request.js
service.interceptors.response.use(
(err) => {
// 处理token超时的问题
if (err.response && err.response.data && err.response.data.code === 401) {
// token超时
store.dispatch('user/logout')
}
ElMessage.error(err)
return Promise.reject(new Error(err))
}
)