Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor and improve the request client and support refreshToken #4157

Merged
merged 29 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ece6418
feat: refreshToken
likui628 Aug 15, 2024
e0853a7
chore: store refreshToken
likui628 Aug 15, 2024
8b79f41
chore: generate token using jsonwebtoken
likui628 Aug 15, 2024
e10d653
chore: set refreshToken in httpOnly cookie
likui628 Aug 15, 2024
9499f4f
perf: authHeader verify
likui628 Aug 15, 2024
3b15977
chore: add add response interceptor
likui628 Aug 16, 2024
4d2f639
Merge branch 'main' into feat-refreshToken
likui628 Aug 16, 2024
71ac59f
chore: test refresh
likui628 Aug 16, 2024
0b51d2b
chore: handle logout
likui628 Aug 16, 2024
96bf85e
chore: type
likui628 Aug 16, 2024
aef8fd6
Merge branch 'main' into feat-refreshToken
likui628 Aug 16, 2024
59091d6
chore: update pnpm-lock.yaml
likui628 Aug 16, 2024
ae1b098
chore: remove test code
likui628 Aug 16, 2024
ceb296e
chore: add todo comment
likui628 Aug 17, 2024
a99a6d1
Merge branch 'main' into feat-refreshToken
likui628 Aug 18, 2024
1c2e822
chore: update pnpm-lock.yaml
likui628 Aug 18, 2024
9a1f1ab
chore: remove default interceptors
likui628 Aug 18, 2024
178df97
chore: copy codes
likui628 Aug 18, 2024
d4bfac4
chore: handle refreshToken invalid
likui628 Aug 18, 2024
ebb91ef
chore: add refreshToken preference
likui628 Aug 19, 2024
081a2e8
chore: typo
vince292007 Aug 19, 2024
519e2f4
chore: refresh token逻辑调整
vince292007 Aug 19, 2024
ca5ff53
refactor: interceptor presets
likui628 Aug 19, 2024
3e1c466
chore: copy codes
likui628 Aug 19, 2024
17b54d9
fix: ci errors
likui628 Aug 19, 2024
b65ab57
chore: add missing await
likui628 Aug 19, 2024
3ecc864
feat: 完善refresh-token逻辑及文档
vince292007 Aug 19, 2024
f4e74d8
fix: ci error
vince292007 Aug 19, 2024
2772990
chore: filename
vince292007 Aug 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/backend-mock/.env
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
PORT=5320
ACCESS_TOKEN_SECRET=access_token_secret
REFRESH_TOKEN_SECRET=refresh_token_secret
15 changes: 7 additions & 8 deletions apps/backend-mock/api/auth/codes.ts
Original file line number Diff line number Diff line change
@@ -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);
});
26 changes: 21 additions & 5 deletions apps/backend-mock/api/auth/login.post.ts
Original file line number Diff line number Diff line change
@@ -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,
});
});
15 changes: 15 additions & 0 deletions apps/backend-mock/api/auth/logout.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie_utils';

export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return setResponseStatus(event, 204);
}

clearRefreshTokenCookie(event);

return setResponseStatus(event, 204);
});
33 changes: 33 additions & 0 deletions apps/backend-mock/api/auth/refresh.post.ts
Original file line number Diff line number Diff line change
@@ -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;
});
15 changes: 7 additions & 8 deletions apps/backend-mock/api/menu/all.ts
Original file line number Diff line number Diff line change
@@ -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);
});
17 changes: 7 additions & 10 deletions apps/backend-mock/api/user/info.ts
Original file line number Diff line number Diff line change
@@ -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);
});
5 changes: 5 additions & 0 deletions apps/backend-mock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
"start": "nitro dev"
},
"dependencies": {
"h3": "^1.12.0",
"jsonwebtoken": "^9.0.2",
"nitropack": "^2.9.7"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.6"
}
}
26 changes: 26 additions & 0 deletions apps/backend-mock/utils/cookie_utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
61 changes: 61 additions & 0 deletions apps/backend-mock/utils/jwt_utils.ts
Original file line number Diff line number Diff line change
@@ -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 && 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;
}
}
10 changes: 9 additions & 1 deletion apps/backend-mock/utils/mock-data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
export const MOCK_USERS = [
export interface UserInfo {
id: number;
password: string;
realName: string;
roles: string[];
username: string;
}

export const MOCK_USERS: UserInfo[] = [
{
id: 0,
password: '123456',
Expand Down
12 changes: 12 additions & 0 deletions apps/backend-mock/utils/response.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { EventHandlerRequest, H3Event } from 'h3';

export function useResponseSuccess<T = any>(data: T) {
return {
code: 0,
Expand All @@ -15,3 +17,13 @@ export function useResponseError(message: string, error: any = null) {
message,
};
}

export function forbiddenResponse(event: H3Event<EventHandlerRequest>) {
setResponseStatus(event, 403);
return useResponseError('ForbiddenException', 'Forbidden Exception');
}

export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
}
24 changes: 22 additions & 2 deletions apps/web-antd/src/api/core/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { requestClient } from '#/api/request';
import { baseRequestClient, requestClient } from '#/api/request';

export namespace AuthApi {
/** 登录接口参数 */
Expand All @@ -12,10 +12,14 @@ export namespace AuthApi {
accessToken: string;
desc: string;
realName: string;
refreshToken: string;
userId: string;
username: string;
}

export interface RefreshTokenResult {
data: string;
status: number;
}
}

/**
Expand All @@ -25,6 +29,22 @@ export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
}

/**
* 刷新accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
withCredentials: true,
});
}

/**
* 退出登录
*/
export async function logoutApi() {
return requestClient.post('/auth/logout');
}

/**
* 获取用户权限码
*/
Expand Down
Loading
Loading