实现原理
登录时 后端会返回 两个 token
token:短期有效的临时凭证,正常请求时使用。
refreshToken:长期有效的凭证(几天到几个月),token 过期后通过 refreshToken 重新获取新 token。
请求拦截器:负责给请求头设置对应的 token。
响应拦截器(重点):每一次响应时,响应头会携带 token返回,通过 localStorage 或 pinia 等进行全局存储。
无感刷新:当客户端请求时发现token过期,自动向服务器获取新的token,无需重新登录和页面刷新。
封装 axios
// request.js
import axios from "axios";
import { useUserStore } from '@/stores'
import { apiToken } from "@/api/api";
const instance = axios.create({
// todo 1.基础地址,超时时间
baseURL: '',
timeout: 10000
})
// 请求拦截器
instance.interceptors.request.use(
(config) => {
const userStore = useUserStore()
// 通过请求体自定义参数 __isRefreshToken 判断 传递 token 还是 refreshToken
config.headers['Authorization'] = !config.__isRefreshToken ? `Bearer ${userStore.token}` : `Bearer ${userStore.refreshToken}`
return config
},
(err) => Promise.reject(err)
)
// 响应拦截器
instance.interceptors.response.use(async (res) => {
const userStore = useUserStore()
// 判断 响应头 是否又返回 token 值
if (res.headers.Authorization) {
// 提取 token 值,全局保存和给请求头赋予最新 token 值
const token = res.headers.Authorization.replace('Bearer ', '')
userStore.setToken(token)
}
// 判断 响应头 是否又返回 refreshtoken 值
if (res.headers.refreshtoken) {
// 提取 refreshtoken 值并全局保存
const refreshtoken = res.headers.Authorization.replace('Bearer ', '')
userStore.setRefreshToken(refreshtoken)
}
// 状态码401:token 过期 且不是刷新 token 的请求
if (res.data.code === 401 && !res.config.__isRefreshToken) {
const tokenStatus = await refreshToken()
if (tokenStatus) {
// 重新赋值 token
res.config.headers.Authorization = `Bearer ${userStore.token}`
// 重新请求
const resp = await instance.request(res.config)
return resp
} else {
// 错误的特色情况 ==> 401 权限不足 或 token 过期 =>拦截到登录页面
console.log('重新登录');
// userStore.delToken()
// userStore.delRefreshToken()
return res.data
}
}
// 正常返回数据
return res.data
})
export default instance
// 通过 refreshToken函数 刷新 token
let promise
function refreshToken() {
// promise 防止多次请求和并发请求时连续刷新 token
if (promise) return promise
promise = new Promise(async (resolve) => {
// 刷新 token
const res = await apiToken()
// 返回是否刷新成功
resolve(res.code === 200)
})
promise.finally(() => {
promise = null // 清空 promise
})
return promise
}
api.js
// api.js
import request from '@/utils/request'
// 登录
export function apiLogin() {
return request.get(`/api/login`)
}
// 获取信息
export function apiInfo() {
return request.get(`/api/info`)
}
// 刷新token
export function apiToken() {
return request.get(`/api/token`, {
__isRefreshToken: true
})
}
pinia
import { ref } from 'vue'
import { defineStore } from 'pinia'
// 用户数据
export const useUserStore = defineStore(
'user',
() => {
const token = ref('')
const refreshToken = ref('')
// 设置 token
const setToken = (access_token) => {
token.value = access_token
}
//设置刷新token
const setRefreshToken = (refresh_token) => {
refreshToken.value = refresh_token
}
// 删除Token
const delToken = () => {
token.value = ''
}
// 删除Token
const delRefreshToken = () => {
refreshToken.value = ''
}
return {
token,
refreshToken,
setRefreshToken,
setToken,
delRefreshToken,
delToken
}
}
)
页面
<script setup>
import { apiLogin,apiInfo } from "@/api/api";
const login =async ()=>{
await apiLogin()
console.log('登录了');
}
const getInfo =async ()=>{
const res = await apiInfo()
console.log(res.msg);
}
</script>
<template>
<div class="btn" @click="login">登录</div>
<div class="btn" @click="getInfo">登录后获取信息</div>
</template>
<style scoped lang="scss">
.btn {
width: 200px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
background: skyblue;
color: #fff;
border-radius: 20px;
}
</style>