feat: Admin 登录密码加密传输

This commit is contained in:
sunlei 2026-06-07 14:06:00 +08:00
parent d25ea3952f
commit d866f15f24
5 changed files with 153 additions and 4 deletions

View File

@ -3,12 +3,23 @@ import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminUser } from '../user/admin-user.entity';
import { AdminAuthService } from './admin-auth.service';
import { AdminPasswordCryptoService } from './admin-password-crypto.service';
import { AdminTokenService } from './admin-token.service';
import { JwtAuthGuard } from './jwt-auth.guard';
@Module({
imports: [ConfigModule, TypeOrmModule.forFeature([AdminUser])],
providers: [AdminAuthService, AdminTokenService, JwtAuthGuard],
exports: [AdminAuthService, AdminTokenService, JwtAuthGuard],
providers: [
AdminAuthService,
AdminPasswordCryptoService,
AdminTokenService,
JwtAuthGuard,
],
exports: [
AdminAuthService,
AdminPasswordCryptoService,
AdminTokenService,
JwtAuthGuard,
],
})
export class AdminAuthGuardModule {}

View File

@ -14,6 +14,8 @@ import { AdminMenuService } from '../menu/admin-menu.service';
import { AdminUser } from '../user/admin-user.entity';
import { AdminUserService } from '../user/admin-user.service';
import { AdminAuthService } from './admin-auth.service';
import { AdminLoginDto } from './admin-auth.dto';
import { AdminPasswordCryptoService } from './admin-password-crypto.service';
import { JwtAuthGuard } from './jwt-auth.guard';
import { WordpressService } from '@/wordpress/wordpress.service';
@ -23,21 +25,32 @@ import { WordpressService } from '@/wordpress/wordpress.service';
export class AdminAuthController {
constructor(
private readonly authService: AdminAuthService,
private readonly passwordCryptoService: AdminPasswordCryptoService,
private readonly menuService: AdminMenuService,
private readonly userService: AdminUserService,
private readonly wordpressService: WordpressService,
) {}
@Get('auth/password-public-key')
@ApiOperation({ summary: '获取 Admin 登录密码加密公钥' })
@Public()
getPasswordPublicKey() {
return vbenSuccess(this.passwordCryptoService.getPublicKey());
}
@Post('auth/login')
@ApiOperation({ summary: 'Admin 用户登录' })
@Public()
async login(
@Body() body: { password?: string; username?: string },
@Body() body: AdminLoginDto,
@Res({ passthrough: true }) res: Response,
) {
const password = this.passwordCryptoService.decryptPassword(
body.encryptedPassword,
);
const { accessToken, refreshToken, user } = await this.authService.login(
body.username,
body.password,
password,
);
const wordpressLogin =
await this.wordpressService.tryLoginWithConfiguredAdmin();

View File

@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
export class AdminLoginDto {
@ApiProperty({ description: 'RSA-OAEP 加密后的登录密码' })
encryptedPassword?: string;
@ApiProperty({ description: '用户名' })
username?: string;
}
export class AdminPasswordPublicKeyDto {
@ApiProperty({ description: '加密算法', example: 'RSA-OAEP' })
algorithm: 'RSA-OAEP';
@ApiProperty({ description: '摘要算法', example: 'SHA-256' })
hash: 'SHA-256';
@ApiProperty({ description: 'PEM 格式 RSA 公钥' })
publicKey: string;
}

View File

@ -0,0 +1,67 @@
import {
constants,
generateKeyPairSync,
privateDecrypt,
} from 'node:crypto';
import { HttpStatus, Injectable } from '@nestjs/common';
import { throwVbenError } from '@/common';
@Injectable()
export class AdminPasswordCryptoService {
private readonly privateKey: string;
private readonly publicKey: string;
constructor() {
const keyPair = generateKeyPairSync('rsa', {
modulusLength: 2048,
privateKeyEncoding: {
format: 'pem',
type: 'pkcs8',
},
publicKeyEncoding: {
format: 'pem',
type: 'spki',
},
});
this.privateKey = keyPair.privateKey;
this.publicKey = keyPair.publicKey;
}
getPublicKey() {
return {
algorithm: 'RSA-OAEP' as const,
hash: 'SHA-256' as const,
publicKey: this.publicKey,
};
}
decryptPassword(encryptedPassword?: string) {
if (!encryptedPassword) {
throwVbenError(
'Encrypted password is required',
HttpStatus.BAD_REQUEST,
'BadRequestException',
);
}
try {
return privateDecrypt(
{
key: this.privateKey,
oaepHash: 'sha256',
padding: constants.RSA_PKCS1_OAEP_PADDING,
},
Buffer.from(encryptedPassword, 'base64'),
).toString('utf8');
} catch {
throwVbenError(
'Password decrypt failed',
HttpStatus.BAD_REQUEST,
'BadRequestException',
);
}
}
}

View File

@ -0,0 +1,38 @@
import { constants, publicEncrypt } from 'node:crypto';
import { HttpException } from '@nestjs/common';
import { AdminPasswordCryptoService } from '../../../src/admin/auth/admin-password-crypto.service';
describe('AdminPasswordCryptoService', () => {
it('decrypts password encrypted by the exported public key', () => {
const service = new AdminPasswordCryptoService();
const { hash, publicKey } = service.getPublicKey();
const encryptedPassword = publicEncrypt(
{
key: publicKey,
oaepHash: hash.toLowerCase().replace('-', ''),
padding: constants.RSA_PKCS1_OAEP_PADDING,
},
Buffer.from('123456', 'utf8'),
).toString('base64');
expect(service.decryptPassword(encryptedPassword)).toBe('123456');
});
it('rejects invalid encrypted password payloads', () => {
const service = new AdminPasswordCryptoService();
try {
service.decryptPassword('not-base64');
throw new Error('decryptPassword should fail');
} catch (error) {
expect(error).toBeInstanceOf(HttpException);
expect((error as HttpException).getResponse()).toEqual(
expect.objectContaining({
err: 'BadRequestException',
msg: 'Password decrypt failed',
}),
);
}
});
});