Procházet zdrojové kódy

feat(auth): 实现谷歌二次验证功能

- 在 AuthController 中添加 bindingGoogleTwoFactorValidate 方法用于绑定谷歌验证器
- 在 AuthService 中实现 bindingGoogleTwoFactorValidate 方法的逻辑
- 在 UsersService 中添加 binding 方法生成并保存用户密钥
- 在 utils 目录下新增 authenticator.ts 文件实现生成密钥和验证码的逻辑
- 更新 User 实体,添加 twoFactorCode 字段用于存储用户密钥
- 修改 login 方法,增加对谷歌验证码的验证逻辑
wuyi před 1 rokem
rodič
revize
b9cfbb6ede

+ 1 - 0
package.json

@@ -89,6 +89,7 @@
     "reflect-metadata": "^0.1.13",
     "rimraf": "^4.1.2",
     "rxjs": "^7.8.0",
+    "time2fa": "^1.4.0",
     "typeorm": "^0.3.12",
     "uuid": "^9.0.0",
     "ws": "^8.14.2",

+ 12 - 5
src/auth/auth.controller.ts

@@ -10,7 +10,8 @@ import { UserRegisterDto } from 'src/users/dto/user-register.dto'
 @ApiTags('auth')
 @Controller('/auth')
 export class AuthController {
-    constructor(private readonly authService: AuthService) {}
+    constructor(private readonly authService: AuthService) {
+    }
 
     @Public()
     @Post('/phoneLogin')
@@ -20,14 +21,20 @@ export class AuthController {
 
     @Public()
     @Post('/login')
-    async login(@Body() { username, password }) {
-        return await this.authService.login(username, password)
+    async login(@Body() { username, password, code }) {
+        return await this.authService.login(username, password, code)
     }
 
     @Public()
     @Post('/admin/login')
-    async loginAdmin(@Body() { username, password }) {
-        return await this.authService.loginAdmin(username, password)
+    async loginAdmin(@Body() { username, password, code }) {
+        return await this.authService.loginAdmin(username, password, code)
+    }
+
+    @Public()
+    @Post('/binding')
+    async bindingGoogleTwoFactorValidate(@Body() { username, password }) {
+        return await this.authService.bindingGoogleTwoFactorValidate(username, password)
     }
 
     @Get('/admin/user/:userId/token')

+ 11 - 5
src/auth/auth.service.ts

@@ -4,10 +4,12 @@ import { JwtService } from '@nestjs/jwt'
 import { UsersService } from '../users/users.service'
 import { Role } from 'src/model/role.enum'
 import { UserRegisterDto } from 'src/users/dto/user-register.dto'
+import { generateKey } from '../utils/authenticator'
 
 @Injectable()
 export class AuthService {
-    constructor(private readonly usersService: UsersService, private readonly jwtService: JwtService) {}
+    constructor(private readonly usersService: UsersService, private readonly jwtService: JwtService) {
+    }
 
     async loginByPhone(loginDto: PhoneLoginDto) {
         throw new Error('Permission denied')
@@ -23,8 +25,8 @@ export class AuthService {
         }
     }
 
-    async login(username: string, password: string) {
-        let user = await this.usersService.login(username, password)
+    async login(username: string, password: string, code: string) {
+        let user = await this.usersService.login(username, password, code)
         if (user.roles.includes(Role.Admin)) {
             throw new UnauthorizedException('Permission denied')
         }
@@ -39,11 +41,11 @@ export class AuthService {
         }
     }
 
-    async loginAdmin(username: string, password: string) {
+    async loginAdmin(username: string, password: string, code: string) {
         // if (process.env.ALLOW_ADMIN_LOGIN !== 'true') {
         //     throw new UnauthorizedException('Permission denied')
         // }
-        let user = await this.usersService.login(username, password)
+        let user = await this.usersService.login(username, password, code)
         // if (!user.roles.includes(Role.Admin)) {
         //     throw new UnauthorizedException('Permission denied')
         // }
@@ -58,6 +60,10 @@ export class AuthService {
         }
     }
 
+    async bindingGoogleTwoFactorValidate(username: string, password: string) {
+        return await this.usersService.binding(username)
+    }
+
     async getToken(id: number) {
         let user = await this.usersService.findById(id)
         const payload = {

+ 3 - 0
src/users/entities/users.entity.ts

@@ -68,4 +68,7 @@ export class Users {
     @Column({ default: false })
     isVip: boolean
 
+    @Column({ nullable: true })
+    twoFactorCode: string
+
 }

+ 26 - 2
src/users/users.service.ts

@@ -24,6 +24,7 @@ import { Role } from 'src/model/role.enum'
 import { PageRequest } from '../common/dto/page-request'
 import { endOfDay, startOfDay } from 'date-fns'
 import { BalanceRecord, BalanceType } from '../balance/entities/balance-record.entities'
+import { generateKey, generatePasscodes, verifyPasscode } from '../utils/authenticator'
 
 @Injectable()
 export class UsersService implements OnModuleInit {
@@ -111,7 +112,7 @@ export class UsersService implements OnModuleInit {
         return user
     }
 
-    public async login(username: string, password: string): Promise<Users> {
+    public async login(username: string, password: string, code: string): Promise<Users> {
         let user = await this.userRepository.findOneBy({ username })
         if (!user) {
             throw new UnauthorizedException('Username and password doesn\'t match')
@@ -120,9 +121,32 @@ export class UsersService implements OnModuleInit {
         if (!isMatch) {
             throw new UnauthorizedException('Username and password doesn\'t match')
         }
+        if (!user.roles.includes(Role.Admin)) {
+            if (user.twoFactorCode === null || user.twoFactorCode === '') {
+                throw new UnauthorizedException('请绑定谷歌验证器获取认证码.')
+            } else {
+                const verified = await verifyPasscode(user.twoFactorCode, code)
+                if (!verified) {
+                    throw new UnauthorizedException('认证码错误,请重试.')
+                }
+            }
+        }
         return user
     }
 
+    public async binding(username: string) {
+        const users = await this.userRepository.findOneBy({ username })
+        if (users.twoFactorCode) {
+            return 'success'
+        }
+        const key = await generateKey(username)
+        console.log('key', key)
+        users.twoFactorCode = key.secret
+        await this.userRepository.save(users)
+
+        return key.url
+    }
+
     public async create(userDto: UserCreateDto): Promise<IUsers> {
         try {
             if (userDto.password) {
@@ -265,7 +289,7 @@ export class UsersService implements OnModuleInit {
                     nextUserId = [users[0].id]
                 } else if (users.length > 1) {
                     nextUserId = numbers
-                }else {
+                } else {
                     nextUserId = null
                 }
             }

+ 14 - 0
src/utils/authenticator.ts

@@ -0,0 +1,14 @@
+import { Totp, generateConfig } from 'time2fa'
+
+export async function generateKey(username: string) {
+    return Totp.generateKey({ issuer: 'RCS', user: username })
+}
+
+export async function generatePasscodes(secretKey: string) {
+    const config = generateConfig()
+    return Totp.generatePasscodes({ secret: secretKey }, config)
+}
+
+export async function verifyPasscode(secretKey: string, passcode: string) {
+    return Totp.validate({ passcode: passcode, secret: secretKey })
+}