Browse Source
* feat: refreshToken * chore: store refreshToken * chore: generate token using jsonwebtoken * chore: set refreshToken in httpOnly cookie * perf: authHeader verify * chore: add add response interceptor * chore: test refresh * chore: handle logout * chore: type * chore: update pnpm-lock.yaml * chore: remove test code * chore: add todo comment * chore: update pnpm-lock.yaml * chore: remove default interceptors * chore: copy codes * chore: handle refreshToken invalid * chore: add refreshToken preference * chore: typo * chore: refresh token逻辑调整 * refactor: interceptor presets * chore: copy codes * fix: ci errors * chore: add missing await * feat: 完善refresh-token逻辑及文档 * fix: ci error * chore: filename --------- Co-authored-by: vince <vince292007@gmail.com>pull/4195/head
committed by
GitHub
40 changed files with 1049 additions and 517 deletions
@ -1 +1,3 @@ |
|||
PORT=5320 |
|||
ACCESS_TOKEN_SECRET=access_token_secret |
|||
REFRESH_TOKEN_SECRET=refresh_token_secret |
|||
|
|||
@ -1,15 +1,14 @@ |
|||
export default eventHandler((event) => { |
|||
const token = getHeader(event, 'Authorization'); |
|||
import { verifyAccessToken } from '~/utils/jwt-utils'; |
|||
import { unAuthorizedResponse } from '~/utils/response'; |
|||
|
|||
if (!token) { |
|||
setResponseStatus(event, 401); |
|||
return useResponseError('UnauthorizedException', 'Unauthorized Exception'); |
|||
export default eventHandler((event) => { |
|||
const userinfo = verifyAccessToken(event); |
|||
if (!userinfo) { |
|||
return unAuthorizedResponse(event); |
|||
} |
|||
|
|||
const username = Buffer.from(token, 'base64').toString('utf8'); |
|||
|
|||
const codes = |
|||
MOCK_CODES.find((item) => item.username === username)?.codes ?? []; |
|||
MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? []; |
|||
|
|||
return useResponseSuccess(codes); |
|||
}); |
|||
|
|||
@ -1,20 +1,36 @@ |
|||
import { |
|||
clearRefreshTokenCookie, |
|||
setRefreshTokenCookie, |
|||
} from '~/utils/cookie-utils'; |
|||
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils'; |
|||
import { forbiddenResponse } from '~/utils/response'; |
|||
|
|||
export default defineEventHandler(async (event) => { |
|||
const { password, username } = await readBody(event); |
|||
if (!password || !username) { |
|||
setResponseStatus(event, 400); |
|||
return useResponseError( |
|||
'BadRequestException', |
|||
'Username and password are required', |
|||
); |
|||
} |
|||
|
|||
const findUser = MOCK_USERS.find( |
|||
(item) => item.username === username && item.password === password, |
|||
); |
|||
|
|||
if (!findUser) { |
|||
setResponseStatus(event, 403); |
|||
return useResponseError('UnauthorizedException', '用户名或密码错误'); |
|||
clearRefreshTokenCookie(event); |
|||
return forbiddenResponse(event); |
|||
} |
|||
|
|||
const accessToken = Buffer.from(username).toString('base64'); |
|||
const accessToken = generateAccessToken(findUser); |
|||
const refreshToken = generateRefreshToken(findUser); |
|||
|
|||
setRefreshTokenCookie(event, refreshToken); |
|||
|
|||
return useResponseSuccess({ |
|||
...findUser, |
|||
accessToken, |
|||
// TODO: refresh token
|
|||
refreshToken: accessToken, |
|||
}); |
|||
}); |
|||
|
|||
@ -0,0 +1,15 @@ |
|||
import { |
|||
clearRefreshTokenCookie, |
|||
getRefreshTokenFromCookie, |
|||
} from '~/utils/cookie-utils'; |
|||
|
|||
export default defineEventHandler(async (event) => { |
|||
const refreshToken = getRefreshTokenFromCookie(event); |
|||
if (!refreshToken) { |
|||
return useResponseSuccess(''); |
|||
} |
|||
|
|||
clearRefreshTokenCookie(event); |
|||
|
|||
return useResponseSuccess(''); |
|||
}); |
|||
@ -0,0 +1,33 @@ |
|||
import { |
|||
clearRefreshTokenCookie, |
|||
getRefreshTokenFromCookie, |
|||
setRefreshTokenCookie, |
|||
} from '~/utils/cookie-utils'; |
|||
import { verifyRefreshToken } from '~/utils/jwt-utils'; |
|||
import { forbiddenResponse } from '~/utils/response'; |
|||
|
|||
export default defineEventHandler(async (event) => { |
|||
const refreshToken = getRefreshTokenFromCookie(event); |
|||
if (!refreshToken) { |
|||
return forbiddenResponse(event); |
|||
} |
|||
|
|||
clearRefreshTokenCookie(event); |
|||
|
|||
const userinfo = verifyRefreshToken(refreshToken); |
|||
if (!userinfo) { |
|||
return forbiddenResponse(event); |
|||
} |
|||
|
|||
const findUser = MOCK_USERS.find( |
|||
(item) => item.username === userinfo.username, |
|||
); |
|||
if (!findUser) { |
|||
return forbiddenResponse(event); |
|||
} |
|||
const accessToken = generateAccessToken(findUser); |
|||
|
|||
setRefreshTokenCookie(event, refreshToken); |
|||
|
|||
return accessToken; |
|||
}); |
|||
@ -1,14 +1,13 @@ |
|||
export default eventHandler((event) => { |
|||
const token = getHeader(event, 'Authorization'); |
|||
import { verifyAccessToken } from '~/utils/jwt-utils'; |
|||
import { unAuthorizedResponse } from '~/utils/response'; |
|||
|
|||
if (!token) { |
|||
setResponseStatus(event, 401); |
|||
return useResponseError('UnauthorizedException', 'Unauthorized Exception'); |
|||
export default eventHandler((event) => { |
|||
const userinfo = verifyAccessToken(event); |
|||
if (!userinfo) { |
|||
return unAuthorizedResponse(event); |
|||
} |
|||
|
|||
const username = Buffer.from(token, 'base64').toString('utf8'); |
|||
|
|||
const menus = |
|||
MOCK_MENUS.find((item) => item.username === username)?.menus ?? []; |
|||
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? []; |
|||
return useResponseSuccess(menus); |
|||
}); |
|||
|
|||
@ -1,14 +1,11 @@ |
|||
import { verifyAccessToken } from '~/utils/jwt-utils'; |
|||
import { unAuthorizedResponse } from '~/utils/response'; |
|||
|
|||
export default eventHandler((event) => { |
|||
const token = getHeader(event, 'Authorization'); |
|||
if (!token) { |
|||
setResponseStatus(event, 401); |
|||
return useResponseError('UnauthorizedException', 'Unauthorized Exception'); |
|||
const userinfo = verifyAccessToken(event); |
|||
if (!userinfo) { |
|||
return unAuthorizedResponse(event); |
|||
} |
|||
|
|||
const username = Buffer.from(token, 'base64').toString('utf8'); |
|||
|
|||
const user = MOCK_USERS.find((item) => item.username === username); |
|||
|
|||
const { password: _pwd, ...userInfo } = user; |
|||
return useResponseSuccess(userInfo); |
|||
return useResponseSuccess(userinfo); |
|||
}); |
|||
|
|||
@ -0,0 +1,26 @@ |
|||
import type { EventHandlerRequest, H3Event } from 'h3'; |
|||
|
|||
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) { |
|||
deleteCookie(event, 'jwt', { |
|||
httpOnly: true, |
|||
sameSite: 'none', |
|||
secure: true, |
|||
}); |
|||
} |
|||
|
|||
export function setRefreshTokenCookie( |
|||
event: H3Event<EventHandlerRequest>, |
|||
refreshToken: string, |
|||
) { |
|||
setCookie(event, 'jwt', refreshToken, { |
|||
httpOnly: true, |
|||
maxAge: 24 * 60 * 60 * 1000, |
|||
sameSite: 'none', |
|||
secure: true, |
|||
}); |
|||
} |
|||
|
|||
export function getRefreshTokenFromCookie(event: H3Event<EventHandlerRequest>) { |
|||
const refreshToken = getCookie(event, 'jwt'); |
|||
return refreshToken; |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
import type { EventHandlerRequest, H3Event } from 'h3'; |
|||
|
|||
import jwt from 'jsonwebtoken'; |
|||
|
|||
import { UserInfo } from './mock-data'; |
|||
|
|||
export interface UserPayload extends UserInfo { |
|||
iat: number; |
|||
exp: number; |
|||
} |
|||
|
|||
export function generateAccessToken(user: UserInfo) { |
|||
return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '2h' }); |
|||
} |
|||
|
|||
export function generateRefreshToken(user: UserInfo) { |
|||
return jwt.sign(user, process.env.REFRESH_TOKEN_SECRET, { |
|||
expiresIn: '30d', |
|||
}); |
|||
} |
|||
|
|||
export function verifyAccessToken( |
|||
event: H3Event<EventHandlerRequest>, |
|||
): null | Omit<UserInfo, 'password'> { |
|||
const authHeader = getHeader(event, 'Authorization'); |
|||
if (!authHeader?.startsWith('Bearer')) { |
|||
return null; |
|||
} |
|||
|
|||
const token = authHeader.split(' ')[1]; |
|||
try { |
|||
const decoded = jwt.verify( |
|||
token, |
|||
process.env.ACCESS_TOKEN_SECRET, |
|||
) as UserPayload; |
|||
|
|||
const username = decoded.username; |
|||
const user = MOCK_USERS.find((item) => item.username === username); |
|||
const { password: _pwd, ...userinfo } = user; |
|||
return userinfo; |
|||
} catch { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
export function verifyRefreshToken( |
|||
token: string, |
|||
): null | Omit<UserInfo, 'password'> { |
|||
try { |
|||
const decoded = jwt.verify( |
|||
token, |
|||
process.env.REFRESH_TOKEN_SECRET, |
|||
) as UserPayload; |
|||
const username = decoded.username; |
|||
const user = MOCK_USERS.find((item) => item.username === username); |
|||
const { password: _pwd, ...userinfo } = user; |
|||
return userinfo; |
|||
} catch { |
|||
return null; |
|||
} |
|||
} |
|||
@ -1,67 +1,101 @@ |
|||
/** |
|||
* 该文件可自行根据业务逻辑进行调整 |
|||
*/ |
|||
import type { HttpResponse } from '@vben/request'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { RequestClient } from '@vben/request'; |
|||
import { |
|||
authenticateResponseInterceptor, |
|||
errorMessageResponseInterceptor, |
|||
RequestClient, |
|||
} from '@vben/request'; |
|||
import { useAccessStore } from '@vben/stores'; |
|||
|
|||
import { message } from 'ant-design-vue'; |
|||
|
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
import { refreshTokenApi } from './core'; |
|||
|
|||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
|||
|
|||
function createRequestClient(baseURL: string) { |
|||
const client = new RequestClient({ |
|||
baseURL, |
|||
// 为每个请求携带 Authorization
|
|||
makeAuthorization: () => { |
|||
return { |
|||
// 默认
|
|||
key: 'Authorization', |
|||
tokenHandler: () => { |
|||
const accessStore = useAccessStore(); |
|||
return { |
|||
refreshToken: `${accessStore.refreshToken}`, |
|||
token: `${accessStore.accessToken}`, |
|||
}; |
|||
}, |
|||
unAuthorizedHandler: async () => { |
|||
const accessStore = useAccessStore(); |
|||
const authStore = useAuthStore(); |
|||
accessStore.setAccessToken(null); |
|||
|
|||
if (preferences.app.loginExpiredMode === 'modal') { |
|||
accessStore.setLoginExpired(true); |
|||
} else { |
|||
// 退出登录
|
|||
await authStore.logout(); |
|||
} |
|||
}, |
|||
}; |
|||
}, |
|||
makeErrorMessage: (msg) => message.error(msg), |
|||
}); |
|||
|
|||
/** |
|||
* 重新认证逻辑 |
|||
*/ |
|||
async function doReAuthenticate() { |
|||
console.warn('Access token or refresh token is invalid or expired. '); |
|||
const accessStore = useAccessStore(); |
|||
const authStore = useAuthStore(); |
|||
accessStore.setAccessToken(null); |
|||
if (preferences.app.loginExpiredMode === 'modal') { |
|||
accessStore.setLoginExpired(true); |
|||
} else { |
|||
await authStore.logout(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 刷新token逻辑 |
|||
*/ |
|||
async function doRefreshToken() { |
|||
const accessStore = useAccessStore(); |
|||
const resp = await refreshTokenApi(); |
|||
const newToken = resp.data; |
|||
accessStore.setAccessToken(newToken); |
|||
return newToken; |
|||
} |
|||
|
|||
function formatToken(token: null | string) { |
|||
return token ? `Bearer ${token}` : null; |
|||
} |
|||
|
|||
makeRequestHeaders: () => { |
|||
return { |
|||
// 为每个请求携带 Accept-Language
|
|||
'Accept-Language': preferences.app.locale, |
|||
}; |
|||
// 请求头处理
|
|||
client.addRequestInterceptor({ |
|||
fulfilled: async (config) => { |
|||
const accessStore = useAccessStore(); |
|||
|
|||
config.headers.Authorization = formatToken(accessStore.accessToken); |
|||
config.headers['Accept-Language'] = preferences.app.locale; |
|||
return config; |
|||
}, |
|||
}); |
|||
client.addResponseInterceptor<HttpResponse>((response) => { |
|||
const { data: responseData, status } = response; |
|||
|
|||
const { code, data, message: msg } = responseData; |
|||
if (status >= 200 && status < 400 && code === 0) { |
|||
return data; |
|||
} |
|||
throw new Error(`Error ${status}: ${msg}`); |
|||
// response数据解构
|
|||
client.addResponseInterceptor({ |
|||
fulfilled: (response) => { |
|||
const { data: responseData, status } = response; |
|||
|
|||
const { code, data, message: msg } = responseData; |
|||
if (status >= 200 && status < 400 && code === 0) { |
|||
return data; |
|||
} |
|||
throw new Error(`Error ${status}: ${msg}`); |
|||
}, |
|||
}); |
|||
|
|||
// token过期的处理
|
|||
client.addResponseInterceptor( |
|||
authenticateResponseInterceptor({ |
|||
client, |
|||
doReAuthenticate, |
|||
doRefreshToken, |
|||
enableRefreshToken: preferences.app.enableRefreshToken, |
|||
formatToken, |
|||
}), |
|||
); |
|||
|
|||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
|||
client.addResponseInterceptor( |
|||
errorMessageResponseInterceptor((msg: string) => message.error(msg)), |
|||
); |
|||
|
|||
return client; |
|||
} |
|||
|
|||
export const requestClient = createRequestClient(apiURL); |
|||
|
|||
export const baseRequestClient = new RequestClient({ baseURL: apiURL }); |
|||
|
|||
@ -1,67 +1,101 @@ |
|||
/** |
|||
* 该文件可自行根据业务逻辑进行调整 |
|||
*/ |
|||
import type { HttpResponse } from '@vben/request'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { RequestClient } from '@vben/request'; |
|||
import { |
|||
authenticateResponseInterceptor, |
|||
errorMessageResponseInterceptor, |
|||
RequestClient, |
|||
} from '@vben/request'; |
|||
import { useAccessStore } from '@vben/stores'; |
|||
|
|||
import { ElMessage } from 'element-plus'; |
|||
|
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
import { refreshTokenApi } from './core'; |
|||
|
|||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
|||
|
|||
function createRequestClient(baseURL: string) { |
|||
const client = new RequestClient({ |
|||
baseURL, |
|||
// 为每个请求携带 Authorization
|
|||
makeAuthorization: () => { |
|||
return { |
|||
// 默认
|
|||
key: 'Authorization', |
|||
tokenHandler: () => { |
|||
const accessStore = useAccessStore(); |
|||
return { |
|||
refreshToken: `${accessStore.refreshToken}`, |
|||
token: `${accessStore.accessToken}`, |
|||
}; |
|||
}, |
|||
unAuthorizedHandler: async () => { |
|||
const accessStore = useAccessStore(); |
|||
const authStore = useAuthStore(); |
|||
accessStore.setAccessToken(null); |
|||
|
|||
if (preferences.app.loginExpiredMode === 'modal') { |
|||
accessStore.setLoginExpired(true); |
|||
} else { |
|||
// 退出登录
|
|||
await authStore.logout(); |
|||
} |
|||
}, |
|||
}; |
|||
}, |
|||
makeErrorMessage: (msg) => ElMessage.error(msg), |
|||
}); |
|||
|
|||
/** |
|||
* 重新认证逻辑 |
|||
*/ |
|||
async function doReAuthenticate() { |
|||
console.warn('Access token or refresh token is invalid or expired. '); |
|||
const accessStore = useAccessStore(); |
|||
const authStore = useAuthStore(); |
|||
accessStore.setAccessToken(null); |
|||
if (preferences.app.loginExpiredMode === 'modal') { |
|||
accessStore.setLoginExpired(true); |
|||
} else { |
|||
await authStore.logout(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 刷新token逻辑 |
|||
*/ |
|||
async function doRefreshToken() { |
|||
const accessStore = useAccessStore(); |
|||
const resp = await refreshTokenApi(); |
|||
const newToken = resp.data; |
|||
accessStore.setAccessToken(newToken); |
|||
return newToken; |
|||
} |
|||
|
|||
function formatToken(token: null | string) { |
|||
return token ? `Bearer ${token}` : null; |
|||
} |
|||
|
|||
makeRequestHeaders: () => { |
|||
return { |
|||
// 为每个请求携带 Accept-Language
|
|||
'Accept-Language': preferences.app.locale, |
|||
}; |
|||
// 请求头处理
|
|||
client.addRequestInterceptor({ |
|||
fulfilled: async (config) => { |
|||
const accessStore = useAccessStore(); |
|||
|
|||
config.headers.Authorization = formatToken(accessStore.accessToken); |
|||
config.headers['Accept-Language'] = preferences.app.locale; |
|||
return config; |
|||
}, |
|||
}); |
|||
client.addResponseInterceptor<HttpResponse>((response) => { |
|||
const { data: responseData, status } = response; |
|||
|
|||
const { code, data, message: msg } = responseData; |
|||
if (status >= 200 && status < 400 && code === 0) { |
|||
return data; |
|||
} |
|||
throw new Error(`Error ${status}: ${msg}`); |
|||
// response数据解构
|
|||
client.addResponseInterceptor({ |
|||
fulfilled: (response) => { |
|||
const { data: responseData, status } = response; |
|||
|
|||
const { code, data, message: msg } = responseData; |
|||
if (status >= 200 && status < 400 && code === 0) { |
|||
return data; |
|||
} |
|||
throw new Error(`Error ${status}: ${msg}`); |
|||
}, |
|||
}); |
|||
|
|||
// token过期的处理
|
|||
client.addResponseInterceptor( |
|||
authenticateResponseInterceptor({ |
|||
client, |
|||
doReAuthenticate, |
|||
doRefreshToken, |
|||
enableRefreshToken: preferences.app.enableRefreshToken, |
|||
formatToken, |
|||
}), |
|||
); |
|||
|
|||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
|||
client.addResponseInterceptor( |
|||
errorMessageResponseInterceptor((msg: string) => ElMessage.error(msg)), |
|||
); |
|||
|
|||
return client; |
|||
} |
|||
|
|||
export const requestClient = createRequestClient(apiURL); |
|||
|
|||
export const baseRequestClient = new RequestClient({ baseURL: apiURL }); |
|||
|
|||
@ -1,66 +1,100 @@ |
|||
/** |
|||
* 该文件可自行根据业务逻辑进行调整 |
|||
*/ |
|||
import type { HttpResponse } from '@vben/request'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { RequestClient } from '@vben/request'; |
|||
import { |
|||
authenticateResponseInterceptor, |
|||
errorMessageResponseInterceptor, |
|||
RequestClient, |
|||
} from '@vben/request'; |
|||
import { useAccessStore } from '@vben/stores'; |
|||
|
|||
import { message } from '#/naive'; |
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
import { refreshTokenApi } from './core'; |
|||
|
|||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
|||
|
|||
function createRequestClient(baseURL: string) { |
|||
const client = new RequestClient({ |
|||
baseURL, |
|||
// 为每个请求携带 Authorization
|
|||
makeAuthorization: () => { |
|||
return { |
|||
// 默认
|
|||
key: 'Authorization', |
|||
tokenHandler: () => { |
|||
const accessStore = useAccessStore(); |
|||
return { |
|||
refreshToken: `${accessStore.refreshToken}`, |
|||
token: `${accessStore.accessToken}`, |
|||
}; |
|||
}, |
|||
unAuthorizedHandler: async () => { |
|||
const accessStore = useAccessStore(); |
|||
const authStore = useAuthStore(); |
|||
accessStore.setAccessToken(null); |
|||
|
|||
if (preferences.app.loginExpiredMode === 'modal') { |
|||
accessStore.setLoginExpired(true); |
|||
} else { |
|||
// 退出登录
|
|||
await authStore.logout(); |
|||
} |
|||
}, |
|||
}; |
|||
}, |
|||
makeErrorMessage: (msg) => message.error(msg), |
|||
}); |
|||
|
|||
/** |
|||
* 重新认证逻辑 |
|||
*/ |
|||
async function doReAuthenticate() { |
|||
console.warn('Access token or refresh token is invalid or expired. '); |
|||
const accessStore = useAccessStore(); |
|||
const authStore = useAuthStore(); |
|||
accessStore.setAccessToken(null); |
|||
if (preferences.app.loginExpiredMode === 'modal') { |
|||
accessStore.setLoginExpired(true); |
|||
} else { |
|||
await authStore.logout(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 刷新token逻辑 |
|||
*/ |
|||
async function doRefreshToken() { |
|||
const accessStore = useAccessStore(); |
|||
const resp = await refreshTokenApi(); |
|||
const newToken = resp.data; |
|||
accessStore.setAccessToken(newToken); |
|||
return newToken; |
|||
} |
|||
|
|||
function formatToken(token: null | string) { |
|||
return token ? `Bearer ${token}` : null; |
|||
} |
|||
|
|||
makeRequestHeaders: () => { |
|||
return { |
|||
// 为每个请求携带 Accept-Language
|
|||
'Accept-Language': preferences.app.locale, |
|||
}; |
|||
// 请求头处理
|
|||
client.addRequestInterceptor({ |
|||
fulfilled: async (config) => { |
|||
const accessStore = useAccessStore(); |
|||
|
|||
config.headers.Authorization = formatToken(accessStore.accessToken); |
|||
config.headers['Accept-Language'] = preferences.app.locale; |
|||
return config; |
|||
}, |
|||
}); |
|||
client.addResponseInterceptor<HttpResponse>((response) => { |
|||
const { data: responseData, status } = response; |
|||
|
|||
const { code, data, message: msg } = responseData; |
|||
if (status >= 200 && status < 400 && code === 0) { |
|||
return data; |
|||
} |
|||
throw new Error(`Error ${status}: ${msg}`); |
|||
// response数据解构
|
|||
client.addResponseInterceptor({ |
|||
fulfilled: (response) => { |
|||
const { data: responseData, status } = response; |
|||
|
|||
const { code, data, message: msg } = responseData; |
|||
if (status >= 200 && status < 400 && code === 0) { |
|||
return data; |
|||
} |
|||
throw new Error(`Error ${status}: ${msg}`); |
|||
}, |
|||
}); |
|||
|
|||
// token过期的处理
|
|||
client.addResponseInterceptor( |
|||
authenticateResponseInterceptor({ |
|||
client, |
|||
doReAuthenticate, |
|||
doRefreshToken, |
|||
enableRefreshToken: preferences.app.enableRefreshToken, |
|||
formatToken, |
|||
}), |
|||
); |
|||
|
|||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
|||
client.addResponseInterceptor( |
|||
errorMessageResponseInterceptor((msg: string) => message.error(msg)), |
|||
); |
|||
|
|||
return client; |
|||
} |
|||
|
|||
export const requestClient = createRequestClient(apiURL); |
|||
|
|||
export const baseRequestClient = new RequestClient({ baseURL: apiURL }); |
|||
|
|||
@ -1,2 +1,3 @@ |
|||
export * from './preset-interceptors'; |
|||
export * from './request-client'; |
|||
export type * from './types'; |
|||
|
|||
@ -0,0 +1,124 @@ |
|||
import type { RequestClient } from './request-client'; |
|||
import type { MakeErrorMessageFn, ResponseInterceptorConfig } from './types'; |
|||
|
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import axios from 'axios'; |
|||
|
|||
export const authenticateResponseInterceptor = ({ |
|||
client, |
|||
doReAuthenticate, |
|||
doRefreshToken, |
|||
enableRefreshToken, |
|||
formatToken, |
|||
}: { |
|||
client: RequestClient; |
|||
doReAuthenticate: () => Promise<void>; |
|||
doRefreshToken: () => Promise<string>; |
|||
enableRefreshToken: boolean; |
|||
formatToken: (token: string) => null | string; |
|||
}): ResponseInterceptorConfig => { |
|||
return { |
|||
rejected: async (error) => { |
|||
const { config, response } = error; |
|||
// 如果不是 401 错误,直接抛出异常
|
|||
if (response?.status !== 401) { |
|||
throw error; |
|||
} |
|||
// 判断是否启用了 refreshToken 功能
|
|||
// 如果没有启用或者已经是重试请求了,直接跳转到重新登录
|
|||
if (!enableRefreshToken || config.__isRetryRequest) { |
|||
await doReAuthenticate(); |
|||
throw error; |
|||
} |
|||
// 如果正在刷新 token,则将请求加入队列,等待刷新完成
|
|||
if (client.isRefreshing) { |
|||
return new Promise((resolve) => { |
|||
client.refreshTokenQueue.push((newToken: string) => { |
|||
config.headers.Authorization = formatToken(newToken); |
|||
resolve(client.request(config.url, { ...config })); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
// 标记开始刷新 token
|
|||
client.isRefreshing = true; |
|||
// 标记当前请求为重试请求,避免无限循环
|
|||
config.__isRetryRequest = true; |
|||
|
|||
try { |
|||
const newToken = await doRefreshToken(); |
|||
|
|||
// 处理队列中的请求
|
|||
client.refreshTokenQueue.forEach((callback) => callback(newToken)); |
|||
// 清空队列
|
|||
client.refreshTokenQueue = []; |
|||
|
|||
return client.request(error.config.url, { ...error.config }); |
|||
} catch (refreshError) { |
|||
// 如果刷新 token 失败,处理错误(如强制登出或跳转登录页面)
|
|||
client.refreshTokenQueue.forEach((callback) => callback('')); |
|||
client.refreshTokenQueue = []; |
|||
console.error('Refresh token failed, please login again.'); |
|||
throw refreshError; |
|||
} finally { |
|||
client.isRefreshing = false; |
|||
} |
|||
}, |
|||
}; |
|||
}; |
|||
|
|||
export const errorMessageResponseInterceptor = ( |
|||
makeErrorMessage?: MakeErrorMessageFn, |
|||
): ResponseInterceptorConfig => { |
|||
return { |
|||
rejected: (error: any) => { |
|||
if (axios.isCancel(error)) { |
|||
return Promise.reject(error); |
|||
} |
|||
|
|||
const err: string = error?.toString?.() ?? ''; |
|||
let errMsg = ''; |
|||
if (err?.includes('Network Error')) { |
|||
errMsg = $t('fallback.http.networkError'); |
|||
} else if (error?.message?.includes?.('timeout')) { |
|||
errMsg = $t('fallback.http.requestTimeout'); |
|||
} |
|||
if (errMsg) { |
|||
makeErrorMessage?.(errMsg); |
|||
return Promise.reject(error); |
|||
} |
|||
|
|||
let errorMessage = error?.response?.data?.error?.message ?? ''; |
|||
const status = error?.response?.status; |
|||
|
|||
switch (status) { |
|||
case 400: { |
|||
errorMessage = $t('fallback.http.badRequest'); |
|||
break; |
|||
} |
|||
case 401: { |
|||
errorMessage = $t('fallback.http.unauthorized'); |
|||
break; |
|||
} |
|||
case 403: { |
|||
errorMessage = $t('fallback.http.forbidden'); |
|||
break; |
|||
} |
|||
case 404: { |
|||
errorMessage = $t('fallback.http.notFound'); |
|||
break; |
|||
} |
|||
case 408: { |
|||
errorMessage = $t('fallback.http.requestTimeout'); |
|||
break; |
|||
} |
|||
default: { |
|||
errorMessage = $t('fallback.http.internalServerError'); |
|||
} |
|||
} |
|||
makeErrorMessage?.(errorMessage); |
|||
return Promise.reject(error); |
|||
}, |
|||
}; |
|||
}; |
|||
@ -1,67 +1,102 @@ |
|||
/** |
|||
* 该文件可自行根据业务逻辑进行调整 |
|||
*/ |
|||
import type { HttpResponse } from '@vben/request'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { RequestClient } from '@vben/request'; |
|||
import { |
|||
authenticateResponseInterceptor, |
|||
errorMessageResponseInterceptor, |
|||
RequestClient, |
|||
} from '@vben/request'; |
|||
import { useAccessStore } from '@vben/stores'; |
|||
|
|||
import { message } from 'ant-design-vue'; |
|||
|
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
import { refreshTokenApi } from './core'; |
|||
|
|||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
|||
|
|||
function createRequestClient(baseURL: string) { |
|||
const client = new RequestClient({ |
|||
baseURL, |
|||
// 为每个请求携带 Authorization
|
|||
makeAuthorization: () => { |
|||
return { |
|||
// 默认
|
|||
key: 'Authorization', |
|||
tokenHandler: () => { |
|||
const accessStore = useAccessStore(); |
|||
return { |
|||
refreshToken: `${accessStore.refreshToken}`, |
|||
token: `${accessStore.accessToken}`, |
|||
}; |
|||
}, |
|||
unAuthorizedHandler: async () => { |
|||
const accessStore = useAccessStore(); |
|||
const authStore = useAuthStore(); |
|||
accessStore.setAccessToken(null); |
|||
|
|||
if (preferences.app.loginExpiredMode === 'modal') { |
|||
accessStore.setLoginExpired(true); |
|||
} else { |
|||
// 退出登录
|
|||
await authStore.logout(); |
|||
} |
|||
}, |
|||
}; |
|||
}, |
|||
makeErrorMessage: (msg) => message.error(msg), |
|||
}); |
|||
|
|||
/** |
|||
* 重新认证逻辑 |
|||
*/ |
|||
async function doReAuthenticate() { |
|||
console.warn('Access token or refresh token is invalid or expired. '); |
|||
const accessStore = useAccessStore(); |
|||
const authStore = useAuthStore(); |
|||
accessStore.setAccessToken(null); |
|||
if (preferences.app.loginExpiredMode === 'modal') { |
|||
accessStore.setLoginExpired(true); |
|||
} else { |
|||
await authStore.logout(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 刷新token逻辑 |
|||
*/ |
|||
async function doRefreshToken() { |
|||
const accessStore = useAccessStore(); |
|||
const resp = await refreshTokenApi(); |
|||
const newToken = resp.data; |
|||
accessStore.setAccessToken(newToken); |
|||
return newToken; |
|||
} |
|||
|
|||
function formatToken(token: null | string) { |
|||
return token ? `Bearer ${token}` : null; |
|||
} |
|||
|
|||
// 请求头处理
|
|||
client.addRequestInterceptor({ |
|||
fulfilled: async (config) => { |
|||
const accessStore = useAccessStore(); |
|||
|
|||
makeRequestHeaders: () => { |
|||
return { |
|||
// 为每个请求携带 Accept-Language
|
|||
'Accept-Language': preferences.app.locale, |
|||
}; |
|||
config.headers.Authorization = formatToken(accessStore.accessToken); |
|||
config.headers['Accept-Language'] = preferences.app.locale; |
|||
return config; |
|||
}, |
|||
}); |
|||
client.addResponseInterceptor<HttpResponse>((response) => { |
|||
const { data: responseData, status } = response; |
|||
|
|||
const { code, data, message: msg } = responseData; |
|||
if (status >= 200 && status < 400 && code === 0) { |
|||
return data; |
|||
} |
|||
throw new Error(`Error ${status}: ${msg}`); |
|||
// response数据解构
|
|||
client.addResponseInterceptor({ |
|||
fulfilled: (response) => { |
|||
const { data: responseData, status } = response; |
|||
|
|||
const { code, data, message: msg } = responseData; |
|||
|
|||
if (status >= 200 && status < 400 && code === 0) { |
|||
return data; |
|||
} |
|||
throw new Error(`Error ${status}: ${msg}`); |
|||
}, |
|||
}); |
|||
|
|||
// token过期的处理
|
|||
client.addResponseInterceptor( |
|||
authenticateResponseInterceptor({ |
|||
client, |
|||
doReAuthenticate, |
|||
doRefreshToken, |
|||
enableRefreshToken: preferences.app.enableRefreshToken, |
|||
formatToken, |
|||
}), |
|||
); |
|||
|
|||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
|||
client.addResponseInterceptor( |
|||
errorMessageResponseInterceptor((msg: string) => message.error(msg)), |
|||
); |
|||
|
|||
return client; |
|||
} |
|||
|
|||
export const requestClient = createRequestClient(apiURL); |
|||
|
|||
export const baseRequestClient = new RequestClient({ baseURL: apiURL }); |
|||
|
|||
Loading…
Reference in new issue