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 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<TwoFactorVo> =
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<Nothing> =
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<Nothing> =
if (authenticationService.removeTwoFactor(twoFactorRemoveParam)) ResponseResult.success()
else ResponseResult.fail()
/**
* Logout

View File

@@ -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<TwoFactorSettingsVo> =
@@ -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<Nothing> {

View File

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

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)
}
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)

View File

@@ -42,6 +42,6 @@ data class LoginParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "二步验证码")
@Schema(description = "双因素验证码")
val twoFactorCode: String?
) : 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
* @since 1.0.0
*/
@Schema(description = "验证二步验证请求参数")
@Schema(description = "验证双因素请求参数")
data class TwoFactorValidateParam(
/**
* Code

View File

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

View File

@@ -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
*

View File

@@ -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 =

View File

@@ -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("")
}
/**

View File

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

View File

@@ -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?,
/**

View File

@@ -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?,
/**

View File

@@ -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?,
/**

View File

@@ -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?,
/**

View File

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