本文实例代码使用的是vue+axiosÏ
什么是Token
Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
请求后台返回的登录数据一般情况如下
{
access_token:"加密的字符串",
expires_in:"7200",
refresh_token:"加密的字符串",
}
- access_token (访问令牌,用于资源访问)
- refresh_token ( 当访问令牌失效,使用这个令牌重新获取访问令牌)
- expire_in( access_tokenÏ过期时间)
基本使用
因为业务模式多种多样所以使用方法也是有很多的(ps:主要是后端想要什么,我们就给什么),比较常见的是Header中携带Token。
Token获取及使用
- 接口封装
/** * 用户请求模块 */ export const login = (dataÏ) => request({ url: '/front/user/login', method: 'POST', data: qs.stringify(data) })
- VueX基本使用
export default new Vuex.Store({ state: { // 初始化 user: null }, mutations: { // 设置用户登录信息 setUser(state, payload) { //因目前后端返回的是json字符串,所以我转义了一下 payload = JSON.parse(payload) //如果pyload中没有过期时间并且存在过期时间长度 if (!payload.expires_at && payload.expires_in) { //设置过期时间 payload.expires_at = new Date().getTime() + payload.expires_in * 1000 } //赋值 state.user = payload } }, actions: { }, modules: { } })
- 登录页面使用(伪代码)
import Vue from 'vue' import { login } from '@/services/user' export default Vue.extend({ name: 'LoginIndex', data() { return { formData: { phone: '18201288771', password: '111111' } } }, methods: { async submit() { try { const { data } = await login(this.formData) // 处理请求结果 if (data.state !== 1) { this.$message.error(data.message) } else { // 使用vuex中的setUser共享登录信息 this.$store.commit('setUser', data.content) this.$message.success('登录成功') } } catch (error) {} this.isLoading = false } } })
- axios请求拦截器
// 请求拦截器,每一个请求都会经过此拦截器。 request.interceptors.request.use((config) => { // 在请求的header中设置token config.headers.Authorization = store.state?.user?.access_token return config }, (error) => { return Promise.reject(error) })
优化——授权过期登录重新返回页面
request.js中
// 跳转至首页封装
const redirectLogin = () => {
router.push({
name: 'login',
query: {
// 通过参数传 登录成功后的跳转地址
redirect: router.currentRoute.fullPath
}
})
}
登录页面
methods: {
// 登录请求方法
async submit() {
try {
const { data } = await login(this.formData)
// 处理请求结果
if (data.state !== 1) {
//..... 登录失败处理逻辑
} else {
//..... 登录成功处理逻辑
// 登录成功后进行路由跳转
this.$router.push((this.$route.query.redirect as string) || '/')
}
} catch (error) {}
}
}
优化——页面刷新Token丢失
export default new Vuex.Store({
state: {
// 初始化时从本地存储中获取
user: JSON.parse(window.localStorage.getItem('user') || 'null')
},
mutations: {
//设置用户登录信息
setUser(state, payload) {
//因目前后端返回的是json字符串,所以我转义了一下
payload = JSON.parse(payload)
//如果pyload中没有过期时间并且存在过期时间长度
if (!payload.expires_at && payload.expires_in) {
//设置过期时间
payload.expires_at = new Date().getTime() + payload.expires_in * 1000
}
//赋值
state.user = payload
//每次设置用户登录信息都存储值本地存储
window.localStorage.setItem('user', JSON.stringify(payload))
}
},
actions: {
},
modules: {
}
})
过期维护
过期维护存前端存在两种方式
- 在请求发起前拦截每个请求,判断token的有效时间是否已经过期。若已过期,则将请求挂起,先刷新token后在继续请求。
- 优点:请求前拦截,节省请求及流量
- 缺点:需要后端额外提供过期时间字段,若本地时间与服务器时间不一致可能存在拦截失败。
- 不在请求前拦截,而是拦截返回后的数据。先放弃请求,接口返回过期后,先刷新token,在进行一次重试。
- 优点:不需要额外的token过期字段及判断时间
- 缺点:会消耗多一次请求,耗流量
请求发起前拦截
// 跳转首页逻辑
const redirectLogin = () => {
router.push({
name: 'login',
query: {
redirect: router.currentRoute.fullPath
}
})
}
// 刷新token后的任务队列
let refreshTokenArray = []
/**
* 刷新token,重新请求
*/
const refreshTokenFn = async () => {
// 判断是否有刷新token
const refreshToken = store.state?.user?.refresh_token || ''
// 如果刷新token存在
if (refreshToken) {
// 使用重新创建的axios请求,防止递归调用
const { data } = await axios.create()({
method: 'POST',
url: '/front/user/refresh_token',
data: qs.stringify({
refreshtoken: refreshToken
})
})
//如果获取token失败 抛出异常
if (!data.content) throw new Error('refreshToken is faild')
// 重新设置token
store.commit('setUser', data.content)
return true
}
throw new Error('refreshToken not find')
}
// 请求拦截器
request.interceptors.request.use(async (config: Config) => {
// 获取用户登录信息
const user = store.state?.user
// 判断access_token 是否过期且接口是否需要token
if (config.isAuthToken && user.expires_at < new Date().getTime()) {
// 是否正在执行刷新token
if (!refreshTokenLoding) {
try {
//刷新token锁为true
refreshTokenLoding = true
await refreshTokenFn()
// 执行获取token后的任务队列
refreshTokenArray.forEach(item => item())
//清空任务队列
refreshTokenArray = []
return config
} catch (error) {
// 如果刷新失败跳转登录页面
redirectLogin()
} finally {
// 无论成功失败消除
refreshTokenLoding = false
}
} else {
// 如果这正在刷新,返回一个 Promise ,并向刷新token成功后执行队列push 函数.
return new Promise(resolve => {
refreshTokenArray.push(() => {
// 返回config请求对象
resolve(config)
})
})
}
}
return config
})
请求发起后拦截
//相应拦截器
request.interceptors.response.use((response) => {
// 2xx 会进入这里
return response
}, async (error) => {
// 判断是否是授权错误
if (error.response === 401) {
// 是否正在刷新
if (!refreshTokenLoding) {
refreshTokenLoding = true
// 尝试使用 refresh_token 获取新的 access_token
try {
// 执行刷新token
await refreshTokenFn()
// 执行刷新后任务队列
refreshTokenArray.forEach(item => item())
//清除任务队列
refreshTokenArray = []
// 重发当前请求
return request(error.config)
// 如果成功 则重发上次请求
} catch (error) {
// 如果失败 跳转至登录
redirectLogin()
} finally {
refreshTokenLoding = false
}
} else {
// 如果当前正在请求
return new Promise(resolve => {
// 当前请求的config投递至刷新后的任务队列中
refreshTokenArray.push(() => {
resolve(request(error.config))
})
})
}
}
//... 其他异常捕获
})
结束语
虽然token大家平常工作中都会使用,但是我见过太多的项目token使用上存在误区。例如为了避免token过期问题,让token的有效期为一周,还有些人甚至设置了一年(手动滑稽)。还有一些人只设置了拦截器,例如请求发现token过期或者后端返回了401,直接让用户跳转至登录页面,这样的用户体验真的很不优化。