Browse Source

Merge pull request #1314 from colinin/vben5-oauth

feat(vben5): Optimize identity authentication
pull/1327/head
yx lin 5 months ago
committed by GitHub
parent
commit
a3737f55fc
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      apps/vben5/apps/app-antd/src/adapter/request/index.ts
  2. 100
      apps/vben5/apps/app-antd/src/store/auth.ts
  3. 5
      apps/vben5/packages/@abp/account/src/api/index.ts
  4. 46
      apps/vben5/packages/@abp/account/src/api/usePhoneLoginApi.ts
  5. 73
      apps/vben5/packages/@abp/account/src/api/useQrCodeLoginApi.ts
  6. 37
      apps/vben5/packages/@abp/account/src/api/useScanQrCodeApi.ts
  7. 108
      apps/vben5/packages/@abp/account/src/api/useTokenApi.ts
  8. 29
      apps/vben5/packages/@abp/account/src/api/useUserInfoApi.ts
  9. 4
      apps/vben5/packages/@abp/account/src/components/QrCodeLogin.vue
  10. 2
      apps/vben5/packages/@abp/account/src/hooks/index.ts
  11. 3
      apps/vben5/packages/@abp/account/src/hooks/useOAuthError.ts
  12. 28
      apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts
  13. 17
      apps/vben5/packages/@abp/account/src/types/token.ts
  14. 138
      apps/vben5/packages/@abp/account/src/utils/auth.ts

3
apps/vben5/apps/app-antd/src/adapter/request/index.ts

@ -37,7 +37,8 @@ export function initRequestClient() {
async function doRefreshToken() {
const authStore = useAuthStore();
try {
return await authStore.refreshSession();
const token = await authStore.refreshSession();
return token ?? '';
} catch {
console.warn('The refresh token has expired or is unavailable.');
}

100
apps/vben5/apps/app-antd/src/store/auth.ts

@ -9,14 +9,7 @@ import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import {
useOidcClient,
usePhoneLoginApi,
useProfileApi,
useQrCodeLoginApi,
useTokenApi,
useUserInfoApi,
} from '@abp/account';
import { useOAuthService, useProfileApi } from '@abp/account';
import { Events, useAbpStore, useEventBus } from '@abp/core';
import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia';
@ -26,48 +19,35 @@ import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
const { publish } = useEventBus();
const { loginApi, refreshTokenApi, logoutApi } = useTokenApi();
const { loginApi: qrcodeLoginApi } = useQrCodeLoginApi();
const { loginApi: phoneLoginApi } = usePhoneLoginApi();
const { getUserInfoApi } = useUserInfoApi();
const { getConfigApi } = useAbpConfigApi();
const { getPictureApi } = useProfileApi();
const accessStore = useAccessStore();
const userStore = useUserStore();
const abpStore = useAbpStore();
const router = useRouter();
const oidcClient = useOidcClient();
const oAuthService = useOAuthService();
const loginLoading = ref(false);
async function refreshSession() {
if (await oidcClient.getAccessToken()) {
const user = await oidcClient.refreshToken();
if (await oAuthService.getAccessToken()) {
const user = await oAuthService.refreshToken();
const newToken = `${user?.token_type} ${user?.access_token}`;
accessStore.setAccessToken(newToken);
if (user?.refresh_token) {
accessStore.setRefreshToken(user.refresh_token);
}
return newToken;
} else {
const { accessToken, tokenType, refreshToken } = await refreshTokenApi({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
refreshToken: accessStore.refreshToken!,
});
const newToken = `${tokenType} ${accessToken}`;
accessStore.setAccessToken(newToken);
accessStore.setRefreshToken(refreshToken);
return newToken;
}
}
async function oidcLogin() {
await oidcClient.login();
await oAuthService.login();
}
async function oidcCallback() {
try {
const user = await oidcClient.handleCallback();
const user = await oAuthService.handleCallback();
return await _loginSuccess({
accessToken: user.access_token,
tokenType: user.token_type,
@ -87,8 +67,17 @@ export const useAuthStore = defineStore('auth', () => {
) {
try {
loginLoading.value = true;
const result = await qrcodeLoginApi({ key, tenantId });
return await _loginSuccess(result, onSuccess);
const user = await oAuthService.loginByQrCode({ key, tenantId });
return await _loginSuccess(
{
accessToken: user.access_token,
tokenType: user.token_type,
refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!,
},
onSuccess,
);
} finally {
loginLoading.value = false;
}
@ -101,8 +90,17 @@ export const useAuthStore = defineStore('auth', () => {
) {
try {
loginLoading.value = true;
const result = await phoneLoginApi({ phoneNumber, code });
return await _loginSuccess(result, onSuccess);
const user = await oAuthService.loginBySmsCode({ phoneNumber, code });
return await _loginSuccess(
{
accessToken: user.access_token,
tokenType: user.token_type,
refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!,
},
onSuccess,
);
} finally {
loginLoading.value = false;
}
@ -119,8 +117,17 @@ export const useAuthStore = defineStore('auth', () => {
) {
try {
loginLoading.value = true;
const result = await loginApi(params as any);
return await _loginSuccess(result, onSuccess);
const user = await oAuthService.loginByPassword(params as any);
return await _loginSuccess(
{
accessToken: user.access_token,
tokenType: user.token_type,
refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!,
},
onSuccess,
);
} finally {
loginLoading.value = false;
}
@ -128,28 +135,11 @@ export const useAuthStore = defineStore('auth', () => {
async function logout(redirect: boolean = true) {
try {
if (await oidcClient.getAccessToken()) {
if (await oAuthService.getAccessToken()) {
accessStore.setAccessToken(null);
await oidcClient.logout();
await oAuthService.logout();
} else {
const logoutTasks: Promise<void>[] = [];
if (accessStore.accessToken) {
logoutTasks.push(
logoutApi({
token: accessStore.accessToken,
tokenType: 'access_token',
}),
);
}
if (accessStore.refreshToken) {
logoutTasks.push(
logoutApi({
token: accessStore.refreshToken,
tokenType: 'refresh_token',
}),
);
}
await Promise.all(logoutTasks);
await oAuthService.revokeTokens();
}
} catch {
// 不做任何处理
@ -172,7 +162,11 @@ export const useAuthStore = defineStore('auth', () => {
async function fetchUserInfo() {
let userInfo: null | (UserInfo & { [key: string]: any }) = null;
const userInfoRes = await getUserInfoApi();
let userInfoRes: { [key: string]: any } = {};
const user = await oAuthService.getUser();
if (user) {
userInfoRes = user.profile;
}
const abpConfig = await getConfigApi();
const picture = await getPictureApi();
userInfo = {

5
apps/vben5/packages/@abp/account/src/api/index.ts

@ -1,7 +1,4 @@
export { useAccountApi } from './useAccountApi';
export { useMySessionApi } from './useMySessionApi';
export { usePhoneLoginApi } from './usePhoneLoginApi';
export { useProfileApi } from './useProfileApi';
export { useQrCodeLoginApi } from './useQrCodeLoginApi';
export { useTokenApi } from './useTokenApi';
export { useUserInfoApi } from './useUserInfoApi';
export { useScanQrCodeApi } from './useScanQrCodeApi';

46
apps/vben5/packages/@abp/account/src/api/usePhoneLoginApi.ts

@ -1,46 +0,0 @@
import type { OAuthTokenResult, PhoneNumberTokenRequest } from '../types/token';
import { useAppConfig } from '@vben/hooks';
import { useRequest } from '@abp/request';
export function usePhoneLoginApi() {
const { cancel, request } = useRequest();
/**
*
* @param input
* @returns token
*/
async function loginApi(input: PhoneNumberTokenRequest) {
const { audience, clientId, clientSecret } = useAppConfig(
import.meta.env,
import.meta.env.PROD,
);
const result = await request<OAuthTokenResult>('/connect/token', {
data: {
client_id: clientId,
client_secret: clientSecret,
grant_type: 'phone_verify',
phone_number: input.phoneNumber,
phone_verify_code: input.code,
scope: audience,
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
});
return {
accessToken: result.access_token,
expiresIn: result.expires_in,
refreshToken: result.refresh_token,
tokenType: result.token_type,
};
}
return {
cancel,
loginApi,
};
}

73
apps/vben5/packages/@abp/account/src/api/useQrCodeLoginApi.ts

@ -1,73 +0,0 @@
import type {
GenerateQrCodeResult,
QrCodeUserInfoResult,
} from '../types/qrcode';
import type { OAuthTokenResult, QrCodeTokenRequest } from '../types/token';
import { useAppConfig } from '@vben/hooks';
import { useRequest } from '@abp/request';
export function useQrCodeLoginApi() {
const { cancel, request } = useRequest();
/**
*
* @returns
*/
function generateApi(): Promise<GenerateQrCodeResult> {
return request<GenerateQrCodeResult>('/api/account/qrcode/generate', {
method: 'POST',
});
}
/**
*
* @param key Key
* @returns
*/
function checkCodeApi(key: string): Promise<QrCodeUserInfoResult> {
return request<QrCodeUserInfoResult>(`/api/account/qrcode/${key}/check`, {
method: 'GET',
});
}
/**
*
* @param input
* @returns token
*/
async function loginApi(input: QrCodeTokenRequest) {
const { audience, clientId, clientSecret } = useAppConfig(
import.meta.env,
import.meta.env.PROD,
);
const result = await request<OAuthTokenResult>('/connect/token', {
data: {
client_id: clientId,
client_secret: clientSecret,
grant_type: 'qr_code',
qrcode_key: input.key,
scope: audience,
tenant_id: input.tenantId,
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
});
return {
accessToken: result.access_token,
expiresIn: result.expires_in,
refreshToken: result.refresh_token,
tokenType: result.token_type,
};
}
return {
cancel,
checkCodeApi,
generateApi,
loginApi,
};
}

37
apps/vben5/packages/@abp/account/src/api/useScanQrCodeApi.ts

@ -0,0 +1,37 @@
import type {
GenerateQrCodeResult,
QrCodeUserInfoResult,
} from '../types/qrcode';
import { useRequest } from '@abp/request';
export function useScanQrCodeApi() {
const { cancel, request } = useRequest();
/**
*
* @returns
*/
function generateApi(): Promise<GenerateQrCodeResult> {
return request<GenerateQrCodeResult>('/api/account/qrcode/generate', {
method: 'POST',
});
}
/**
*
* @param key Key
* @returns
*/
function checkCodeApi(key: string): Promise<QrCodeUserInfoResult> {
return request<QrCodeUserInfoResult>(`/api/account/qrcode/${key}/check`, {
method: 'GET',
});
}
return {
cancel,
checkCodeApi,
generateApi,
};
}

108
apps/vben5/packages/@abp/account/src/api/useTokenApi.ts

@ -1,108 +0,0 @@
import type {
OAuthTokenRefreshModel,
OAuthTokenResult,
PasswordTokenRequestModel,
RevokeTokenRequest,
TokenResult,
} from '../types';
import { useAppConfig } from '@vben/hooks';
import { useRequest } from '@abp/request';
export function useTokenApi() {
const { cancel, request } = useRequest();
/**
*
* @param input
* @returns token
*/
async function loginApi(
input: PasswordTokenRequestModel,
): Promise<TokenResult> {
const { audience, clientId, clientSecret } = useAppConfig(
import.meta.env,
import.meta.env.PROD,
);
const result = await request<OAuthTokenResult>('/connect/token', {
data: {
client_id: clientId,
client_secret: clientSecret,
grant_type: 'password',
scope: audience,
...input,
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
});
return {
accessToken: result.access_token,
expiresIn: result.expires_in,
refreshToken: result.refresh_token,
tokenType: result.token_type,
};
}
/**
*
* @param input
* @returns token
*/
async function refreshTokenApi(input: OAuthTokenRefreshModel) {
const { audience, clientId, clientSecret } = useAppConfig(
import.meta.env,
import.meta.env.PROD,
);
const result = await request<OAuthTokenResult>('/connect/token', {
data: {
client_id: clientId,
client_secret: clientSecret,
grant_type: 'refresh_token',
refresh_token: input.refreshToken,
scope: audience,
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
});
return {
accessToken: result.access_token,
expiresIn: result.expires_in,
refreshToken: result.refresh_token,
tokenType: result.token_type,
};
}
/**
*
* @param input
*/
async function logoutApi(input: RevokeTokenRequest): Promise<void> {
const { clientId, clientSecret } = useAppConfig(
import.meta.env,
import.meta.env.PROD,
);
return await request('/connect/revocat', {
data: {
client_id: clientId,
client_secret: clientSecret,
token: input.token,
token_type_hint: input.tokenType,
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
});
}
return {
cancel,
loginApi,
logoutApi,
refreshTokenApi,
};
}

29
apps/vben5/packages/@abp/account/src/api/useUserInfoApi.ts

@ -1,29 +0,0 @@
import type { OAuthUserInfo, UserInfo } from '../types/user';
import { useRequest } from '@abp/request';
export function useUserInfoApi() {
const { cancel, request } = useRequest();
/**
*
*/
async function getUserInfoApi(): Promise<UserInfo> {
const result = await request<OAuthUserInfo>('/connect/userinfo', {
method: 'GET',
});
return {
...result,
emailVerified: result.email_verified,
givenName: result.given_name,
phoneNumberVerified: result.phone_number_verified,
preferredUsername: result.preferred_username,
uniqueName: result.unique_name,
};
}
return {
cancel,
getUserInfoApi,
};
}

4
apps/vben5/packages/@abp/account/src/components/QrCodeLogin.vue

@ -11,7 +11,7 @@ import { VbenButton } from '@vben-core/shadcn-ui';
import { useQRCode } from '@vueuse/integrations/useQRCode';
import { Spin } from 'ant-design-vue';
import { useQrCodeLoginApi } from '../api';
import { useScanQrCodeApi } from '../api';
import { QrCodeStatus } from '../types/qrcode';
import Title from './components/LoginTitle.vue';
@ -66,7 +66,7 @@ const emits = defineEmits<{
let interval: NodeJS.Timeout;
const router = useRouter();
const { checkCodeApi, generateApi } = useQrCodeLoginApi();
const { checkCodeApi, generateApi } = useScanQrCodeApi();
const qrcodeInfo = ref<QrCodeUserInfoResult>({
key: '',

2
apps/vben5/packages/@abp/account/src/hooks/index.ts

@ -1,2 +1,2 @@
export * from './useOAuthError';
export * from './useOidcClient';
export * from './useOAuthService';

3
apps/vben5/packages/@abp/account/src/hooks/useOAuthError.ts

@ -23,7 +23,8 @@ export function useOAuthError() {
return $t('abp.oauth.errors.requiresTwoFactor');
}
// Token已失效
case 'The token is no longer valid.': {
case 'The token is no longer valid.':
case 'The user is no longer allowed to sign in.': {
return $t('abp.oauth.errors.tokenHasExpired');
}
// 用户尝试登录次数太多,用户被锁定

28
apps/vben5/packages/@abp/account/src/hooks/useOidcClient.ts → apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts

@ -1,14 +1,36 @@
import type {
PasswordTokenRequestModel,
PhoneNumberTokenRequest,
QrCodeTokenRequest,
} from '../types/token';
import { userManager } from '../utils/auth';
export function useOidcClient() {
export function useOAuthService() {
async function login() {
return userManager.signinRedirect();
}
async function loginByPassword(input: PasswordTokenRequestModel) {
return userManager.signinResourceOwnerCredentials(input);
}
async function loginBySmsCode(input: PhoneNumberTokenRequest) {
return userManager.signinSmsCode(input);
}
async function loginByQrCode(input: QrCodeTokenRequest) {
return userManager.signinQrCode(input);
}
async function logout() {
return userManager.signoutRedirect();
}
async function revokeTokens() {
return userManager.revokeTokens(['access_token', 'refresh_token']);
}
async function refreshToken() {
return userManager.signinSilent();
}
@ -33,8 +55,12 @@ export function useOidcClient() {
return {
login,
loginByPassword,
loginBySmsCode,
loginByQrCode,
logout,
refreshToken,
revokeTokens,
getAccessToken,
isAuthenticated,
handleCallback,

17
apps/vben5/packages/@abp/account/src/types/token.ts

@ -16,6 +16,7 @@ interface PasswordTokenRequest extends TokenRequest {
}
/** 手机号授权请求数据模型 */
interface PhoneNumberTokenRequest {
[key: string]: any;
/** 验证码 */
code: string;
/** 手机号 */
@ -23,25 +24,27 @@ interface PhoneNumberTokenRequest {
}
/** 扫码登录授权请求数据模型 */
interface QrCodeTokenRequest {
[key: string]: any;
/** 二维码Key */
key: string;
/** 租户Id */
tenantId?: string;
}
/** 令牌撤销请求数据类型 */
interface RevokeTokenRequest {
/** 令牌 */
token: string;
/** 令牌类型 */
tokenType?: 'access_token' | 'refresh_token';
}
/** 用户密码授权请求数据模型 */
interface PasswordTokenRequestModel {
[key: string]: any;
/** 用户密码 */
password: string;
/** 用户名 */
username: string;
}
/** 令牌撤销请求数据类型 */
interface RevokeTokenRequest {
/** 令牌 */
token: string;
/** 令牌类型 */
tokenType?: 'access_token' | 'refresh_token';
}
/** 令牌返回数据模型 */
interface TokenResult {
/** 访问令牌 */

138
apps/vben5/packages/@abp/account/src/utils/auth.ts

@ -1,8 +1,137 @@
import type { Logger, UserManagerSettings } from 'oidc-client-ts';
import type {
PasswordTokenRequestModel,
PhoneNumberTokenRequest,
QrCodeTokenRequest,
} from '../types/token';
import { useAppConfig } from '@vben/hooks';
import { UserManager, WebStorageStateStore } from 'oidc-client-ts';
import { useRequest } from '@abp/request';
import {
SigninResponse,
UserManager,
WebStorageStateStore,
} from 'oidc-client-ts';
import SecureLS from 'secure-ls';
class AbpUserManager extends UserManager {
async _fetchUser(logger: Logger, body: URLSearchParams) {
const { request } = useRequest();
const url = await this.metadataService.getTokenEndpoint(false);
if (!this.settings.omitScopeWhenRequesting) {
body.set('scope', this.settings.scope);
}
const resp = await request(url, {
data: body,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
logger.debug('got signin response');
const response = new SigninResponse(new URLSearchParams());
Object.assign(response, resp);
const user = await this._buildUser(response);
if (user.profile && user.profile.sub) {
logger.info('success, signed in subject', user.profile.sub);
} else {
logger.info('no subject');
}
return user;
}
_writeChangePasswordToken(
params: URLSearchParams,
model: Record<string, any>,
) {
if (model.ChangePasswordToken) {
params.set('ChangePasswordToken', model.ChangePasswordToken);
}
if (model.NewPassword) {
params.set('NewPassword', model.NewPassword);
}
}
_writeTenantId(params: URLSearchParams, model: Record<string, any>) {
if (model.tenantId) {
params.set('tenantId', model.tenantId);
}
}
_writeTwoFactorToken(params: URLSearchParams, model: Record<string, any>) {
if (model.TwoFactorProvider) {
params.set('TwoFactorProvider', model.TwoFactorProvider);
}
if (model.TwoFactorCode) {
params.set('TwoFactorCode', model.TwoFactorCode);
}
}
_writeUserId(params: URLSearchParams, model: Record<string, any>) {
if (model.userId) {
params.set('userId', model.userId);
}
}
async signinQrCode(params: QrCodeTokenRequest) {
const logger = this._logger.create('signinQrCode');
const client_secret = this.settings.client_secret;
if (!client_secret) {
logger.error('A client_id is required');
throw new Error('A client_id is required');
}
const body = new URLSearchParams({
key: params.key,
grant_type: 'qr_code',
client_id: this.settings.client_id,
client_secret,
});
this._writeUserId(body, params);
this._writeTenantId(body, params);
this._writeTwoFactorToken(body, params);
return await this._fetchUser(logger, body);
}
override async signinResourceOwnerCredentials(
params: PasswordTokenRequestModel,
) {
const logger = this._logger.create('signinResourceOwnerCredentials');
const client_secret = this.settings.client_secret;
if (!client_secret) {
logger.error('A client_id is required');
throw new Error('A client_id is required');
}
const body = new URLSearchParams({
username: params.username,
password: params.password,
grant_type: 'password',
client_id: this.settings.client_id,
client_secret,
});
this._writeUserId(body, params);
this._writeTwoFactorToken(body, params);
this._writeChangePasswordToken(body, params);
return await this._fetchUser(logger, body);
}
async signinSmsCode(params: PhoneNumberTokenRequest) {
const logger = this._logger.create('signinSmsCode');
const client_secret = this.settings.client_secret;
if (!client_secret) {
logger.error('A client_id is required');
throw new Error('A client_id is required');
}
const body = new URLSearchParams({
phone_number: params.phoneNumber,
phone_verify_code: params.code,
grant_type: 'phone_verify',
client_id: this.settings.client_id,
client_secret,
});
this._writeUserId(body, params);
this._writeTwoFactorToken(body, params);
return await this._fetchUser(logger, body);
}
}
const { authority, audience, clientId, clientSecret, disablePKCE } =
useAppConfig(import.meta.env, import.meta.env.PROD);
@ -17,7 +146,7 @@ const ls = new SecureLS({
// @ts-ignore secure-ls does not have a type definition for this
metaKey: `${namespace}-secure-oidc`,
});
export const userManager = new UserManager({
const oidcSettings: UserManagerSettings = {
authority,
client_id: clientId,
client_secret: clientSecret,
@ -50,4 +179,7 @@ export const userManager = new UserManager({
},
}),
disablePKCE,
});
};
const userManager = new AbpUserManager(oidcSettings);
export { oidcSettings, userManager };

Loading…
Cancel
Save