feat: Admin 登录密码加密传输
This commit is contained in:
parent
d25ea3952f
commit
d866f15f24
@ -3,12 +3,23 @@ import { ConfigModule } from '@nestjs/config';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AdminUser } from '../user/admin-user.entity';
|
import { AdminUser } from '../user/admin-user.entity';
|
||||||
import { AdminAuthService } from './admin-auth.service';
|
import { AdminAuthService } from './admin-auth.service';
|
||||||
|
import { AdminPasswordCryptoService } from './admin-password-crypto.service';
|
||||||
import { AdminTokenService } from './admin-token.service';
|
import { AdminTokenService } from './admin-token.service';
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, TypeOrmModule.forFeature([AdminUser])],
|
imports: [ConfigModule, TypeOrmModule.forFeature([AdminUser])],
|
||||||
providers: [AdminAuthService, AdminTokenService, JwtAuthGuard],
|
providers: [
|
||||||
exports: [AdminAuthService, AdminTokenService, JwtAuthGuard],
|
AdminAuthService,
|
||||||
|
AdminPasswordCryptoService,
|
||||||
|
AdminTokenService,
|
||||||
|
JwtAuthGuard,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AdminAuthService,
|
||||||
|
AdminPasswordCryptoService,
|
||||||
|
AdminTokenService,
|
||||||
|
JwtAuthGuard,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AdminAuthGuardModule {}
|
export class AdminAuthGuardModule {}
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import { AdminMenuService } from '../menu/admin-menu.service';
|
|||||||
import { AdminUser } from '../user/admin-user.entity';
|
import { AdminUser } from '../user/admin-user.entity';
|
||||||
import { AdminUserService } from '../user/admin-user.service';
|
import { AdminUserService } from '../user/admin-user.service';
|
||||||
import { AdminAuthService } from './admin-auth.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 { JwtAuthGuard } from './jwt-auth.guard';
|
||||||
import { WordpressService } from '@/wordpress/wordpress.service';
|
import { WordpressService } from '@/wordpress/wordpress.service';
|
||||||
|
|
||||||
@ -23,21 +25,32 @@ import { WordpressService } from '@/wordpress/wordpress.service';
|
|||||||
export class AdminAuthController {
|
export class AdminAuthController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly authService: AdminAuthService,
|
private readonly authService: AdminAuthService,
|
||||||
|
private readonly passwordCryptoService: AdminPasswordCryptoService,
|
||||||
private readonly menuService: AdminMenuService,
|
private readonly menuService: AdminMenuService,
|
||||||
private readonly userService: AdminUserService,
|
private readonly userService: AdminUserService,
|
||||||
private readonly wordpressService: WordpressService,
|
private readonly wordpressService: WordpressService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@Get('auth/password-public-key')
|
||||||
|
@ApiOperation({ summary: '获取 Admin 登录密码加密公钥' })
|
||||||
|
@Public()
|
||||||
|
getPasswordPublicKey() {
|
||||||
|
return vbenSuccess(this.passwordCryptoService.getPublicKey());
|
||||||
|
}
|
||||||
|
|
||||||
@Post('auth/login')
|
@Post('auth/login')
|
||||||
@ApiOperation({ summary: 'Admin 用户登录' })
|
@ApiOperation({ summary: 'Admin 用户登录' })
|
||||||
@Public()
|
@Public()
|
||||||
async login(
|
async login(
|
||||||
@Body() body: { password?: string; username?: string },
|
@Body() body: AdminLoginDto,
|
||||||
@Res({ passthrough: true }) res: Response,
|
@Res({ passthrough: true }) res: Response,
|
||||||
) {
|
) {
|
||||||
|
const password = this.passwordCryptoService.decryptPassword(
|
||||||
|
body.encryptedPassword,
|
||||||
|
);
|
||||||
const { accessToken, refreshToken, user } = await this.authService.login(
|
const { accessToken, refreshToken, user } = await this.authService.login(
|
||||||
body.username,
|
body.username,
|
||||||
body.password,
|
password,
|
||||||
);
|
);
|
||||||
const wordpressLogin =
|
const wordpressLogin =
|
||||||
await this.wordpressService.tryLoginWithConfiguredAdmin();
|
await this.wordpressService.tryLoginWithConfiguredAdmin();
|
||||||
|
|||||||
20
src/admin/auth/admin-auth.dto.ts
Normal file
20
src/admin/auth/admin-auth.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
67
src/admin/auth/admin-password-crypto.service.ts
Normal file
67
src/admin/auth/admin-password-crypto.service.ts
Normal 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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
test/admin/auth/admin-password-crypto.service.spec.ts
Normal file
38
test/admin/auth/admin-password-crypto.service.spec.ts
Normal 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',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user