Optimize two-factor api. Add remove two-factor api.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
|
||||
@@ -42,6 +42,6 @@ data class LoginParam(
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "二步验证码")
|
||||
@Schema(description = "双因素验证码")
|
||||
val twoFactorCode: String?
|
||||
) : CaptchaCodeParam()
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,7 @@ import top.fatweb.oxygen.api.annotation.Trim
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Trim
|
||||
@Schema(description = "二步验证设置请求参数")
|
||||
@Schema(description = "双因素设置请求参数")
|
||||
data class TwoFactorSettingsParam(
|
||||
/**
|
||||
* Issuer
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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("")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?,
|
||||
|
||||
/**
|
||||
|
||||
@@ -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?,
|
||||
|
||||
/**
|
||||
|
||||
@@ -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?,
|
||||
|
||||
/**
|
||||
|
||||
@@ -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?,
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user