Optimize two-factor api. Add remove two-factor api.

This commit is contained in:
2024-03-01 15:35:32 +08:00
parent b52ce7f5e8
commit 570f5a8ac6
20 changed files with 109 additions and 21 deletions

View File

@@ -3,6 +3,7 @@ package top.fatweb.oxygen.api.controller.permission
import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.Valid import jakarta.validation.Valid
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
@@ -163,7 +164,7 @@ class AuthenticationController(
* @see ResponseResult * @see ResponseResult
* @see TwoFactorVo * @see TwoFactorVo
*/ */
@Operation(summary = "创建二步验证") @Operation(summary = "创建双因素验证")
@GetMapping("/two-factor") @GetMapping("/two-factor")
fun createTwoFactor(): ResponseResult<TwoFactorVo> = fun createTwoFactor(): ResponseResult<TwoFactorVo> =
ResponseResult.success(data = authenticationService.createTwoFactor()) ResponseResult.success(data = authenticationService.createTwoFactor())
@@ -174,12 +175,24 @@ class AuthenticationController(
* @author FatttSnake, fatttsnake@gmail.com * @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0 * @since 1.0.0
*/ */
@Operation(summary = "验证二步验证") @Operation(summary = "验证双因素")
@PostMapping("/two-factor") @PostMapping("/two-factor")
fun validateTwoFactor(@RequestBody @Valid twoFactorValidateParam: TwoFactorValidateParam): ResponseResult<Nothing> = fun validateTwoFactor(@RequestBody @Valid twoFactorValidateParam: TwoFactorValidateParam): ResponseResult<Nothing> =
if (authenticationService.validateTwoFactor(twoFactorValidateParam)) ResponseResult.success() if (authenticationService.validateTwoFactor(twoFactorValidateParam)) ResponseResult.success()
else ResponseResult.fail() 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<Nothing> =
if (authenticationService.removeTwoFactor(twoFactorRemoveParam)) ResponseResult.success()
else ResponseResult.fail()
/** /**
* Logout * Logout

View File

@@ -194,7 +194,7 @@ class SettingsController(
* @see ResponseResult * @see ResponseResult
* @see TwoFactorSettingsVo * @see TwoFactorSettingsVo
*/ */
@Operation(summary = "获取二步验证设置") @Operation(summary = "获取双因素设置")
@GetMapping("/two-factor") @GetMapping("/two-factor")
@PreAuthorize("hasAnyAuthority('system:settings:query:two-factor')") @PreAuthorize("hasAnyAuthority('system:settings:query:two-factor')")
fun getTwoFactor(): ResponseResult<TwoFactorSettingsVo> = fun getTwoFactor(): ResponseResult<TwoFactorSettingsVo> =
@@ -211,7 +211,7 @@ class SettingsController(
* @see ResponseResult * @see ResponseResult
*/ */
@Trim @Trim
@Operation(summary = "更新二步验证设置") @Operation(summary = "更新双因素设置")
@PutMapping("/two-factor") @PutMapping("/two-factor")
@PreAuthorize("hasAnyAuthority('system:settings:modify:two-factor')") @PreAuthorize("hasAnyAuthority('system:settings:modify:two-factor')")
fun updateTwoFactor(@RequestBody twoFactorSettingsParam: TwoFactorSettingsParam): ResponseResult<Nothing> { fun updateTwoFactor(@RequestBody twoFactorSettingsParam: TwoFactorSettingsParam): ResponseResult<Nothing> {

View File

@@ -48,6 +48,7 @@ enum class ResponseCode(val code: Int) {
PERMISSION_NEED_TWO_FACTOR(BusinessCode.PERMISSION, 68), PERMISSION_NEED_TWO_FACTOR(BusinessCode.PERMISSION, 68),
PERMISSION_ALREADY_HAS_TWO_FACTOR(BusinessCode.PERMISSION, 69), PERMISSION_ALREADY_HAS_TWO_FACTOR(BusinessCode.PERMISSION, 69),
PERMISSION_NO_TWO_FACTOR_FOUND(BusinessCode.PERMISSION, 70), 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_SUCCESS(BusinessCode.DATABASE, 0),
DATABASE_SELECT_FAILED(BusinessCode.DATABASE, 5), DATABASE_SELECT_FAILED(BusinessCode.DATABASE, 5),

View File

@@ -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")

View File

@@ -180,6 +180,11 @@ class ExceptionHandler {
ResponseResult.fail(ResponseCode.PERMISSION_NO_TWO_FACTOR_FOUND, e.localizedMessage, null) 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 */ /* SQL */
is DatabaseSelectException -> { is DatabaseSelectException -> {
logger.debug(e.localizedMessage, e) logger.debug(e.localizedMessage, e)

View File

@@ -42,6 +42,6 @@ data class LoginParam(
* @author FatttSnake, fatttsnake@gmail.com * @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0 * @since 1.0.0
*/ */
@Schema(description = "二步验证码") @Schema(description = "双因素验证码")
val twoFactorCode: String? val twoFactorCode: String?
) : CaptchaCodeParam() ) : CaptchaCodeParam()

View File

@@ -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?
)

View File

@@ -9,7 +9,7 @@ import jakarta.validation.constraints.NotBlank
* @author FatttSnake, fatttsnake@gmail.com * @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0 * @since 1.0.0
*/ */
@Schema(description = "验证二步验证请求参数") @Schema(description = "验证双因素请求参数")
data class TwoFactorValidateParam( data class TwoFactorValidateParam(
/** /**
* Code * Code

View File

@@ -12,7 +12,7 @@ import top.fatweb.oxygen.api.annotation.Trim
* @since 1.0.0 * @since 1.0.0
*/ */
@Trim @Trim
@Schema(description = "二步验证设置请求参数") @Schema(description = "双因素设置请求参数")
data class TwoFactorSettingsParam( data class TwoFactorSettingsParam(
/** /**
* Issuer * Issuer

View File

@@ -104,6 +104,17 @@ interface IAuthenticationService {
*/ */
fun validateTwoFactor(twoFactorValidateParam: TwoFactorValidateParam): Boolean 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 * Logout
* *

View File

@@ -202,7 +202,9 @@ class AuthenticationServiceImpl(
@EventLogRecord(EventLog.Event.LOGIN) @EventLogRecord(EventLog.Event.LOGIN)
override fun login(request: HttpServletRequest, loginParam: LoginParam): LoginVo { override fun login(request: HttpServletRequest, loginParam: LoginParam): LoginVo {
if (loginParam.twoFactorCode.isNullOrBlank()) {
verifyCaptcha(loginParam.captchaCode!!) verifyCaptcha(loginParam.captchaCode!!)
}
return this.login(request, loginParam.account!!, loginParam.password!!, loginParam.twoFactorCode) 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) 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) @EventLogRecord(EventLog.Event.LOGOUT)
@@ -342,10 +363,14 @@ class AuthenticationServiceImpl(
val userWithPowerByAccount = userService.getUserWithPowerByAccount(account) ?: throw UserNotFoundException() val userWithPowerByAccount = userService.getUserWithPowerByAccount(account) ?: throw UserNotFoundException()
if (!userWithPowerByAccount.twoFactor.isNullOrBlank() if (!userWithPowerByAccount.twoFactor.isNullOrBlank()
&& !userWithPowerByAccount.twoFactor!!.endsWith("?") && !userWithPowerByAccount.twoFactor!!.endsWith("?")
&& twoFactorCode.isNullOrBlank()
) { ) {
if (twoFactorCode.isNullOrBlank()) {
throw NeedTwoFactorException() throw NeedTwoFactorException()
} }
if (!TOTPUtil.validateCode(userWithPowerByAccount.twoFactor!!, twoFactorCode)) {
throw TwoFactorVerificationCodeErrorException()
}
}
val usernamePasswordAuthenticationToken = val usernamePasswordAuthenticationToken =
UsernamePasswordAuthenticationToken(account, password) UsernamePasswordAuthenticationToken(account, password)

View File

@@ -120,7 +120,7 @@ object TOTPUtil {
secretKey.append(allChars[Random.nextInt(allChars.length)]) secretKey.append(allChars[Random.nextInt(allChars.length)])
} }
return secretKey.toString().toList().shuffled().joinToString() return secretKey.toString().toList().shuffled().joinToString("")
} }
/** /**

View File

@@ -8,7 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema
* @author FatttSnake, fatttsnake@gmail.com * @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0 * @since 1.0.0
*/ */
@Schema(description = "二步验证返回参数") @Schema(description = "双因素返回参数")
data class TwoFactorVo ( data class TwoFactorVo (
/** /**
* QR code SVG as base64 * QR code SVG as base64

View File

@@ -38,7 +38,7 @@ data class UserWithInfoVo(
* @author FatttSnake, fatttsnake@gmail.com * @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0 * @since 1.0.0
*/ */
@Schema(description = "启用二步验证", example = "true") @Schema(description = "启用双因素", example = "true")
val twoFactor: Boolean?, val twoFactor: Boolean?,
/** /**

View File

@@ -49,7 +49,7 @@ data class UserWithPasswordRoleInfoVo(
* @author FatttSnake, fatttsnake@gmail.com * @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0 * @since 1.0.0
*/ */
@Schema(description = "启用二步验证", example = "true") @Schema(description = "启用双因素", example = "true")
val twoFactor: Boolean?, val twoFactor: Boolean?,
/** /**

View File

@@ -38,7 +38,7 @@ data class UserWithPowerInfoVo(
* @author FatttSnake, fatttsnake@gmail.com * @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0 * @since 1.0.0
*/ */
@Schema(description = "启用二步验证", example = "true") @Schema(description = "启用双因素验证", example = "true")
val twoFactor: Boolean?, val twoFactor: Boolean?,
/** /**

View File

@@ -40,7 +40,7 @@ data class UserWithRoleInfoVo(
* @author FatttSnake, fatttsnake@gmail.com * @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0 * @since 1.0.0
*/ */
@Schema(description = "启用二步验证", example = "true") @Schema(description = "启用双因素", example = "true")
val twoFactor: Boolean?, val twoFactor: Boolean?,
/** /**

View File

@@ -8,7 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema
* @author FatttSnake, fatttsnake@gmail.com * @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0 * @since 1.0.0
*/ */
@Schema(description = "二步验证设置返回参数") @Schema(description = "双因素设置返回参数")
data class TwoFactorSettingsVo( data class TwoFactorSettingsVo(
/** /**
* Issuer * Issuer

View File

@@ -169,11 +169,11 @@ insert into t_s_operation(id, name, code, func_id)
(1530101, '基础', 'system:settings:query:base', 1530100), (1530101, '基础', 'system:settings:query:base', 1530100),
(1530102, '邮件', 'system:settings:query:mail', 1530100), (1530102, '邮件', 'system:settings:query:mail', 1530100),
(1530103, '敏感词', 'system:settings:query:sensitive', 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), (1530301, '基础', 'system:settings:modify:base', 1530300),
(1530302, '邮件', 'system:settings:modify:mail', 1530300), (1530302, '邮件', 'system:settings:modify:mail', 1530300),
(1530303, '敏感词', 'system:settings:modify:sensitive', 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), (1540101, '类别', 'system:tool:query:category', 1540100),
(1540102, '基板', 'system:tool:query:base', 1540100), (1540102, '基板', 'system:tool:query:base', 1540100),
(1540103, '模板', 'system:tool:query:template', 1540100), (1540103, '模板', 'system:tool:query:template', 1540100),

View File

@@ -5,7 +5,7 @@ create table if not exists t_s_user
id bigint not null primary key, id bigint not null primary key,
username varchar(20) not null comment '用户名', username varchar(20) not null comment '用户名',
password char(70) 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 '验证邮箱', verify varchar(144) null comment '验证邮箱',
forget varchar(144) null comment '忘记密码', forget varchar(144) null comment '忘记密码',
locking int not null comment '锁定', locking int not null comment '锁定',