From 570f5a8ac65f8eb7e4383649890ede4411a985cf Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Fri, 1 Mar 2024 15:35:32 +0800 Subject: [PATCH] Optimize two-factor api. Add remove two-factor api. --- .../permission/AuthenticationController.kt | 17 ++++++++-- .../controller/system/SettingsController.kt | 4 +-- .../oxygen/api/entity/common/ResponseCode.kt | 1 + ...TwoFactorVerificationCodeErrorException.kt | 10 ++++++ .../oxygen/api/handler/ExceptionHandler.kt | 5 +++ .../oxygen/api/param/permission/LoginParam.kt | 2 +- .../param/permission/TwoFactorRemoveParam.kt | 23 +++++++++++++ .../permission/TwoFactorValidateParam.kt | 2 +- .../param/system/TwoFactorSettingsParam.kt | 2 +- .../permission/IAuthenticationService.kt | 11 +++++++ .../impl/AuthenticationServiceImpl.kt | 33 ++++++++++++++++--- .../top/fatweb/oxygen/api/util/TOTPUtil.kt | 2 +- .../oxygen/api/vo/permission/TwoFactorVo.kt | 2 +- .../api/vo/permission/UserWithInfoVo.kt | 2 +- .../permission/UserWithPasswordRoleInfoVo.kt | 2 +- .../api/vo/permission/UserWithPowerInfoVo.kt | 2 +- .../api/vo/permission/UserWithRoleInfoVo.kt | 2 +- .../api/vo/system/TwoFactorSettingsVo.kt | 2 +- .../db/migration/master/R__Basic_data.sql | 4 +-- .../V1_0_0_231019__Add_table_'t_s_user'.sql | 2 +- 20 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 src/main/kotlin/top/fatweb/oxygen/api/exception/TwoFactorVerificationCodeErrorException.kt create mode 100644 src/main/kotlin/top/fatweb/oxygen/api/param/permission/TwoFactorRemoveParam.kt diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/AuthenticationController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/AuthenticationController.kt index b6c9666..9808fba 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/AuthenticationController.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/AuthenticationController.kt @@ -3,6 +3,7 @@ package top.fatweb.oxygen.api.controller.permission import io.swagger.v3.oas.annotations.Operation import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid +import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @@ -163,7 +164,7 @@ class AuthenticationController( * @see ResponseResult * @see TwoFactorVo */ - @Operation(summary = "创建二步验证") + @Operation(summary = "创建双因素验证码") @GetMapping("/two-factor") fun createTwoFactor(): ResponseResult = ResponseResult.success(data = authenticationService.createTwoFactor()) @@ -174,12 +175,24 @@ class AuthenticationController( * @author FatttSnake, fatttsnake@gmail.com * @since 1.0.0 */ - @Operation(summary = "验证二步验证") + @Operation(summary = "验证双因素") @PostMapping("/two-factor") fun validateTwoFactor(@RequestBody @Valid twoFactorValidateParam: TwoFactorValidateParam): ResponseResult = if (authenticationService.validateTwoFactor(twoFactorValidateParam)) ResponseResult.success() else ResponseResult.fail() + /** + * Remove two-factor + * + * @author FatttSnake, fatttsnake@gmail.com + * @since 1.0.0 + */ + @Operation(summary = "移除双因素") + @DeleteMapping("/two-factor") + fun removeTwoFactor(@RequestBody @Valid twoFactorRemoveParam: TwoFactorRemoveParam): ResponseResult = + if (authenticationService.removeTwoFactor(twoFactorRemoveParam)) ResponseResult.success() + else ResponseResult.fail() + /** * Logout diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/system/SettingsController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/system/SettingsController.kt index 1dea226..c41a6a0 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/controller/system/SettingsController.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/system/SettingsController.kt @@ -194,7 +194,7 @@ class SettingsController( * @see ResponseResult * @see TwoFactorSettingsVo */ - @Operation(summary = "获取二步验证设置") + @Operation(summary = "获取双因素设置") @GetMapping("/two-factor") @PreAuthorize("hasAnyAuthority('system:settings:query:two-factor')") fun getTwoFactor(): ResponseResult = @@ -211,7 +211,7 @@ class SettingsController( * @see ResponseResult */ @Trim - @Operation(summary = "更新二步验证设置") + @Operation(summary = "更新双因素设置") @PutMapping("/two-factor") @PreAuthorize("hasAnyAuthority('system:settings:modify:two-factor')") fun updateTwoFactor(@RequestBody twoFactorSettingsParam: TwoFactorSettingsParam): ResponseResult { diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/common/ResponseCode.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/common/ResponseCode.kt index 60a5300..e61be09 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/entity/common/ResponseCode.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/common/ResponseCode.kt @@ -48,6 +48,7 @@ enum class ResponseCode(val code: Int) { PERMISSION_NEED_TWO_FACTOR(BusinessCode.PERMISSION, 68), PERMISSION_ALREADY_HAS_TWO_FACTOR(BusinessCode.PERMISSION, 69), PERMISSION_NO_TWO_FACTOR_FOUND(BusinessCode.PERMISSION, 70), + PERMISSION_TWO_FACTOR_VERIFICATION_CODE_ERROR(BusinessCode.PERMISSION, 71), DATABASE_SELECT_SUCCESS(BusinessCode.DATABASE, 0), DATABASE_SELECT_FAILED(BusinessCode.DATABASE, 5), diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/TwoFactorVerificationCodeErrorException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/TwoFactorVerificationCodeErrorException.kt new file mode 100644 index 0000000..e93a68d --- /dev/null +++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/TwoFactorVerificationCodeErrorException.kt @@ -0,0 +1,10 @@ +package top.fatweb.oxygen.api.exception + +/** + * Two-factor verification code error exception + * + * @author FatttSnake, fatttsnake@gmail.com + * @since 1.0.0 + * @see RuntimeException + */ +class TwoFactorVerificationCodeErrorException : RuntimeException("Two-factor verification code error") \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt b/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt index b8baab0..9acf7f8 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt @@ -180,6 +180,11 @@ class ExceptionHandler { ResponseResult.fail(ResponseCode.PERMISSION_NO_TWO_FACTOR_FOUND, e.localizedMessage, null) } + is TwoFactorVerificationCodeErrorException -> { + logger.debug(e.localizedMessage, e) + ResponseResult.fail(ResponseCode.PERMISSION_TWO_FACTOR_VERIFICATION_CODE_ERROR, e.localizedMessage, null) + } + /* SQL */ is DatabaseSelectException -> { logger.debug(e.localizedMessage, e) diff --git a/src/main/kotlin/top/fatweb/oxygen/api/param/permission/LoginParam.kt b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/LoginParam.kt index efa013f..c00f95c 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/param/permission/LoginParam.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/LoginParam.kt @@ -42,6 +42,6 @@ data class LoginParam( * @author FatttSnake, fatttsnake@gmail.com * @since 1.0.0 */ - @Schema(description = "二步验证码") + @Schema(description = "双因素验证码") val twoFactorCode: String? ) : CaptchaCodeParam() \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/oxygen/api/param/permission/TwoFactorRemoveParam.kt b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/TwoFactorRemoveParam.kt new file mode 100644 index 0000000..7a242fb --- /dev/null +++ b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/TwoFactorRemoveParam.kt @@ -0,0 +1,23 @@ +package top.fatweb.oxygen.api.param.permission + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank + +/** + * Remove two-factor parameters + * + * @author FatttSnake, fatttsnake@gmail.com + * @since 1.0.0 + */ +@Schema(description = "移除双因素请求参数") +data class TwoFactorRemoveParam( + /** + * Code + * + * @author FatttSnake, fatttsnake@gmail.com + * @since 1.0.0 + */ + @Schema(description = "验证码") + @field:NotBlank(message = "Code can not be blank") + val code: String? +) diff --git a/src/main/kotlin/top/fatweb/oxygen/api/param/permission/TwoFactorValidateParam.kt b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/TwoFactorValidateParam.kt index a75af7a..1a6230a 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/param/permission/TwoFactorValidateParam.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/TwoFactorValidateParam.kt @@ -9,7 +9,7 @@ import jakarta.validation.constraints.NotBlank * @author FatttSnake, fatttsnake@gmail.com * @since 1.0.0 */ -@Schema(description = "验证二步验证请求参数") +@Schema(description = "验证双因素请求参数") data class TwoFactorValidateParam( /** * Code diff --git a/src/main/kotlin/top/fatweb/oxygen/api/param/system/TwoFactorSettingsParam.kt b/src/main/kotlin/top/fatweb/oxygen/api/param/system/TwoFactorSettingsParam.kt index cbf3601..cb157e2 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/param/system/TwoFactorSettingsParam.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/param/system/TwoFactorSettingsParam.kt @@ -12,7 +12,7 @@ import top.fatweb.oxygen.api.annotation.Trim * @since 1.0.0 */ @Trim -@Schema(description = "二步验证设置请求参数") +@Schema(description = "双因素设置请求参数") data class TwoFactorSettingsParam( /** * Issuer diff --git a/src/main/kotlin/top/fatweb/oxygen/api/service/permission/IAuthenticationService.kt b/src/main/kotlin/top/fatweb/oxygen/api/service/permission/IAuthenticationService.kt index b9d6eb4..d5e8ad9 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/service/permission/IAuthenticationService.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/service/permission/IAuthenticationService.kt @@ -104,6 +104,17 @@ interface IAuthenticationService { */ fun validateTwoFactor(twoFactorValidateParam: TwoFactorValidateParam): Boolean + /** + * Remove two-factor + * + * @param twoFactorRemoveParam Remove two-factor parameters + * @return Result + * @author FatttSnake, fatttsnake@gmail.com + * @since 1.0.0 + * @see TwoFactorRemoveParam + */ + fun removeTwoFactor(twoFactorRemoveParam: TwoFactorRemoveParam): Boolean + /** * Logout * diff --git a/src/main/kotlin/top/fatweb/oxygen/api/service/permission/impl/AuthenticationServiceImpl.kt b/src/main/kotlin/top/fatweb/oxygen/api/service/permission/impl/AuthenticationServiceImpl.kt index 5c782d2..81dd802 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/service/permission/impl/AuthenticationServiceImpl.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/service/permission/impl/AuthenticationServiceImpl.kt @@ -202,7 +202,9 @@ class AuthenticationServiceImpl( @EventLogRecord(EventLog.Event.LOGIN) override fun login(request: HttpServletRequest, loginParam: LoginParam): LoginVo { - verifyCaptcha(loginParam.captchaCode!!) + if (loginParam.twoFactorCode.isNullOrBlank()) { + verifyCaptcha(loginParam.captchaCode!!) + } return this.login(request, loginParam.account!!, loginParam.password!!, loginParam.twoFactorCode) } @@ -236,7 +238,26 @@ class AuthenticationServiceImpl( } val secretKey = user.twoFactor!!.substring(0, user.twoFactor!!.length - 1) - return TOTPUtil.validateCode(secretKey, twoFactorValidateParam.code!!) + if (TOTPUtil.validateCode(secretKey, twoFactorValidateParam.code!!)) { + userService.update(KtUpdateWrapper(User()).eq(User::id, user.id).set(User::twoFactor, secretKey)) + return true + } + + return false + } + + override fun removeTwoFactor(twoFactorRemoveParam: TwoFactorRemoveParam): Boolean { + val user = userService.getById(WebUtil.getLoginUserId()) ?: throw UserNotFoundException() + if (user.twoFactor.isNullOrBlank() || user.twoFactor!!.endsWith("?")) { + throw NoTwoFactorFoundException() + } + + if (TOTPUtil.validateCode(user.twoFactor!!, twoFactorRemoveParam.code!!)) { + userService.update(KtUpdateWrapper(User()).eq(User::id, user.id).set(User::twoFactor, null)) + return true + } + + return false } @EventLogRecord(EventLog.Event.LOGOUT) @@ -342,9 +363,13 @@ class AuthenticationServiceImpl( val userWithPowerByAccount = userService.getUserWithPowerByAccount(account) ?: throw UserNotFoundException() if (!userWithPowerByAccount.twoFactor.isNullOrBlank() && !userWithPowerByAccount.twoFactor!!.endsWith("?") - && twoFactorCode.isNullOrBlank() ) { - throw NeedTwoFactorException() + if (twoFactorCode.isNullOrBlank()) { + throw NeedTwoFactorException() + } + if (!TOTPUtil.validateCode(userWithPowerByAccount.twoFactor!!, twoFactorCode)) { + throw TwoFactorVerificationCodeErrorException() + } } val usernamePasswordAuthenticationToken = diff --git a/src/main/kotlin/top/fatweb/oxygen/api/util/TOTPUtil.kt b/src/main/kotlin/top/fatweb/oxygen/api/util/TOTPUtil.kt index 4b73cc2..93d5b63 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/util/TOTPUtil.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/util/TOTPUtil.kt @@ -120,7 +120,7 @@ object TOTPUtil { secretKey.append(allChars[Random.nextInt(allChars.length)]) } - return secretKey.toString().toList().shuffled().joinToString() + return secretKey.toString().toList().shuffled().joinToString("") } /** diff --git a/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/TwoFactorVo.kt b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/TwoFactorVo.kt index 5ce3040..d49d8b9 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/TwoFactorVo.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/TwoFactorVo.kt @@ -8,7 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema * @author FatttSnake, fatttsnake@gmail.com * @since 1.0.0 */ -@Schema(description = "二步验证返回参数") +@Schema(description = "双因素返回参数") data class TwoFactorVo ( /** * QR code SVG as base64 diff --git a/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithInfoVo.kt b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithInfoVo.kt index a7c53ad..4ee5b99 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithInfoVo.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithInfoVo.kt @@ -38,7 +38,7 @@ data class UserWithInfoVo( * @author FatttSnake, fatttsnake@gmail.com * @since 1.0.0 */ - @Schema(description = "启用二步验证", example = "true") + @Schema(description = "启用双因素", example = "true") val twoFactor: Boolean?, /** diff --git a/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithPasswordRoleInfoVo.kt b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithPasswordRoleInfoVo.kt index 96992de..35bbbe1 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithPasswordRoleInfoVo.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithPasswordRoleInfoVo.kt @@ -49,7 +49,7 @@ data class UserWithPasswordRoleInfoVo( * @author FatttSnake, fatttsnake@gmail.com * @since 1.0.0 */ - @Schema(description = "启用二步验证", example = "true") + @Schema(description = "启用双因素", example = "true") val twoFactor: Boolean?, /** diff --git a/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithPowerInfoVo.kt b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithPowerInfoVo.kt index d9f4e87..d5e99c9 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithPowerInfoVo.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithPowerInfoVo.kt @@ -38,7 +38,7 @@ data class UserWithPowerInfoVo( * @author FatttSnake, fatttsnake@gmail.com * @since 1.0.0 */ - @Schema(description = "启用二步验证", example = "true") + @Schema(description = "启用双因素验证", example = "true") val twoFactor: Boolean?, /** diff --git a/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithRoleInfoVo.kt b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithRoleInfoVo.kt index 0101b13..dd63140 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithRoleInfoVo.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/UserWithRoleInfoVo.kt @@ -40,7 +40,7 @@ data class UserWithRoleInfoVo( * @author FatttSnake, fatttsnake@gmail.com * @since 1.0.0 */ - @Schema(description = "启用二步验证", example = "true") + @Schema(description = "启用双因素", example = "true") val twoFactor: Boolean?, /** diff --git a/src/main/kotlin/top/fatweb/oxygen/api/vo/system/TwoFactorSettingsVo.kt b/src/main/kotlin/top/fatweb/oxygen/api/vo/system/TwoFactorSettingsVo.kt index 80ee980..5e27d9d 100644 --- a/src/main/kotlin/top/fatweb/oxygen/api/vo/system/TwoFactorSettingsVo.kt +++ b/src/main/kotlin/top/fatweb/oxygen/api/vo/system/TwoFactorSettingsVo.kt @@ -8,7 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema * @author FatttSnake, fatttsnake@gmail.com * @since 1.0.0 */ -@Schema(description = "二步验证设置返回参数") +@Schema(description = "双因素设置返回参数") data class TwoFactorSettingsVo( /** * Issuer diff --git a/src/main/resources/db/migration/master/R__Basic_data.sql b/src/main/resources/db/migration/master/R__Basic_data.sql index bbf1dd0..8de3b8b 100644 --- a/src/main/resources/db/migration/master/R__Basic_data.sql +++ b/src/main/resources/db/migration/master/R__Basic_data.sql @@ -169,11 +169,11 @@ insert into t_s_operation(id, name, code, func_id) (1530101, '基础', 'system:settings:query:base', 1530100), (1530102, '邮件', 'system:settings:query:mail', 1530100), (1530103, '敏感词', 'system:settings:query:sensitive', 1530100), - (1530104, '二步验证', 'system:settings:query:two-factor', 1530100), + (1530104, '双因素', 'system:settings:query:two-factor', 1530100), (1530301, '基础', 'system:settings:modify:base', 1530300), (1530302, '邮件', 'system:settings:modify:mail', 1530300), (1530303, '敏感词', 'system:settings:modify:sensitive', 1530300), - (1530304, '二步验证', 'system:settings:modify:two-factor', 1530300), + (1530304, '双因素', 'system:settings:modify:two-factor', 1530300), (1540101, '类别', 'system:tool:query:category', 1540100), (1540102, '基板', 'system:tool:query:base', 1540100), (1540103, '模板', 'system:tool:query:template', 1540100), diff --git a/src/main/resources/db/migration/master/V1_0_0_231019__Add_table_'t_s_user'.sql b/src/main/resources/db/migration/master/V1_0_0_231019__Add_table_'t_s_user'.sql index b5b49c4..4526913 100644 --- a/src/main/resources/db/migration/master/V1_0_0_231019__Add_table_'t_s_user'.sql +++ b/src/main/resources/db/migration/master/V1_0_0_231019__Add_table_'t_s_user'.sql @@ -5,7 +5,7 @@ create table if not exists t_s_user id bigint not null primary key, username varchar(20) not null comment '用户名', password char(70) not null comment '密码', - two_factor varchar(40) null comment '二步验证', + two_factor varchar(40) null comment '双因素', verify varchar(144) null comment '验证邮箱', forget varchar(144) null comment '忘记密码', locking int not null comment '锁定',