feat: 新增请求层拦截器、鉴权store与会话空闲检测
parent
3ee4aa6ee7
commit
fb22f8d99b
|
|
@ -0,0 +1,7 @@
|
|||
import request from './request'
|
||||
|
||||
export const sendSmsApi = (data) => request.post('/auth/sms/send', data)
|
||||
export const loginApi = (data) => request.post('/auth/login', data)
|
||||
export const logoutApi = () => request.post('/auth/logout')
|
||||
export const fetchUserInfoApi = () => request.get('/auth/userinfo')
|
||||
export const fetchMenuApi = () => request.get('/auth/menu')
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import axios from 'axios'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
export function getBaseUrl() {
|
||||
return localStorage.getItem('base_url_override') || import.meta.env.VITE_API_BASE_URL
|
||||
}
|
||||
|
||||
export function setBaseUrlOverride(url) {
|
||||
if (url) localStorage.setItem('base_url_override', url)
|
||||
else localStorage.removeItem('base_url_override')
|
||||
}
|
||||
|
||||
const request = axios.create({ withCredentials: true, timeout: 15000 })
|
||||
|
||||
request.interceptors.request.use((config) => {
|
||||
config.baseURL = getBaseUrl()
|
||||
const authStore = useAuthStore()
|
||||
if (authStore.accessToken) {
|
||||
config.headers.Authorization = `Bearer ${authStore.accessToken}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
let isRefreshing = false
|
||||
let waitQueue = []
|
||||
|
||||
async function refreshAccessToken() {
|
||||
const authStore = useAuthStore()
|
||||
const res = await axios.post(`${getBaseUrl()}/auth/refresh`, {}, { withCredentials: true })
|
||||
if (res.data.code !== 200) throw new Error('refresh failed')
|
||||
authStore.setAccessToken(res.data.data.accessToken)
|
||||
return res.data.data.accessToken
|
||||
}
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const { code, msg } = response.data || {}
|
||||
if (code !== undefined && code !== 200) {
|
||||
message.error(msg || '请求失败')
|
||||
}
|
||||
return response
|
||||
},
|
||||
async (error) => {
|
||||
const authStore = useAuthStore()
|
||||
const { response, config } = error
|
||||
if (response?.status === 401 && config && !config._retried) {
|
||||
config._retried = true
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true
|
||||
try {
|
||||
const token = await refreshAccessToken()
|
||||
isRefreshing = false
|
||||
waitQueue.forEach((cb) => cb(token))
|
||||
waitQueue = []
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
return request(config)
|
||||
} catch (e) {
|
||||
isRefreshing = false
|
||||
waitQueue = []
|
||||
authStore.clear()
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
waitQueue.push((token) => {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
resolve(request(config))
|
||||
})
|
||||
})
|
||||
}
|
||||
if (response?.status === 403) {
|
||||
message.error('无权限访问该资源')
|
||||
}
|
||||
if (!response) {
|
||||
message.error('网络异常,请检查后端服务是否可用')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { loginApi, logoutApi, fetchMenuApi } from '@/api/auth'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
accessToken: localStorage.getItem('access_token') || '',
|
||||
userInfo: null,
|
||||
menuTree: [],
|
||||
buttonCodes: [],
|
||||
routesReady: false
|
||||
}),
|
||||
actions: {
|
||||
setAccessToken(token) {
|
||||
this.accessToken = token
|
||||
localStorage.setItem('access_token', token)
|
||||
},
|
||||
async login(payload) {
|
||||
const res = await loginApi(payload)
|
||||
if (res.data.code === 200) {
|
||||
const { accessToken, userInfo, menuTree, buttonCodes } = res.data.data
|
||||
this.setAccessToken(accessToken)
|
||||
this.userInfo = userInfo
|
||||
this.menuTree = menuTree
|
||||
this.buttonCodes = buttonCodes
|
||||
}
|
||||
return res.data
|
||||
},
|
||||
async fetchMenu() {
|
||||
const res = await fetchMenuApi()
|
||||
if (res.data.code === 200) {
|
||||
this.menuTree = res.data.data.menuTree
|
||||
this.buttonCodes = res.data.data.buttonCodes
|
||||
}
|
||||
return res.data
|
||||
},
|
||||
async logout() {
|
||||
await logoutApi()
|
||||
this.clear()
|
||||
},
|
||||
clear() {
|
||||
this.accessToken = ''
|
||||
this.userInfo = null
|
||||
this.menuTree = []
|
||||
this.buttonCodes = []
|
||||
this.routesReady = false
|
||||
localStorage.removeItem('access_token')
|
||||
},
|
||||
markRoutesReady() {
|
||||
this.routesReady = true
|
||||
},
|
||||
hasButton(code) {
|
||||
return this.buttonCodes.includes(code)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
const ACTIVE_EVENTS = ['mousemove', 'mousedown', 'keydown', 'scroll', 'click']
|
||||
|
||||
export function startIdleWatcher(onTimeout, timeoutMs = 5 * 60 * 1000) {
|
||||
let timer = null
|
||||
|
||||
function reset() {
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(onTimeout, timeoutMs)
|
||||
}
|
||||
|
||||
ACTIVE_EVENTS.forEach((evt) => window.addEventListener(evt, reset))
|
||||
reset()
|
||||
|
||||
return function stop() {
|
||||
if (timer) clearTimeout(timer)
|
||||
ACTIVE_EVENTS.forEach((evt) => window.removeEventListener(evt, reset))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue