feat: Admin 登录密码加密传输

This commit is contained in:
sunlei 2026-06-07 14:07:21 +08:00
parent 95c194e842
commit 5f018449e6
2 changed files with 84 additions and 48 deletions

View File

@ -7,6 +7,11 @@ export namespace AuthApi {
username?: string;
}
export interface LoginRequest {
encryptedPassword?: string;
username?: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken: string;
@ -33,15 +38,90 @@ export namespace AuthApi {
};
user?: Record<string, any>;
}
export interface PasswordPublicKeyResult {
algorithm: 'RSA-OAEP';
hash: 'SHA-256';
publicKey: string;
}
}
function pemToArrayBuffer(pem: string) {
const base64 = pem
.replaceAll('-----BEGIN PUBLIC KEY-----', '')
.replaceAll('-----END PUBLIC KEY-----', '')
.replaceAll(/\s/g, '');
const binary = window.atob(base64);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.codePointAt(index) || 0;
}
return bytes.buffer;
}
function arrayBufferToBase64(buffer: ArrayBuffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
bytes.forEach((byte) => {
binary += String.fromCodePoint(byte);
});
return window.btoa(binary);
}
async function encryptPassword(password: string) {
const { hash, publicKey } = await getPasswordPublicKeyApi();
const cryptoKey = await window.crypto.subtle.importKey(
'spki',
pemToArrayBuffer(publicKey),
{
hash,
name: 'RSA-OAEP',
},
false,
['encrypt'],
);
const encrypted = await window.crypto.subtle.encrypt(
{
name: 'RSA-OAEP',
},
cryptoKey,
new TextEncoder().encode(password),
);
return arrayBufferToBase64(encrypted);
}
async function buildLoginRequest(data: AuthApi.LoginParams) {
return {
encryptedPassword: await encryptPassword(data.password || ''),
username: data.username,
} satisfies AuthApi.LoginRequest;
}
/**
*
*/
export async function getPasswordPublicKeyApi() {
return requestClient.get<AuthApi.PasswordPublicKeyResult>(
'/auth/password-public-key',
);
}
/**
*
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data, {
withCredentials: true,
});
return requestClient.post<AuthApi.LoginResult>(
'/auth/login',
await buildLoginRequest(data),
{
withCredentials: true,
},
);
}
/**

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { BasicOption, Recordable } from '@vben/types';
import type { Recordable } from '@vben/types';
import { computed, markRaw, useTemplateRef } from 'vue';
@ -13,57 +13,13 @@ defineOptions({ name: 'Login' });
const authStore = useAuthStore();
const MOCK_USER_OPTIONS: BasicOption[] = [
{
label: 'Super',
value: 'vben',
},
{
label: 'Admin',
value: 'admin',
},
{
label: 'User',
value: 'jack',
},
];
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenSelect',
componentProps: {
options: MOCK_USER_OPTIONS,
placeholder: $t('authentication.selectAccount'),
},
fieldName: 'selectAccount',
label: $t('authentication.selectAccount'),
rules: z
.string()
.min(1, { message: $t('authentication.selectAccount') })
.optional()
.default('vben'),
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
dependencies: {
trigger(values, form) {
if (values.selectAccount) {
const findUser = MOCK_USER_OPTIONS.find(
(item) => item.value === values.selectAccount,
);
if (findUser) {
form.setValues({
username: findUser.value,
});
}
}
},
triggerFields: ['selectAccount'],
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),