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 |
PORT=5320 |
||||
|
ACCESS_TOKEN_SECRET=access_token_secret |
||||
|
REFRESH_TOKEN_SECRET=refresh_token_secret |
||||
|
|||||
@ -1,15 +1,14 @@ |
|||||
export default eventHandler((event) => { |
import { verifyAccessToken } from '~/utils/jwt-utils'; |
||||
const token = getHeader(event, 'Authorization'); |
import { unAuthorizedResponse } from '~/utils/response'; |
||||
|
|
||||
if (!token) { |
export default eventHandler((event) => { |
||||
setResponseStatus(event, 401); |
const userinfo = verifyAccessToken(event); |
||||
return useResponseError('UnauthorizedException', 'Unauthorized Exception'); |
if (!userinfo) { |
||||
|
return unAuthorizedResponse(event); |
||||
} |
} |
||||
|
|
||||
const username = Buffer.from(token, 'base64').toString('utf8'); |
|
||||
|
|
||||
const codes = |
const codes = |
||||
MOCK_CODES.find((item) => item.username === username)?.codes ?? []; |
MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? []; |
||||
|
|
||||
return useResponseSuccess(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) => { |
export default defineEventHandler(async (event) => { |
||||
const { password, username } = await readBody(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( |
const findUser = MOCK_USERS.find( |
||||
(item) => item.username === username && item.password === password, |
(item) => item.username === username && item.password === password, |
||||
); |
); |
||||
|
|
||||
if (!findUser) { |
if (!findUser) { |
||||
setResponseStatus(event, 403); |
clearRefreshTokenCookie(event); |
||||
return useResponseError('UnauthorizedException', '用户名或密码错误'); |
return forbiddenResponse(event); |
||||
} |
} |
||||
|
|
||||
const accessToken = Buffer.from(username).toString('base64'); |
const accessToken = generateAccessToken(findUser); |
||||
|
const refreshToken = generateRefreshToken(findUser); |
||||
|
|
||||
|
setRefreshTokenCookie(event, refreshToken); |
||||
|
|
||||
return useResponseSuccess({ |
return useResponseSuccess({ |
||||
|
...findUser, |
||||
accessToken, |
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) => { |
import { verifyAccessToken } from '~/utils/jwt-utils'; |
||||
const token = getHeader(event, 'Authorization'); |
import { unAuthorizedResponse } from '~/utils/response'; |
||||
|
|
||||
if (!token) { |
export default eventHandler((event) => { |
||||
setResponseStatus(event, 401); |
const userinfo = verifyAccessToken(event); |
||||
return useResponseError('UnauthorizedException', 'Unauthorized Exception'); |
if (!userinfo) { |
||||
|
return unAuthorizedResponse(event); |
||||
} |
} |
||||
|
|
||||
const username = Buffer.from(token, 'base64').toString('utf8'); |
|
||||
|
|
||||
const menus = |
const menus = |
||||
MOCK_MENUS.find((item) => item.username === username)?.menus ?? []; |
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? []; |
||||
return useResponseSuccess(menus); |
return useResponseSuccess(menus); |
||||
}); |
}); |
||||
|
|||||
@ -1,14 +1,11 @@ |
|||||
|
import { verifyAccessToken } from '~/utils/jwt-utils'; |
||||
|
import { unAuthorizedResponse } from '~/utils/response'; |
||||
|
|
||||
export default eventHandler((event) => { |
export default eventHandler((event) => { |
||||
const token = getHeader(event, 'Authorization'); |
const userinfo = verifyAccessToken(event); |
||||
if (!token) { |
if (!userinfo) { |
||||
setResponseStatus(event, 401); |
return unAuthorizedResponse(event); |
||||
return useResponseError('UnauthorizedException', 'Unauthorized Exception'); |
|
||||
} |
} |
||||
|
|
||||
const username = Buffer.from(token, 'base64').toString('utf8'); |
return useResponseSuccess(userinfo); |
||||
|
|
||||
const user = MOCK_USERS.find((item) => item.username === username); |
|
||||
|
|
||||
const { password: _pwd, ...userInfo } = user; |
|
||||
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 { useAppConfig } from '@vben/hooks'; |
||||
import { preferences } from '@vben/preferences'; |
import { preferences } from '@vben/preferences'; |
||||
import { RequestClient } from '@vben/request'; |
import { |
||||
|
authenticateResponseInterceptor, |
||||
|
errorMessageResponseInterceptor, |
||||
|
RequestClient, |
||||
|
} from '@vben/request'; |
||||
import { useAccessStore } from '@vben/stores'; |
import { useAccessStore } from '@vben/stores'; |
||||
|
|
||||
import { message } from 'ant-design-vue'; |
import { message } from 'ant-design-vue'; |
||||
|
|
||||
import { useAuthStore } from '#/store'; |
import { useAuthStore } from '#/store'; |
||||
|
|
||||
|
import { refreshTokenApi } from './core'; |
||||
|
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
||||
|
|
||||
function createRequestClient(baseURL: string) { |
function createRequestClient(baseURL: string) { |
||||
const client = new RequestClient({ |
const client = new RequestClient({ |
||||
baseURL, |
baseURL, |
||||
// 为每个请求携带 Authorization
|
}); |
||||
makeAuthorization: () => { |
|
||||
return { |
/** |
||||
// 默认
|
* 重新认证逻辑 |
||||
key: 'Authorization', |
*/ |
||||
tokenHandler: () => { |
async function doReAuthenticate() { |
||||
const accessStore = useAccessStore(); |
console.warn('Access token or refresh token is invalid or expired. '); |
||||
return { |
const accessStore = useAccessStore(); |
||||
refreshToken: `${accessStore.refreshToken}`, |
const authStore = useAuthStore(); |
||||
token: `${accessStore.accessToken}`, |
accessStore.setAccessToken(null); |
||||
}; |
if (preferences.app.loginExpiredMode === 'modal') { |
||||
}, |
accessStore.setLoginExpired(true); |
||||
unAuthorizedHandler: async () => { |
} else { |
||||
const accessStore = useAccessStore(); |
await authStore.logout(); |
||||
const authStore = useAuthStore(); |
} |
||||
accessStore.setAccessToken(null); |
} |
||||
|
|
||||
if (preferences.app.loginExpiredMode === 'modal') { |
/** |
||||
accessStore.setLoginExpired(true); |
* 刷新token逻辑 |
||||
} else { |
*/ |
||||
// 退出登录
|
async function doRefreshToken() { |
||||
await authStore.logout(); |
const accessStore = useAccessStore(); |
||||
} |
const resp = await refreshTokenApi(); |
||||
}, |
const newToken = resp.data; |
||||
}; |
accessStore.setAccessToken(newToken); |
||||
}, |
return newToken; |
||||
makeErrorMessage: (msg) => message.error(msg), |
} |
||||
|
|
||||
|
function formatToken(token: null | string) { |
||||
|
return token ? `Bearer ${token}` : null; |
||||
|
} |
||||
|
|
||||
makeRequestHeaders: () => { |
// 请求头处理
|
||||
return { |
client.addRequestInterceptor({ |
||||
// 为每个请求携带 Accept-Language
|
fulfilled: async (config) => { |
||||
'Accept-Language': preferences.app.locale, |
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; |
// response数据解构
|
||||
if (status >= 200 && status < 400 && code === 0) { |
client.addResponseInterceptor({ |
||||
return data; |
fulfilled: (response) => { |
||||
} |
const { data: responseData, status } = response; |
||||
throw new Error(`Error ${status}: ${msg}`); |
|
||||
|
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; |
return client; |
||||
} |
} |
||||
|
|
||||
export const requestClient = createRequestClient(apiURL); |
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 { useAppConfig } from '@vben/hooks'; |
||||
import { preferences } from '@vben/preferences'; |
import { preferences } from '@vben/preferences'; |
||||
import { RequestClient } from '@vben/request'; |
import { |
||||
|
authenticateResponseInterceptor, |
||||
|
errorMessageResponseInterceptor, |
||||
|
RequestClient, |
||||
|
} from '@vben/request'; |
||||
import { useAccessStore } from '@vben/stores'; |
import { useAccessStore } from '@vben/stores'; |
||||
|
|
||||
import { ElMessage } from 'element-plus'; |
import { ElMessage } from 'element-plus'; |
||||
|
|
||||
import { useAuthStore } from '#/store'; |
import { useAuthStore } from '#/store'; |
||||
|
|
||||
|
import { refreshTokenApi } from './core'; |
||||
|
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
||||
|
|
||||
function createRequestClient(baseURL: string) { |
function createRequestClient(baseURL: string) { |
||||
const client = new RequestClient({ |
const client = new RequestClient({ |
||||
baseURL, |
baseURL, |
||||
// 为每个请求携带 Authorization
|
}); |
||||
makeAuthorization: () => { |
|
||||
return { |
/** |
||||
// 默认
|
* 重新认证逻辑 |
||||
key: 'Authorization', |
*/ |
||||
tokenHandler: () => { |
async function doReAuthenticate() { |
||||
const accessStore = useAccessStore(); |
console.warn('Access token or refresh token is invalid or expired. '); |
||||
return { |
const accessStore = useAccessStore(); |
||||
refreshToken: `${accessStore.refreshToken}`, |
const authStore = useAuthStore(); |
||||
token: `${accessStore.accessToken}`, |
accessStore.setAccessToken(null); |
||||
}; |
if (preferences.app.loginExpiredMode === 'modal') { |
||||
}, |
accessStore.setLoginExpired(true); |
||||
unAuthorizedHandler: async () => { |
} else { |
||||
const accessStore = useAccessStore(); |
await authStore.logout(); |
||||
const authStore = useAuthStore(); |
} |
||||
accessStore.setAccessToken(null); |
} |
||||
|
|
||||
if (preferences.app.loginExpiredMode === 'modal') { |
/** |
||||
accessStore.setLoginExpired(true); |
* 刷新token逻辑 |
||||
} else { |
*/ |
||||
// 退出登录
|
async function doRefreshToken() { |
||||
await authStore.logout(); |
const accessStore = useAccessStore(); |
||||
} |
const resp = await refreshTokenApi(); |
||||
}, |
const newToken = resp.data; |
||||
}; |
accessStore.setAccessToken(newToken); |
||||
}, |
return newToken; |
||||
makeErrorMessage: (msg) => ElMessage.error(msg), |
} |
||||
|
|
||||
|
function formatToken(token: null | string) { |
||||
|
return token ? `Bearer ${token}` : null; |
||||
|
} |
||||
|
|
||||
makeRequestHeaders: () => { |
// 请求头处理
|
||||
return { |
client.addRequestInterceptor({ |
||||
// 为每个请求携带 Accept-Language
|
fulfilled: async (config) => { |
||||
'Accept-Language': preferences.app.locale, |
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; |
// response数据解构
|
||||
if (status >= 200 && status < 400 && code === 0) { |
client.addResponseInterceptor({ |
||||
return data; |
fulfilled: (response) => { |
||||
} |
const { data: responseData, status } = response; |
||||
throw new Error(`Error ${status}: ${msg}`); |
|
||||
|
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; |
return client; |
||||
} |
} |
||||
|
|
||||
export const requestClient = createRequestClient(apiURL); |
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 { useAppConfig } from '@vben/hooks'; |
||||
import { preferences } from '@vben/preferences'; |
import { preferences } from '@vben/preferences'; |
||||
import { RequestClient } from '@vben/request'; |
import { |
||||
|
authenticateResponseInterceptor, |
||||
|
errorMessageResponseInterceptor, |
||||
|
RequestClient, |
||||
|
} from '@vben/request'; |
||||
import { useAccessStore } from '@vben/stores'; |
import { useAccessStore } from '@vben/stores'; |
||||
|
|
||||
import { message } from '#/naive'; |
import { message } from '#/naive'; |
||||
import { useAuthStore } from '#/store'; |
import { useAuthStore } from '#/store'; |
||||
|
|
||||
|
import { refreshTokenApi } from './core'; |
||||
|
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
||||
|
|
||||
function createRequestClient(baseURL: string) { |
function createRequestClient(baseURL: string) { |
||||
const client = new RequestClient({ |
const client = new RequestClient({ |
||||
baseURL, |
baseURL, |
||||
// 为每个请求携带 Authorization
|
}); |
||||
makeAuthorization: () => { |
|
||||
return { |
/** |
||||
// 默认
|
* 重新认证逻辑 |
||||
key: 'Authorization', |
*/ |
||||
tokenHandler: () => { |
async function doReAuthenticate() { |
||||
const accessStore = useAccessStore(); |
console.warn('Access token or refresh token is invalid or expired. '); |
||||
return { |
const accessStore = useAccessStore(); |
||||
refreshToken: `${accessStore.refreshToken}`, |
const authStore = useAuthStore(); |
||||
token: `${accessStore.accessToken}`, |
accessStore.setAccessToken(null); |
||||
}; |
if (preferences.app.loginExpiredMode === 'modal') { |
||||
}, |
accessStore.setLoginExpired(true); |
||||
unAuthorizedHandler: async () => { |
} else { |
||||
const accessStore = useAccessStore(); |
await authStore.logout(); |
||||
const authStore = useAuthStore(); |
} |
||||
accessStore.setAccessToken(null); |
} |
||||
|
|
||||
if (preferences.app.loginExpiredMode === 'modal') { |
/** |
||||
accessStore.setLoginExpired(true); |
* 刷新token逻辑 |
||||
} else { |
*/ |
||||
// 退出登录
|
async function doRefreshToken() { |
||||
await authStore.logout(); |
const accessStore = useAccessStore(); |
||||
} |
const resp = await refreshTokenApi(); |
||||
}, |
const newToken = resp.data; |
||||
}; |
accessStore.setAccessToken(newToken); |
||||
}, |
return newToken; |
||||
makeErrorMessage: (msg) => message.error(msg), |
} |
||||
|
|
||||
|
function formatToken(token: null | string) { |
||||
|
return token ? `Bearer ${token}` : null; |
||||
|
} |
||||
|
|
||||
makeRequestHeaders: () => { |
// 请求头处理
|
||||
return { |
client.addRequestInterceptor({ |
||||
// 为每个请求携带 Accept-Language
|
fulfilled: async (config) => { |
||||
'Accept-Language': preferences.app.locale, |
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; |
// response数据解构
|
||||
if (status >= 200 && status < 400 && code === 0) { |
client.addResponseInterceptor({ |
||||
return data; |
fulfilled: (response) => { |
||||
} |
const { data: responseData, status } = response; |
||||
throw new Error(`Error ${status}: ${msg}`); |
|
||||
|
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; |
return client; |
||||
} |
} |
||||
|
|
||||
export const requestClient = createRequestClient(apiURL); |
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 * from './request-client'; |
||||
export type * from './types'; |
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 { useAppConfig } from '@vben/hooks'; |
||||
import { preferences } from '@vben/preferences'; |
import { preferences } from '@vben/preferences'; |
||||
import { RequestClient } from '@vben/request'; |
import { |
||||
|
authenticateResponseInterceptor, |
||||
|
errorMessageResponseInterceptor, |
||||
|
RequestClient, |
||||
|
} from '@vben/request'; |
||||
import { useAccessStore } from '@vben/stores'; |
import { useAccessStore } from '@vben/stores'; |
||||
|
|
||||
import { message } from 'ant-design-vue'; |
import { message } from 'ant-design-vue'; |
||||
|
|
||||
import { useAuthStore } from '#/store'; |
import { useAuthStore } from '#/store'; |
||||
|
|
||||
|
import { refreshTokenApi } from './core'; |
||||
|
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
||||
|
|
||||
function createRequestClient(baseURL: string) { |
function createRequestClient(baseURL: string) { |
||||
const client = new RequestClient({ |
const client = new RequestClient({ |
||||
baseURL, |
baseURL, |
||||
// 为每个请求携带 Authorization
|
}); |
||||
makeAuthorization: () => { |
|
||||
return { |
/** |
||||
// 默认
|
* 重新认证逻辑 |
||||
key: 'Authorization', |
*/ |
||||
tokenHandler: () => { |
async function doReAuthenticate() { |
||||
const accessStore = useAccessStore(); |
console.warn('Access token or refresh token is invalid or expired. '); |
||||
return { |
const accessStore = useAccessStore(); |
||||
refreshToken: `${accessStore.refreshToken}`, |
const authStore = useAuthStore(); |
||||
token: `${accessStore.accessToken}`, |
accessStore.setAccessToken(null); |
||||
}; |
if (preferences.app.loginExpiredMode === 'modal') { |
||||
}, |
accessStore.setLoginExpired(true); |
||||
unAuthorizedHandler: async () => { |
} else { |
||||
const accessStore = useAccessStore(); |
await authStore.logout(); |
||||
const authStore = useAuthStore(); |
} |
||||
accessStore.setAccessToken(null); |
} |
||||
|
|
||||
if (preferences.app.loginExpiredMode === 'modal') { |
/** |
||||
accessStore.setLoginExpired(true); |
* 刷新token逻辑 |
||||
} else { |
*/ |
||||
// 退出登录
|
async function doRefreshToken() { |
||||
await authStore.logout(); |
const accessStore = useAccessStore(); |
||||
} |
const resp = await refreshTokenApi(); |
||||
}, |
const newToken = resp.data; |
||||
}; |
accessStore.setAccessToken(newToken); |
||||
}, |
return newToken; |
||||
makeErrorMessage: (msg) => message.error(msg), |
} |
||||
|
|
||||
|
function formatToken(token: null | string) { |
||||
|
return token ? `Bearer ${token}` : null; |
||||
|
} |
||||
|
|
||||
|
// 请求头处理
|
||||
|
client.addRequestInterceptor({ |
||||
|
fulfilled: async (config) => { |
||||
|
const accessStore = useAccessStore(); |
||||
|
|
||||
makeRequestHeaders: () => { |
config.headers.Authorization = formatToken(accessStore.accessToken); |
||||
return { |
config.headers['Accept-Language'] = preferences.app.locale; |
||||
// 为每个请求携带 Accept-Language
|
return config; |
||||
'Accept-Language': preferences.app.locale, |
|
||||
}; |
|
||||
}, |
}, |
||||
}); |
}); |
||||
client.addResponseInterceptor<HttpResponse>((response) => { |
|
||||
const { data: responseData, status } = response; |
|
||||
|
|
||||
const { code, data, message: msg } = responseData; |
// response数据解构
|
||||
if (status >= 200 && status < 400 && code === 0) { |
client.addResponseInterceptor({ |
||||
return data; |
fulfilled: (response) => { |
||||
} |
const { data: responseData, status } = response; |
||||
throw new Error(`Error ${status}: ${msg}`); |
|
||||
|
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; |
return client; |
||||
} |
} |
||||
|
|
||||
export const requestClient = createRequestClient(apiURL); |
export const requestClient = createRequestClient(apiURL); |
||||
|
|
||||
|
export const baseRequestClient = new RequestClient({ baseURL: apiURL }); |
||||
|
|||||
Loading…
Reference in new issue