diff --git a/pom.xml b/pom.xml
index 133a5d1..6ed7540 100644
--- a/pom.xml
+++ b/pom.xml
@@ -207,6 +207,16 @@
oshi-core
6.4.9
+
+ commons-codec
+ commons-codec
+ 1.16.1
+
+
+ com.google.zxing
+ core
+ 3.5.3
+
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 6958926..b6c9666 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
@@ -16,6 +16,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 +154,33 @@ 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 =
+ 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 =
+ if (authenticationService.validateTwoFactor(twoFactorValidateParam)) 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 9ca025b..1dea226 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
@@ -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 = ResponseResult.success(data = settingsService.getBase())
+ fun getApp(): ResponseResult =
+ 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 = ResponseResult.success(data = settingsService.getMail())
+ fun getMail(): ResponseResult =
+ 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 =
+ 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 {
+ settingsService.updateTwoFactor(twoFactorSettingsParam)
+ return ResponseResult.success()
+ }
}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/UserConverter.kt b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/UserConverter.kt
index 9f299b7..dec373e 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/UserConverter.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/converter/permission/UserConverter.kt
@@ -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,
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 83a3dfd..60a5300 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
@@ -45,6 +45,9 @@ 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),
DATABASE_SELECT_SUCCESS(BusinessCode.DATABASE, 0),
DATABASE_SELECT_FAILED(BusinessCode.DATABASE, 5),
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/User.kt b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/User.kt
index 11324c6..93512fb 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/User.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/entity/permission/User.kt
@@ -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? = 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)"
}
}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/AlreadyHasTwoFactorException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/AlreadyHasTwoFactorException.kt
new file mode 100644
index 0000000..f9339cc
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/AlreadyHasTwoFactorException.kt
@@ -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")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/NeedTwoFactorException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/NeedTwoFactorException.kt
new file mode 100644
index 0000000..fb51ca4
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/NeedTwoFactorException.kt
@@ -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")
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/NoTwoFactorFoundException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/NoTwoFactorFoundException.kt
new file mode 100644
index 0000000..c739da1
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/NoTwoFactorFoundException.kt
@@ -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")
\ 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 8624e10..b8baab0 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt
@@ -165,6 +165,21 @@ 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)
+ }
+
/* 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 66bc3e9..efa013f 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
@@ -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()
\ No newline at end of file
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
new file mode 100644
index 0000000..a75af7a
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/TwoFactorValidateParam.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
+
+/**
+ * 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?
+)
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
new file mode 100644
index 0000000..cbf3601
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/param/system/TwoFactorSettingsParam.kt
@@ -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?
+)
\ No newline at end of file
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 a242964..b9d6eb4 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
@@ -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,27 @@ 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
+
/**
* 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 bd0e265..5c782d2 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
@@ -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
@@ -205,7 +204,39 @@ class AuthenticationServiceImpl(
override fun login(request: HttpServletRequest, loginParam: LoginParam): LoginVo {
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)
+
+ return TOTPUtil.validateCode(secretKey, twoFactorValidateParam.code!!)
}
@EventLogRecord(EventLog.Event.LOGOUT)
@@ -302,7 +333,20 @@ 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("?")
+ && twoFactorCode.isNullOrBlank()
+ ) {
+ throw NeedTwoFactorException()
+ }
+
val usernamePasswordAuthenticationToken =
UsernamePasswordAuthenticationToken(account, password)
val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken)
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/service/permission/impl/UserDetailsServiceImpl.kt b/src/main/kotlin/top/fatweb/oxygen/api/service/permission/impl/UserDetailsServiceImpl.kt
index 5b3bad7..95a3498 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/service/permission/impl/UserDetailsServiceImpl.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/service/permission/impl/UserDetailsServiceImpl.kt
@@ -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)
}
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/service/system/ISettingsService.kt b/src/main/kotlin/top/fatweb/oxygen/api/service/system/ISettingsService.kt
index 0e7eee6..00e8e28 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/service/system/ISettingsService.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/service/system/ISettingsService.kt
@@ -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)
}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/service/system/impl/SettingsServiceImpl.kt b/src/main/kotlin/top/fatweb/oxygen/api/service/system/impl/SettingsServiceImpl.kt
index aa3a7e5..556cc2a 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/service/system/impl/SettingsServiceImpl.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/service/system/impl/SettingsServiceImpl.kt
@@ -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)
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/settings/SettingsOperator.kt b/src/main/kotlin/top/fatweb/oxygen/api/settings/SettingsOperator.kt
index 7388b72..7f71dea 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/settings/SettingsOperator.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/settings/SettingsOperator.kt
@@ -101,7 +101,8 @@ object SettingsOperator {
* @see KMutableProperty1
* @see BaseSettings
*/
- fun getAppValue(field: KMutableProperty1): V? = this.getAppValue(field, null)
+ fun getAppValue(field: KMutableProperty1): V? =
+ this.getAppValue(field, null)
/**
* Get base settings value with default value
@@ -154,7 +155,8 @@ object SettingsOperator {
* @see KMutableProperty1
* @see MailSettings
*/
- fun getMailValue(field: KMutableProperty1): V? = this.getMailValue(field, null)
+ fun getMailValue(field: KMutableProperty1): V? =
+ this.getMailValue(field, null)
/**
* Get value from mail settings with default value
@@ -169,4 +171,51 @@ object SettingsOperator {
*/
fun getMailValue(field: KMutableProperty1, 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 setTwoFactorValue(field: KMutableProperty1, 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 getTwoFactorValue(field: KMutableProperty1): 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 getTwoFactorValue(field: KMutableProperty1, default: V): V =
+ systemSettings.twoFactor?.let(field) ?: default
}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/settings/SystemSettings.kt b/src/main/kotlin/top/fatweb/oxygen/api/settings/SystemSettings.kt
index b15a9f7..ee2b038 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/settings/SystemSettings.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/settings/SystemSettings.kt
@@ -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
)
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/settings/TwoFactorSettings.kt b/src/main/kotlin/top/fatweb/oxygen/api/settings/TwoFactorSettings.kt
new file mode 100644
index 0000000..7eac4a5
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/settings/TwoFactorSettings.kt
@@ -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
+)
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/util/TOTPUtil.kt b/src/main/kotlin/top/fatweb/oxygen/api/util/TOTPUtil.kt
new file mode 100644
index 0000000..4b73cc2
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/util/TOTPUtil.kt
@@ -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()
+ 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("")
+
+ 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))
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..5ce3040
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/TwoFactorVo.kt
@@ -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?
+)
\ No newline at end of file
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 f1f14f7..a7c53ad 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
@@ -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
*
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 945bbdd..96992de 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
@@ -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
*
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 4616b50..d9f4e87 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
@@ -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
*
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 edc69db..0101b13 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
@@ -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
*
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
new file mode 100644
index 0000000..80ee980
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/vo/system/TwoFactorSettingsVo.kt
@@ -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?
+)
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 e8bb177..bbf1dd0 100644
--- a/src/main/resources/db/migration/master/R__Basic_data.sql
+++ b/src/main/resources/db/migration/master/R__Basic_data.sql
@@ -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),
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 9e2287d..b5b49c4 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,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 '系统-用户表';
\ No newline at end of file
diff --git a/src/main/resources/mapper/permission/UserMapper.xml b/src/main/resources/mapper/permission/UserMapper.xml
index 40cb1fb..ecbc757 100644
--- a/src/main/resources/mapper/permission/UserMapper.xml
+++ b/src/main/resources/mapper/permission/UserMapper.xml
@@ -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,
@@ -61,11 +62,11 @@