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