Add two-factor #12
10
pom.xml
10
pom.xml
@@ -207,6 +207,16 @@
|
||||
<artifactId>oshi-core</artifactId>
|
||||
<version>6.4.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.16.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>3.5.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -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
|
||||
@@ -16,6 +17,7 @@ import top.fatweb.oxygen.api.util.WebUtil
|
||||
import top.fatweb.oxygen.api.vo.permission.LoginVo
|
||||
import top.fatweb.oxygen.api.vo.permission.RegisterVo
|
||||
import top.fatweb.oxygen.api.vo.permission.TokenVo
|
||||
import top.fatweb.oxygen.api.vo.permission.TwoFactorVo
|
||||
|
||||
/**
|
||||
* Authentication controller
|
||||
@@ -153,6 +155,45 @@ class AuthenticationController(
|
||||
authenticationService.login(request, loginParam)
|
||||
)
|
||||
|
||||
/**
|
||||
* Create two-factor
|
||||
*
|
||||
* @return Response object includes two-factor QR code
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see ResponseResult
|
||||
* @see TwoFactorVo
|
||||
*/
|
||||
@Operation(summary = "创建双因素验证码")
|
||||
@GetMapping("/two-factor")
|
||||
fun createTwoFactor(): ResponseResult<TwoFactorVo> =
|
||||
ResponseResult.success(data = authenticationService.createTwoFactor())
|
||||
|
||||
/**
|
||||
* Validate two-factor
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@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
|
||||
*
|
||||
|
||||
@@ -3,12 +3,7 @@ package top.fatweb.oxygen.api.controller.system
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.PutMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import top.fatweb.oxygen.api.annotation.BaseController
|
||||
import top.fatweb.oxygen.api.annotation.Trim
|
||||
import top.fatweb.oxygen.api.entity.common.ResponseCode
|
||||
@@ -19,6 +14,7 @@ import top.fatweb.oxygen.api.service.system.ISettingsService
|
||||
import top.fatweb.oxygen.api.vo.system.BaseSettingsVo
|
||||
import top.fatweb.oxygen.api.vo.system.MailSettingsVo
|
||||
import top.fatweb.oxygen.api.vo.system.SensitiveWordVo
|
||||
import top.fatweb.oxygen.api.vo.system.TwoFactorSettingsVo
|
||||
|
||||
/**
|
||||
* System settings management controller
|
||||
@@ -45,7 +41,8 @@ class SettingsController(
|
||||
@Operation(summary = "获取基础设置")
|
||||
@GetMapping("/base")
|
||||
@PreAuthorize("hasAnyAuthority('system:settings:query:base')")
|
||||
fun getApp(): ResponseResult<BaseSettingsVo> = ResponseResult.success(data = settingsService.getBase())
|
||||
fun getApp(): ResponseResult<BaseSettingsVo> =
|
||||
ResponseResult.success(data = settingsService.getBase())
|
||||
|
||||
/**
|
||||
* Update base settings
|
||||
@@ -78,7 +75,8 @@ class SettingsController(
|
||||
@Operation(summary = "获取邮件设置")
|
||||
@GetMapping("/mail")
|
||||
@PreAuthorize("hasAnyAuthority('system:settings:query:mail')")
|
||||
fun getMail(): ResponseResult<MailSettingsVo> = ResponseResult.success(data = settingsService.getMail())
|
||||
fun getMail(): ResponseResult<MailSettingsVo> =
|
||||
ResponseResult.success(data = settingsService.getMail())
|
||||
|
||||
/**
|
||||
* Update mail settings
|
||||
@@ -186,4 +184,38 @@ class SettingsController(
|
||||
sensitiveWordService.delete(id)
|
||||
return ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get two-factor settings
|
||||
*
|
||||
* @return Response object includes two-factor settings information
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see ResponseResult
|
||||
* @see TwoFactorSettingsVo
|
||||
*/
|
||||
@Operation(summary = "获取双因素设置")
|
||||
@GetMapping("/two-factor")
|
||||
@PreAuthorize("hasAnyAuthority('system:settings:query:two-factor')")
|
||||
fun getTwoFactor(): ResponseResult<TwoFactorSettingsVo> =
|
||||
ResponseResult.success(data = settingsService.getTwoFactor())
|
||||
|
||||
/**
|
||||
* Update two-factor settings
|
||||
*
|
||||
* @param twoFactorSettingsParam Two-factor settings parameters
|
||||
* @return Response object
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see TwoFactorSettingsParam
|
||||
* @see ResponseResult
|
||||
*/
|
||||
@Trim
|
||||
@Operation(summary = "更新双因素设置")
|
||||
@PutMapping("/two-factor")
|
||||
@PreAuthorize("hasAnyAuthority('system:settings:modify:two-factor')")
|
||||
fun updateTwoFactor(@RequestBody twoFactorSettingsParam: TwoFactorSettingsParam): ResponseResult<Nothing> {
|
||||
settingsService.updateTwoFactor(twoFactorSettingsParam)
|
||||
return ResponseResult.success()
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ object UserConverter {
|
||||
fun userToUserWithPowerInfoVo(user: User) = UserWithPowerInfoVo(
|
||||
id = user.id,
|
||||
username = user.username,
|
||||
twoFactor = !user.twoFactor.isNullOrBlank() && !user.twoFactor!!.endsWith("?"),
|
||||
verified = user.verify.isNullOrBlank(),
|
||||
locking = user.locking?.let { it == 1 },
|
||||
expiration = user.expiration,
|
||||
@@ -65,6 +66,7 @@ object UserConverter {
|
||||
fun userToUserWithRoleInfoVo(user: User) = UserWithRoleInfoVo(
|
||||
id = user.id,
|
||||
username = user.username,
|
||||
twoFactor = !user.twoFactor.isNullOrBlank() && !user.twoFactor!!.endsWith("?"),
|
||||
verify = user.verify,
|
||||
locking = user.locking?.let { it == 1 },
|
||||
expiration = user.expiration,
|
||||
@@ -94,6 +96,7 @@ object UserConverter {
|
||||
fun userToUserWithInfoVo(user: User) = UserWithInfoVo(
|
||||
id = user.id,
|
||||
username = user.username,
|
||||
twoFactor = !user.twoFactor.isNullOrBlank() && !user.twoFactor!!.endsWith("?"),
|
||||
verified = user.verify.isNullOrBlank(),
|
||||
locking = user.locking?.let { it == 1 },
|
||||
expiration = user.expiration,
|
||||
@@ -122,6 +125,7 @@ object UserConverter {
|
||||
id = user.id,
|
||||
username = user.username,
|
||||
password = user.password,
|
||||
twoFactor = !user.twoFactor.isNullOrBlank() && !user.twoFactor!!.endsWith("?"),
|
||||
verify = user.verify,
|
||||
locking = user.locking?.let { it == 1 },
|
||||
expiration = user.expiration,
|
||||
|
||||
@@ -45,6 +45,10 @@ enum class ResponseCode(val code: Int) {
|
||||
PERMISSION_USER_NOT_FOUND(BusinessCode.PERMISSION, 65),
|
||||
PERMISSION_RETRIEVE_CODE_ERROR_OR_EXPIRED(BusinessCode.PERMISSION, 66),
|
||||
PERMISSION_ACCOUNT_NEED_RESET_PASSWORD(BusinessCode.PERMISSION, 67),
|
||||
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),
|
||||
|
||||
@@ -46,6 +46,15 @@ class User() : Serializable {
|
||||
@TableField("password")
|
||||
var password: String? = null
|
||||
|
||||
/**
|
||||
* Two-factor
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@TableField("two_factor")
|
||||
var twoFactor: String? = null
|
||||
|
||||
/**
|
||||
* Verify email
|
||||
*
|
||||
@@ -251,6 +260,6 @@ class User() : Serializable {
|
||||
var operations: List<Operation>? = null
|
||||
|
||||
override fun toString(): String {
|
||||
return "User(id=$id, username=$username, password=$password, verify=$verify, forget=$forget, locking=$locking, expiration=$expiration, credentialsExpiration=$credentialsExpiration, enable=$enable, currentLoginTime=$currentLoginTime, currentLoginIp=$currentLoginIp, lastLoginTime=$lastLoginTime, lastLoginIp=$lastLoginIp, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version, userInfo=$userInfo, roles=$roles, groups=$groups, modules=$modules, menus=$menus, funcs=$funcs, operations=$operations)"
|
||||
return "User(id=$id, username=$username, password=$password, twoFactor=$twoFactor, verify=$verify, forget=$forget, locking=$locking, expiration=$expiration, credentialsExpiration=$credentialsExpiration, enable=$enable, currentLoginTime=$currentLoginTime, currentLoginIp=$currentLoginIp, lastLoginTime=$lastLoginTime, lastLoginIp=$lastLoginIp, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version, userInfo=$userInfo, roles=$roles, groups=$groups, modules=$modules, menus=$menus, funcs=$funcs, operations=$operations)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package top.fatweb.oxygen.api.exception
|
||||
|
||||
/**
|
||||
* Already has two-factor exception
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see RuntimeException
|
||||
*/
|
||||
class AlreadyHasTwoFactorException : RuntimeException("Already has two-factor")
|
||||
@@ -0,0 +1,10 @@
|
||||
package top.fatweb.oxygen.api.exception
|
||||
|
||||
/**
|
||||
* Need two-factor code exception
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see RuntimeException
|
||||
*/
|
||||
class NeedTwoFactorException : RuntimeException("Need two-factor code")
|
||||
@@ -0,0 +1,10 @@
|
||||
package top.fatweb.oxygen.api.exception
|
||||
|
||||
/**
|
||||
* No two-factor found exception
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see RuntimeException
|
||||
*/
|
||||
class NoTwoFactorFoundException : RuntimeException("No two-factor found")
|
||||
@@ -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")
|
||||
@@ -165,6 +165,26 @@ class ExceptionHandler {
|
||||
ResponseResult.fail(ResponseCode.SYSTEM_INVALID_CAPTCHA_CODE, e.localizedMessage, null)
|
||||
}
|
||||
|
||||
is NeedTwoFactorException -> {
|
||||
logger.debug(e.localizedMessage, e)
|
||||
ResponseResult.fail(ResponseCode.PERMISSION_NEED_TWO_FACTOR, e.localizedMessage, null)
|
||||
}
|
||||
|
||||
is AlreadyHasTwoFactorException -> {
|
||||
logger.debug(e.localizedMessage, e)
|
||||
ResponseResult.fail(ResponseCode.PERMISSION_ALREADY_HAS_TWO_FACTOR, e.localizedMessage, null)
|
||||
}
|
||||
|
||||
is NoTwoFactorFoundException -> {
|
||||
logger.debug(e.localizedMessage, e)
|
||||
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)
|
||||
|
||||
@@ -34,5 +34,14 @@ data class LoginParam(
|
||||
*/
|
||||
@Schema(description = "密码", required = true)
|
||||
@field:NotBlank(message = "Password can not be blank")
|
||||
val password: String?
|
||||
val password: String?,
|
||||
|
||||
/**
|
||||
* Two-factor code
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@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?
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package top.fatweb.oxygen.api.param.permission
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
|
||||
/**
|
||||
* Validate two-factor parameters
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "验证双因素请求参数")
|
||||
data class TwoFactorValidateParam(
|
||||
/**
|
||||
* Code
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "验证码")
|
||||
@field:NotBlank(message = "Code can not be blank")
|
||||
val code: String?
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
package top.fatweb.oxygen.api.param.system
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import jakarta.validation.constraints.Min
|
||||
import jakarta.validation.constraints.NotNull
|
||||
import top.fatweb.oxygen.api.annotation.Trim
|
||||
|
||||
/**
|
||||
* Two-factor settings parameters
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Trim
|
||||
@Schema(description = "双因素设置请求参数")
|
||||
data class TwoFactorSettingsParam(
|
||||
/**
|
||||
* Issuer
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Trim
|
||||
@Schema(description = "发布者")
|
||||
var issuer: String?,
|
||||
|
||||
/**
|
||||
* Length of secret key
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "密钥长度")
|
||||
@field:NotNull(message = "Length of secret key can not be null")
|
||||
@field:Min(value = 3, message = "The length of the key must be greater than or equal to 3")
|
||||
val secretKeyLength: Int?
|
||||
)
|
||||
@@ -6,6 +6,7 @@ import top.fatweb.oxygen.api.param.permission.*
|
||||
import top.fatweb.oxygen.api.vo.permission.LoginVo
|
||||
import top.fatweb.oxygen.api.vo.permission.RegisterVo
|
||||
import top.fatweb.oxygen.api.vo.permission.TokenVo
|
||||
import top.fatweb.oxygen.api.vo.permission.TwoFactorVo
|
||||
|
||||
/**
|
||||
* Authentication service interface
|
||||
@@ -82,6 +83,38 @@ interface IAuthenticationService {
|
||||
*/
|
||||
fun login(request: HttpServletRequest, loginParam: LoginParam): LoginVo
|
||||
|
||||
/**
|
||||
* Create two-factor
|
||||
*
|
||||
* @return Two-factor QR code
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see TwoFactorVo
|
||||
*/
|
||||
fun createTwoFactor(): TwoFactorVo
|
||||
|
||||
/**
|
||||
* Validate two-factor
|
||||
*
|
||||
* @param twoFactorValidateParam Validate two-factor parameters
|
||||
* @return Result
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see TwoFactorValidateParam
|
||||
*/
|
||||
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
|
||||
*
|
||||
|
||||
@@ -29,13 +29,12 @@ import top.fatweb.oxygen.api.service.permission.IUserService
|
||||
import top.fatweb.oxygen.api.service.system.ISensitiveWordService
|
||||
import top.fatweb.oxygen.api.settings.BaseSettings
|
||||
import top.fatweb.oxygen.api.settings.SettingsOperator
|
||||
import top.fatweb.oxygen.api.util.JwtUtil
|
||||
import top.fatweb.oxygen.api.util.MailUtil
|
||||
import top.fatweb.oxygen.api.util.RedisUtil
|
||||
import top.fatweb.oxygen.api.util.WebUtil
|
||||
import top.fatweb.oxygen.api.settings.TwoFactorSettings
|
||||
import top.fatweb.oxygen.api.util.*
|
||||
import top.fatweb.oxygen.api.vo.permission.LoginVo
|
||||
import top.fatweb.oxygen.api.vo.permission.RegisterVo
|
||||
import top.fatweb.oxygen.api.vo.permission.TokenVo
|
||||
import top.fatweb.oxygen.api.vo.permission.TwoFactorVo
|
||||
import java.io.StringWriter
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
@@ -203,9 +202,62 @@ class AuthenticationServiceImpl(
|
||||
|
||||
@EventLogRecord(EventLog.Event.LOGIN)
|
||||
override fun login(request: HttpServletRequest, loginParam: LoginParam): LoginVo {
|
||||
if (loginParam.twoFactorCode.isNullOrBlank()) {
|
||||
verifyCaptcha(loginParam.captchaCode!!)
|
||||
}
|
||||
|
||||
return this.login(request, loginParam.account!!, loginParam.password!!)
|
||||
return this.login(request, loginParam.account!!, loginParam.password!!, loginParam.twoFactorCode)
|
||||
}
|
||||
|
||||
override fun createTwoFactor(): TwoFactorVo {
|
||||
val user = userService.getById(WebUtil.getLoginUserId()) ?: throw UserNotFoundException()
|
||||
|
||||
if (!user.twoFactor.isNullOrBlank() && !user.twoFactor!!.endsWith("?")) {
|
||||
throw AlreadyHasTwoFactorException()
|
||||
}
|
||||
|
||||
val secretKey = TOTPUtil.generateSecretKey(SettingsOperator.getTwoFactorValue(TwoFactorSettings::secretKeyLength, 16))
|
||||
val qrCodeSVGBase64 = TOTPUtil.generateQRCodeSVGBase64(
|
||||
SettingsOperator.getTwoFactorValue(TwoFactorSettings::issuer, "OxygenToolbox"),
|
||||
user.username!!,
|
||||
secretKey
|
||||
)
|
||||
|
||||
userService.update(KtUpdateWrapper(User()).eq(User::id, user.id).set(User::twoFactor, "${secretKey}?"))
|
||||
|
||||
return TwoFactorVo(qrCodeSVGBase64)
|
||||
}
|
||||
|
||||
override fun validateTwoFactor(twoFactorValidateParam: TwoFactorValidateParam): Boolean {
|
||||
val user = userService.getById(WebUtil.getLoginUserId()) ?: throw UserNotFoundException()
|
||||
if (user.twoFactor.isNullOrBlank()) {
|
||||
throw NoTwoFactorFoundException()
|
||||
}
|
||||
if (!user.twoFactor!!.endsWith("?")) {
|
||||
throw AlreadyHasTwoFactorException()
|
||||
}
|
||||
val secretKey = user.twoFactor!!.substring(0, user.twoFactor!!.length - 1)
|
||||
|
||||
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)
|
||||
@@ -302,7 +354,24 @@ class AuthenticationServiceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
private fun login(request: HttpServletRequest, account: String, password: String): LoginVo {
|
||||
private fun login(
|
||||
request: HttpServletRequest,
|
||||
account: String,
|
||||
password: String,
|
||||
twoFactorCode: String? = null
|
||||
): LoginVo {
|
||||
val userWithPowerByAccount = userService.getUserWithPowerByAccount(account) ?: throw UserNotFoundException()
|
||||
if (!userWithPowerByAccount.twoFactor.isNullOrBlank()
|
||||
&& !userWithPowerByAccount.twoFactor!!.endsWith("?")
|
||||
) {
|
||||
if (twoFactorCode.isNullOrBlank()) {
|
||||
throw NeedTwoFactorException()
|
||||
}
|
||||
if (!TOTPUtil.validateCode(userWithPowerByAccount.twoFactor!!, twoFactorCode)) {
|
||||
throw TwoFactorVerificationCodeErrorException()
|
||||
}
|
||||
}
|
||||
|
||||
val usernamePasswordAuthenticationToken =
|
||||
UsernamePasswordAuthenticationToken(account, password)
|
||||
val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken)
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.stereotype.Service
|
||||
import top.fatweb.oxygen.api.entity.permission.LoginUser
|
||||
import top.fatweb.oxygen.api.exception.UserNotFoundException
|
||||
import top.fatweb.oxygen.api.service.permission.IUserService
|
||||
|
||||
/**
|
||||
@@ -18,7 +19,7 @@ import top.fatweb.oxygen.api.service.permission.IUserService
|
||||
class UserDetailsServiceImpl(val userService: IUserService) : UserDetailsService {
|
||||
override fun loadUserByUsername(account: String): UserDetails {
|
||||
val user = userService.getUserWithPowerByAccount(account)
|
||||
user ?: throw Exception("Username not found")
|
||||
user ?: throw UserNotFoundException()
|
||||
|
||||
return LoginUser(user)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ package top.fatweb.oxygen.api.service.system
|
||||
import top.fatweb.oxygen.api.param.system.BaseSettingsParam
|
||||
import top.fatweb.oxygen.api.param.system.MailSendParam
|
||||
import top.fatweb.oxygen.api.param.system.MailSettingsParam
|
||||
import top.fatweb.oxygen.api.param.system.TwoFactorSettingsParam
|
||||
import top.fatweb.oxygen.api.vo.system.BaseSettingsVo
|
||||
import top.fatweb.oxygen.api.vo.system.MailSettingsVo
|
||||
import top.fatweb.oxygen.api.vo.system.TwoFactorSettingsVo
|
||||
|
||||
/**
|
||||
* Settings service interface
|
||||
@@ -62,4 +64,24 @@ interface ISettingsService {
|
||||
* @see MailSettingsParam
|
||||
*/
|
||||
fun sendMail(mailSendParam: MailSendParam)
|
||||
|
||||
/**
|
||||
* Get two-factor settings
|
||||
*
|
||||
* @return TwoFactorSettingsVo object
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see TwoFactorSettingsVo
|
||||
*/
|
||||
fun getTwoFactor(): TwoFactorSettingsVo?
|
||||
|
||||
/**
|
||||
* Update two-factor settings
|
||||
*
|
||||
* @param twoFactorSettingsParam Two-factor settings parameters
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see TwoFactorSettingsParam
|
||||
*/
|
||||
fun updateTwoFactor(twoFactorSettingsParam: TwoFactorSettingsParam)
|
||||
}
|
||||
@@ -4,15 +4,18 @@ import org.springframework.stereotype.Service
|
||||
import top.fatweb.oxygen.api.param.system.BaseSettingsParam
|
||||
import top.fatweb.oxygen.api.param.system.MailSendParam
|
||||
import top.fatweb.oxygen.api.param.system.MailSettingsParam
|
||||
import top.fatweb.oxygen.api.param.system.TwoFactorSettingsParam
|
||||
import top.fatweb.oxygen.api.properties.ServerProperties
|
||||
import top.fatweb.oxygen.api.service.system.ISettingsService
|
||||
import top.fatweb.oxygen.api.settings.BaseSettings
|
||||
import top.fatweb.oxygen.api.settings.MailSettings
|
||||
import top.fatweb.oxygen.api.settings.SettingsOperator
|
||||
import top.fatweb.oxygen.api.settings.TwoFactorSettings
|
||||
import top.fatweb.oxygen.api.util.MailUtil
|
||||
import top.fatweb.oxygen.api.util.StrUtil
|
||||
import top.fatweb.oxygen.api.vo.system.BaseSettingsVo
|
||||
import top.fatweb.oxygen.api.vo.system.MailSettingsVo
|
||||
import top.fatweb.oxygen.api.vo.system.TwoFactorSettingsVo
|
||||
|
||||
/**
|
||||
* Settings service implement
|
||||
@@ -79,4 +82,16 @@ class SettingsServiceImpl : ISettingsService {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTwoFactor()= TwoFactorSettingsVo(
|
||||
issuer = SettingsOperator.getTwoFactorValue(TwoFactorSettings::issuer, "OxygenToolbox"),
|
||||
secretKeyLength = SettingsOperator.getTwoFactorValue(TwoFactorSettings::secretKeyLength, 16)
|
||||
)
|
||||
|
||||
override fun updateTwoFactor(twoFactorSettingsParam: TwoFactorSettingsParam) {
|
||||
twoFactorSettingsParam.run {
|
||||
SettingsOperator.setTwoFactorValue(TwoFactorSettings::issuer, issuer)
|
||||
SettingsOperator.setTwoFactorValue(TwoFactorSettings::secretKeyLength, secretKeyLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,8 @@ object SettingsOperator {
|
||||
* @see KMutableProperty1
|
||||
* @see BaseSettings
|
||||
*/
|
||||
fun <V> getAppValue(field: KMutableProperty1<BaseSettings, V?>): V? = this.getAppValue(field, null)
|
||||
fun <V> getAppValue(field: KMutableProperty1<BaseSettings, V?>): V? =
|
||||
this.getAppValue(field, null)
|
||||
|
||||
/**
|
||||
* Get base settings value with default value
|
||||
@@ -154,7 +155,8 @@ object SettingsOperator {
|
||||
* @see KMutableProperty1
|
||||
* @see MailSettings
|
||||
*/
|
||||
fun <V> getMailValue(field: KMutableProperty1<MailSettings, V?>): V? = this.getMailValue(field, null)
|
||||
fun <V> getMailValue(field: KMutableProperty1<MailSettings, V?>): V? =
|
||||
this.getMailValue(field, null)
|
||||
|
||||
/**
|
||||
* Get value from mail settings with default value
|
||||
@@ -169,4 +171,51 @@ object SettingsOperator {
|
||||
*/
|
||||
fun <V> getMailValue(field: KMutableProperty1<MailSettings, V?>, default: V): V =
|
||||
systemSettings.mail?.let(field) ?: default
|
||||
|
||||
/**
|
||||
* Set two-factor settings value
|
||||
*
|
||||
* @param field Field to set value. e.g. TwoFactorSettings::type
|
||||
* @param value Value to set
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see KMutableProperty1
|
||||
* @see TwoFactorSettings
|
||||
*/
|
||||
fun <V> setTwoFactorValue(field: KMutableProperty1<TwoFactorSettings, V?>, value: V?) {
|
||||
systemSettings.twoFactor?.let {
|
||||
field.set(it, value)
|
||||
} ?: let {
|
||||
systemSettings.twoFactor = TwoFactorSettings().also { field.set(it, value) }
|
||||
}
|
||||
|
||||
saveSettingsToFile()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from two-factor settings
|
||||
*
|
||||
* @param field Field to get value from. e.g. TwoFactorSettings::type
|
||||
* @return Value
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see KMutableProperty1
|
||||
* @see TwoFactorSettings
|
||||
*/
|
||||
fun <V> getTwoFactorValue(field: KMutableProperty1<TwoFactorSettings, V?>): V? =
|
||||
this.getTwoFactorValue(field, null)
|
||||
|
||||
/**
|
||||
* Get value from two-factor settings with default value
|
||||
*
|
||||
* @param field Field to get value from. e.g. TwoFactorSettings::type
|
||||
* @param default Return default value when setting not found
|
||||
* @return Value
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see KMutableProperty1
|
||||
* @see TwoFactorSettings
|
||||
*/
|
||||
fun <V> getTwoFactorValue(field: KMutableProperty1<TwoFactorSettings, V?>, default: V): V =
|
||||
systemSettings.twoFactor?.let(field) ?: default
|
||||
}
|
||||
@@ -24,5 +24,13 @@ data class SystemSettings(
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
var mail: MailSettings? = null
|
||||
var mail: MailSettings? = null,
|
||||
|
||||
/**
|
||||
* Two-factor setting
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
var twoFactor: TwoFactorSettings? = null
|
||||
)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package top.fatweb.oxygen.api.settings
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
|
||||
/**
|
||||
* Two-factor settings entity
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
data class TwoFactorSettings(
|
||||
/**
|
||||
* Issuer
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
var issuer: String? = null,
|
||||
|
||||
/**
|
||||
* Length of secret key
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
var secretKeyLength: Int? = 16
|
||||
)
|
||||
198
src/main/kotlin/top/fatweb/oxygen/api/util/TOTPUtil.kt
Normal file
198
src/main/kotlin/top/fatweb/oxygen/api/util/TOTPUtil.kt
Normal file
@@ -0,0 +1,198 @@
|
||||
package top.fatweb.oxygen.api.util
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import org.apache.commons.codec.binary.Base32
|
||||
import java.net.URLEncoder
|
||||
import java.nio.ByteBuffer
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.experimental.and
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.math.pow
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* TOTP util
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
object TOTPUtil {
|
||||
private val TIME_PERIOD = 30.seconds
|
||||
private val FLEXIBLE_TIME = 5.seconds
|
||||
private const val DIGITS = 6
|
||||
|
||||
private fun computeCounterForMillis(millis: Long): Long = Math.floorDiv(millis, TIME_PERIOD.inWholeMilliseconds)
|
||||
|
||||
private fun generateHash(secret: ByteArray, payload: ByteArray): ByteArray {
|
||||
val secretKey = Base32().decode(secret)
|
||||
val hmacSha1 = Mac.getInstance("HmacSHA1")
|
||||
hmacSha1.init(SecretKeySpec(secretKey, "RAW"))
|
||||
|
||||
return hmacSha1.doFinal(payload)
|
||||
}
|
||||
|
||||
private fun truncateHash(hash: ByteArray): ByteArray {
|
||||
val offset = hash.last().and(0x0F).toInt()
|
||||
val truncatedHash = ByteArray(4)
|
||||
for (i in 0..3) {
|
||||
truncatedHash[i] = hash[offset + i]
|
||||
}
|
||||
truncatedHash[0] = truncatedHash[0].and(0x7F)
|
||||
|
||||
return truncatedHash
|
||||
}
|
||||
|
||||
private fun calculateCode(key: String, time: Long): String {
|
||||
val timeCounter = computeCounterForMillis(time)
|
||||
val payload = ByteBuffer.allocate(8).putLong(0, timeCounter).array()
|
||||
val secretKey = key.toByteArray(Charsets.UTF_8)
|
||||
val hash = generateHash(secretKey, payload)
|
||||
val truncatedHash = truncateHash(hash)
|
||||
val code = ByteBuffer.wrap(truncatedHash).int % 10.0.pow(DIGITS).toInt()
|
||||
|
||||
return code.toString().padStart(DIGITS, '0')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TOTP code
|
||||
*
|
||||
* @param secretKey Secret key
|
||||
* @return TOTP Code
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
fun generateCode(secretKey: String, time: Long = System.currentTimeMillis()): String =
|
||||
calculateCode(secretKey, time)
|
||||
|
||||
/**
|
||||
* Validate TOTP code
|
||||
*
|
||||
* @param secretKey Secret key
|
||||
* @param code TOTP code
|
||||
* @return Result
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
fun validateCode(secretKey: String, code: String): Boolean {
|
||||
var time = System.currentTimeMillis()
|
||||
if (calculateCode(secretKey, time) == code) {
|
||||
return true
|
||||
}
|
||||
time -= FLEXIBLE_TIME.inWholeMilliseconds
|
||||
|
||||
return calculateCode(secretKey, time) == code
|
||||
}
|
||||
|
||||
private fun encodeUrl(str: String) = URLEncoder.encode(str, Charsets.UTF_8).replace("+", "%20")
|
||||
|
||||
/**
|
||||
* Generate secret key
|
||||
*
|
||||
* @param length Secret key length
|
||||
* @return Secret key
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
fun generateSecretKey(length: Int = 16): String {
|
||||
if (length < 3) {
|
||||
throw IllegalArgumentException("Password length should be at least 3.")
|
||||
}
|
||||
|
||||
val lowercaseChars = "abcdefghijklmnopqrstuvwxyz"
|
||||
val uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
val digits = "0123456789"
|
||||
val allChars = lowercaseChars + uppercaseChars + digits
|
||||
|
||||
val secretKey = StringBuilder()
|
||||
secretKey.append(
|
||||
lowercaseChars[Random.nextInt(lowercaseChars.length)],
|
||||
uppercaseChars[Random.nextInt(uppercaseChars.length)],
|
||||
digits[Random.nextInt(digits.length)]
|
||||
)
|
||||
repeat(length - 3) {
|
||||
secretKey.append(allChars[Random.nextInt(allChars.length)])
|
||||
}
|
||||
|
||||
return secretKey.toString().toList().shuffled().joinToString("")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TOTP URL
|
||||
*
|
||||
* @param issuer Issuer
|
||||
* @param username Username
|
||||
* @param secretKey Secret key
|
||||
* @return TOTP URL
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
fun generateUrl(issuer: String, username: String, secretKey: String): String =
|
||||
"otpauth://totp/${encodeUrl(issuer)}:${encodeUrl(username)}?secret=${encodeUrl(secretKey)}"
|
||||
|
||||
/**
|
||||
* Generate TOTP QR code
|
||||
*
|
||||
* @param issuer Issuer
|
||||
* @param username Username
|
||||
* @param secretKey Secret key
|
||||
* @return QR code
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see BitMatrix
|
||||
*/
|
||||
fun generateQRCode(issuer: String, username: String, secretKey: String): BitMatrix {
|
||||
val hints = HashMap<EncodeHintType, Any>()
|
||||
hints[EncodeHintType.CHARACTER_SET] = Charsets.UTF_8
|
||||
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.H
|
||||
hints[EncodeHintType.MARGIN] = 1
|
||||
|
||||
return QRCodeWriter().encode(generateUrl(issuer, username, secretKey), BarcodeFormat.QR_CODE, 200, 200, hints)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TOTP QR code SVG
|
||||
*
|
||||
* @param issuer Issuer
|
||||
* @param username Username
|
||||
* @param secretKey Secret key
|
||||
* @return QR code as SVG string
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
fun generateQRCodeSVG(issuer: String, username: String, secretKey: String): String {
|
||||
val qrCode = generateQRCode(issuer, username, secretKey)
|
||||
val stringBuilder = StringBuilder("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 ${qrCode.width} ${qrCode.height}\">")
|
||||
|
||||
for (y in 0 until qrCode.height) {
|
||||
for (x in 0 until qrCode.width) {
|
||||
if (qrCode.get(x, y)) {
|
||||
stringBuilder.appendLine(" <rect width=\"1\" height=\"1\" x=\"$x\" y=\"$y\"/>")
|
||||
}
|
||||
}
|
||||
}
|
||||
stringBuilder.append("</svg>")
|
||||
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TOTP QR code SVG as Base64 string
|
||||
*
|
||||
* @param issuer Issuer
|
||||
* @param username Username
|
||||
* @param secretKey Secret key
|
||||
* @return TOTP QR code base64
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
fun generateQRCodeSVGBase64(issuer: String, username: String, secretKey: String) =
|
||||
Base64.encode(generateQRCodeSVG(issuer, username, secretKey).toByteArray(Charsets.UTF_8))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package top.fatweb.oxygen.api.vo.permission
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
|
||||
/**
|
||||
* Two-factor value object
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "双因素返回参数")
|
||||
data class TwoFactorVo (
|
||||
/**
|
||||
* QR code SVG as base64
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "二维码 SVG Base64")
|
||||
val qrCodeSVGBase64: String?
|
||||
)
|
||||
@@ -32,6 +32,15 @@ data class UserWithInfoVo(
|
||||
@Schema(description = "用户名", example = "User")
|
||||
val username: String?,
|
||||
|
||||
/**
|
||||
* Two-factor enable
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "启用双因素", example = "true")
|
||||
val twoFactor: Boolean?,
|
||||
|
||||
/**
|
||||
* Verified
|
||||
*
|
||||
|
||||
@@ -43,6 +43,15 @@ data class UserWithPasswordRoleInfoVo(
|
||||
@Schema(description = "密码")
|
||||
val password: String?,
|
||||
|
||||
/**
|
||||
* Two-factor enable
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "启用双因素", example = "true")
|
||||
val twoFactor: Boolean?,
|
||||
|
||||
/**
|
||||
* Verify
|
||||
*
|
||||
|
||||
@@ -32,6 +32,15 @@ data class UserWithPowerInfoVo(
|
||||
@Schema(description = "用户名", example = "User")
|
||||
val username: String?,
|
||||
|
||||
/**
|
||||
* Two-factor enable
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "启用双因素验证", example = "true")
|
||||
val twoFactor: Boolean?,
|
||||
|
||||
/**
|
||||
* Verified
|
||||
*
|
||||
|
||||
@@ -34,6 +34,15 @@ data class UserWithRoleInfoVo(
|
||||
@Schema(description = "用户名", example = "User")
|
||||
val username: String?,
|
||||
|
||||
/**
|
||||
* Two-factor enable
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "启用双因素", example = "true")
|
||||
val twoFactor: Boolean?,
|
||||
|
||||
/**
|
||||
* Verify
|
||||
*
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package top.fatweb.oxygen.api.vo.system
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
|
||||
/**
|
||||
* Two-factor settings value object
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "双因素设置返回参数")
|
||||
data class TwoFactorSettingsVo(
|
||||
/**
|
||||
* Issuer
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "发布者")
|
||||
val issuer: String?,
|
||||
|
||||
/**
|
||||
* Length of secret key
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "密钥长度")
|
||||
val secretKeyLength: Int?
|
||||
)
|
||||
@@ -69,9 +69,11 @@ insert into t_s_power (id, type_id)
|
||||
(1530101, 4),
|
||||
(1530102, 4),
|
||||
(1530103, 4),
|
||||
(1530104, 4),
|
||||
(1530301, 4),
|
||||
(1530302, 4),
|
||||
(1530303, 4),
|
||||
(1530304, 4),
|
||||
(1540101, 4),
|
||||
(1540102, 4),
|
||||
(1540103, 4),
|
||||
@@ -167,9 +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),
|
||||
(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),
|
||||
(1540101, '类别', 'system:tool:query:category', 1540100),
|
||||
(1540102, '基板', 'system:tool:query:base', 1540100),
|
||||
(1540103, '模板', 'system:tool:query:template', 1540100),
|
||||
|
||||
@@ -5,6 +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 '双因素',
|
||||
verify varchar(144) null comment '验证邮箱',
|
||||
forget varchar(144) null comment '忘记密码',
|
||||
locking int not null comment '锁定',
|
||||
@@ -20,6 +21,7 @@ create table if not exists t_s_user
|
||||
deleted bigint not null default 0,
|
||||
version int not null default 0,
|
||||
constraint t_s_user_unique_username unique (username, deleted),
|
||||
constraint t_s_user_unique_two_factor unique (two_factor, deleted),
|
||||
constraint t_s_user_unique_verify unique (verify, deleted),
|
||||
constraint t_s_user_unique_forget unique (forget, deleted)
|
||||
) comment '系统-用户表';
|
||||
@@ -5,6 +5,7 @@
|
||||
select t_s_user.id as user_id,
|
||||
t_s_user.username as user_username,
|
||||
t_s_user.password as user_password,
|
||||
t_s_user.two_factor as user_two_factor,
|
||||
t_s_user.verify as user_verify,
|
||||
t_s_user.forget as user_forget,
|
||||
t_s_user.locking as user_locking,
|
||||
@@ -140,6 +141,7 @@
|
||||
<select id="selectListWithRoleInfoByIds" resultMap="userWithRoleInfoMap">
|
||||
select t_s_user.id as user_id,
|
||||
t_s_user.username as user_username,
|
||||
t_s_user.two_factor as user_two_factor,
|
||||
t_s_user.verify as user_verify,
|
||||
t_s_user.forget as user_forget,
|
||||
t_s_user.locking as user_locking,
|
||||
@@ -196,6 +198,7 @@
|
||||
<select id="selectOneWithRoleInfoById" resultMap="userWithRoleInfoMap">
|
||||
select t_s_user.id as user_id,
|
||||
t_s_user.username as user_username,
|
||||
t_s_user.two_factor as user_two_factor,
|
||||
t_s_user.verify as user_verify,
|
||||
t_s_user.forget as user_forget,
|
||||
t_s_user.locking as user_locking,
|
||||
@@ -303,6 +306,7 @@
|
||||
<resultMap id="userBaseMap" type="user">
|
||||
<id property="id" column="user_id"/>
|
||||
<result property="username" column="user_username"/>
|
||||
<result property="twoFactor" column="user_two_factor"/>
|
||||
<result property="verify" column="user_verify"/>
|
||||
<result property="forget" column="user_forget"/>
|
||||
<result property="locking" column="user_locking"/>
|
||||
|
||||
Reference in New Issue
Block a user