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 d2b1dd3..ef3ad98 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
@@ -14,6 +14,7 @@ enum class ResponseCode(val code: Int) {
SYSTEM_REQUEST_ILLEGAL(BusinessCode.SYSTEM, 52),
SYSTEM_ARGUMENT_NOT_VALID(BusinessCode.SYSTEM, 53),
SYSTEM_INVALID_CAPTCHA_CODE(BusinessCode.SYSTEM, 54),
+ SYSTEM_REQUEST_TOO_FREQUENT(BusinessCode.SYSTEM, 55),
PERMISSION_LOGIN_SUCCESS(BusinessCode.PERMISSION, 0),
PERMISSION_PASSWORD_CHANGE_SUCCESS(BusinessCode.PERMISSION, 1),
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/RequestTooFrequent.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/RequestTooFrequent.kt
new file mode 100644
index 0000000..be1f1e2
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/RequestTooFrequent.kt
@@ -0,0 +1,9 @@
+package top.fatweb.oxygen.api.exception
+
+/**
+ * Request too frequent exception
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+class RequestTooFrequent: RuntimeException("Request too frequent")
\ 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 333724e..477bc62 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt
@@ -58,6 +58,11 @@ class ExceptionHandler {
ResponseResult.fail(ResponseCode.SYSTEM_ARGUMENT_NOT_VALID, errorMessage, null)
}
+ is RequestTooFrequent -> {
+ logger.debug(e.localizedMessage, e)
+ ResponseResult.fail(ResponseCode.SYSTEM_REQUEST_TOO_FREQUENT, e.localizedMessage, null)
+ }
+
is InsufficientAuthenticationException -> {
logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.PERMISSION_UNAUTHORIZED, e.localizedMessage, null)
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/param/CaptchaCodeParam.kt b/src/main/kotlin/top/fatweb/oxygen/api/param/CaptchaCodeParam.kt
new file mode 100644
index 0000000..fa686ce
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/param/CaptchaCodeParam.kt
@@ -0,0 +1,22 @@
+package top.fatweb.oxygen.api.param
+
+import io.swagger.v3.oas.annotations.media.Schema
+import jakarta.validation.constraints.NotBlank
+
+/**
+ * Captcha code parameter
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+open class CaptchaCodeParam {
+ /**
+ * Captcha code
+ *
+ * @author FatttSnake, fatttsnake@gmail.com
+ * @since 1.0.0
+ */
+ @Schema(description = "验证码", required = true)
+ @field:NotBlank(message = "Captcha code can not be blank")
+ var captchaCode: String? = null
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/param/permission/ForgetParam.kt b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/ForgetParam.kt
index 05091d2..e6ff4ce 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/param/permission/ForgetParam.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/ForgetParam.kt
@@ -3,6 +3,7 @@ package top.fatweb.oxygen.api.param.permission
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern
+import top.fatweb.oxygen.api.param.CaptchaCodeParam
/**
* Forget password parameters
@@ -22,4 +23,4 @@ data class ForgetParam(
@field:NotBlank(message = "Email can not be blank")
@field:Pattern(regexp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*\$", message = "Illegal email address")
val email: String?
-)
+) : CaptchaCodeParam()
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 4f8b391..8112d73 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
@@ -2,6 +2,7 @@ package top.fatweb.oxygen.api.param.permission
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
+import top.fatweb.oxygen.api.param.CaptchaCodeParam
/**
* Login parameters
@@ -29,15 +30,5 @@ data class LoginParam(
*/
@Schema(description = "密码", required = true)
@field:NotBlank(message = "Password can not be blank")
- val password: String?,
-
- /**
- * Captcha code
- *
- * @author FatttSnake, fatttsnake@gmail.com
- * @since 1.0.0
- */
- @Schema(description = "验证码", required = true)
- @field:NotBlank(message = "Captcha code can not be blank")
- val captchaCode: String?
-)
\ No newline at end of file
+ val password: String?
+) : CaptchaCodeParam()
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/param/permission/RegisterParam.kt b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/RegisterParam.kt
index e32ee3a..47af7de 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/param/permission/RegisterParam.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/RegisterParam.kt
@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Size
+import top.fatweb.oxygen.api.param.CaptchaCodeParam
/**
* Register parameters
@@ -45,4 +46,4 @@ data class RegisterParam(
@field:NotBlank(message = "Password can not be blank")
@field:Size(min = 10, max = 30)
val password: String?
-)
\ No newline at end of file
+) : CaptchaCodeParam()
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/param/permission/RetrieveParam.kt b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/RetrieveParam.kt
index f3571d5..b94a786 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/param/permission/RetrieveParam.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/param/permission/RetrieveParam.kt
@@ -3,6 +3,7 @@ package top.fatweb.oxygen.api.param.permission
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
+import top.fatweb.oxygen.api.param.CaptchaCodeParam
/**
* Retrieve password parameters
@@ -32,4 +33,4 @@ data class RetrieveParam(
@field:NotBlank(message = "New password can not be blank")
@field:Size(min = 10, max = 30)
val password: String?
-)
+) : CaptchaCodeParam()
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 95949d9..5bb7f3f 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
@@ -67,6 +67,8 @@ class AuthenticationServiceImpl(
@EventLogRecord(EventLog.Event.REGISTER)
@Transactional
override fun register(request: HttpServletRequest, registerParam: RegisterParam): RegisterVo {
+ verifyCaptcha(registerParam.captchaCode!!)
+
val user = User().apply {
username = registerParam.username
password = passwordEncoder.encode(registerParam.password)
@@ -98,6 +100,12 @@ class AuthenticationServiceImpl(
user.verify ?: throw NoVerificationRequiredException()
+ if (LocalDateTime.ofInstant(Instant.ofEpochMilli(user.verify!!.split("-").first().toLong()), ZoneOffset.UTC)
+ .isAfter(LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5))
+ ) {
+ throw RequestTooFrequent()
+ }
+
user.verify =
"${
LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()
@@ -110,30 +118,6 @@ class AuthenticationServiceImpl(
} ?: throw AccessDeniedException("Access Denied")
}
- private fun sendVerifyMail(username: String, code: String, email: String) {
- val velocityContext = VelocityContext().apply {
- put("appName", SettingsOperator.getAppValue(BaseSettings::appName, "氧工具"))
- put("appUrl", SettingsOperator.getAppValue(BaseSettings::appUrl, "http://localhost"))
- put("username", username)
- put(
- "verifyUrl",
- SettingsOperator.getAppValue(BaseSettings::verifyUrl, "http://localhost/verify?code=\${verifyCode}")
- .replace(
- Regex("(?<=([^\\\\]))\\$\\{verifyCode}"), code
- )
- )
- }
- val template = velocityEngine.getTemplate("templates/email-verify-account-cn.vm")
-
- val stringWriter = StringWriter()
- template.merge(velocityContext, stringWriter)
-
- MailUtil.sendSimpleMail(
- "验证您的账号", stringWriter.toString(), true,
- email
- )
- }
-
@EventLogRecord(EventLog.Event.VERIFY)
@Transactional
override fun verify(verifyParam: VerifyParam) {
@@ -161,8 +145,19 @@ class AuthenticationServiceImpl(
@Transactional
override fun forget(request: HttpServletRequest, forgetParam: ForgetParam) {
+ verifyCaptcha(forgetParam.captchaCode!!)
+
val user = userService.getUserWithPowerByAccount(forgetParam.email!!)
user ?: let { throw UserNotFoundException() }
+
+ user.forget?.let {
+ if (LocalDateTime.ofInstant(Instant.ofEpochMilli(it.split("-").first().toLong()), ZoneOffset.UTC)
+ .isAfter(LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5))
+ ) {
+ throw RequestTooFrequent()
+ }
+ }
+
val code = "${
LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()
}-${UUID.randomUUID()}-${UUID.randomUUID()}-${UUID.randomUUID()}"
@@ -170,6 +165,95 @@ class AuthenticationServiceImpl(
sendRetrieveMail(user.username!!, request.remoteAddr, code, forgetParam.email)
}
+ @Transactional
+ override fun retrieve(request: HttpServletRequest, retrieveParam: RetrieveParam) {
+ verifyCaptcha(retrieveParam.captchaCode!!)
+
+ val codeStrings = retrieveParam.code!!.split("-")
+ if (codeStrings.size != 16) {
+ throw RetrieveCodeErrorOrExpiredException()
+ }
+ try {
+ if (LocalDateTime.ofInstant(Instant.ofEpochMilli(codeStrings.first().toLong()), ZoneOffset.UTC)
+ .isBefore(LocalDateTime.now(ZoneOffset.UTC).minusHours(2))
+ ) {
+ throw RetrieveCodeErrorOrExpiredException()
+ }
+ } catch (e: Exception) {
+ throw RetrieveCodeErrorOrExpiredException()
+ }
+
+ val user = userService.getOne(KtQueryWrapper(User()).eq(User::forget, retrieveParam.code))
+ ?: throw RetrieveCodeErrorOrExpiredException()
+ val userInfo = userInfoService.getOne(KtQueryWrapper(UserInfo()).eq(UserInfo::userId, user.id))
+
+ userService.update(
+ KtUpdateWrapper(User()).eq(User::id, user.id).set(User::forget, null)
+ .set(User::password, passwordEncoder.encode(retrieveParam.password!!))
+ )
+
+ WebUtil.offlineUser(redisUtil, user.id!!)
+
+ sendPasswordChangedMail(user.username!!, request.remoteAddr, userInfo!!.email!!)
+ }
+
+ @EventLogRecord(EventLog.Event.LOGIN)
+ override fun login(request: HttpServletRequest, loginParam: LoginParam): LoginVo {
+ verifyCaptcha(loginParam.captchaCode!!)
+
+ return this.login(request, loginParam.account!!, loginParam.password!!)
+ }
+
+ @EventLogRecord(EventLog.Event.LOGOUT)
+ override fun logout(token: String): Boolean {
+ val loginUser = WebUtil.getLoginUser() ?: let { throw TokenHasExpiredException() }
+
+ return redisUtil.delObject("${SecurityProperties.jwtIssuer}_login_${loginUser.user.id}:" + token)
+ }
+
+ override fun renewToken(token: String): TokenVo {
+ val loginUser = WebUtil.getLoginUser() ?: let { throw TokenHasExpiredException() }
+
+ val oldRedisKey = "${SecurityProperties.jwtIssuer}_login_${loginUser.user.id}:" + token
+ redisUtil.delObject(oldRedisKey)
+ val jwt = JwtUtil.createJwt(WebUtil.getLoginUserId().toString())
+
+ jwt ?: let {
+ throw RuntimeException("Login failed")
+ }
+
+ val redisKey = "${SecurityProperties.jwtIssuer}_login_${loginUser.user.id}:" + jwt
+ redisUtil.setObject(
+ redisKey, loginUser, SecurityProperties.redisTtl, SecurityProperties.redisTtlUnit
+ )
+
+ return TokenVo(jwt)
+ }
+
+ private fun sendVerifyMail(username: String, code: String, email: String) {
+ val velocityContext = VelocityContext().apply {
+ put("appName", SettingsOperator.getAppValue(BaseSettings::appName, "氧工具"))
+ put("appUrl", SettingsOperator.getAppValue(BaseSettings::appUrl, "http://localhost"))
+ put("username", username)
+ put(
+ "verifyUrl",
+ SettingsOperator.getAppValue(BaseSettings::verifyUrl, "http://localhost/verify?code=\${verifyCode}")
+ .replace(
+ Regex("(?<=([^\\\\]))\\$\\{verifyCode}"), code
+ )
+ )
+ }
+ val template = velocityEngine.getTemplate("templates/email-verify-account-cn.vm")
+
+ val stringWriter = StringWriter()
+ template.merge(velocityContext, stringWriter)
+
+ MailUtil.sendSimpleMail(
+ "验证您的账号", stringWriter.toString(), true,
+ email
+ )
+ }
+
private fun sendRetrieveMail(username: String, ip: String, code: String, email: String) {
val velocityContext = VelocityContext().apply {
put("appName", SettingsOperator.getAppValue(BaseSettings::appName, "氧工具"))
@@ -198,36 +282,6 @@ class AuthenticationServiceImpl(
)
}
- @Transactional
- override fun retrieve(request: HttpServletRequest, retrieveParam: RetrieveParam) {
- val codeStrings = retrieveParam.code!!.split("-")
- if (codeStrings.size != 16) {
- throw RetrieveCodeErrorOrExpiredException()
- }
- try {
- if (LocalDateTime.ofInstant(Instant.ofEpochMilli(codeStrings.first().toLong()), ZoneOffset.UTC)
- .isBefore(LocalDateTime.now(ZoneOffset.UTC).minusHours(2))
- ) {
- throw RetrieveCodeErrorOrExpiredException()
- }
- } catch (e: Exception) {
- throw RetrieveCodeErrorOrExpiredException()
- }
-
- val user = userService.getOne(KtQueryWrapper(User()).eq(User::forget, retrieveParam.code))
- ?: throw RetrieveCodeErrorOrExpiredException()
- val userInfo = userInfoService.getOne(KtQueryWrapper(UserInfo()).eq(UserInfo::userId, user.id))
-
- userService.update(
- KtUpdateWrapper(User()).eq(User::id, user.id).set(User::forget, null)
- .set(User::password, passwordEncoder.encode(retrieveParam.password!!))
- )
-
- WebUtil.offlineUser(redisUtil, user.id!!)
-
- sendPasswordChangedMail(user.username!!, request.remoteAddr, userInfo!!.email!!)
- }
-
private fun sendPasswordChangedMail(username: String, ip: String, email: String) {
val velocityContext = VelocityContext().apply {
put("appName", SettingsOperator.getAppValue(BaseSettings::appName, "氧工具"))
@@ -246,20 +300,6 @@ class AuthenticationServiceImpl(
)
}
- @EventLogRecord(EventLog.Event.LOGIN)
- override fun login(request: HttpServletRequest, loginParam: LoginParam): LoginVo {
- try {
- val siteverifyResponse = turnstileApi.siteverify(loginParam.captchaCode!!)
- if (!siteverifyResponse.success) {
- throw InvalidCaptchaCodeException()
- }
- } catch (e: Exception) {
- throw InvalidCaptchaCodeException()
- }
-
- return this.login(request, loginParam.account!!, loginParam.password!!)
- }
-
private fun login(request: HttpServletRequest, account: String, password: String): LoginVo {
val usernamePasswordAuthenticationToken =
UsernamePasswordAuthenticationToken(account, password)
@@ -292,29 +332,14 @@ class AuthenticationServiceImpl(
return LoginVo(jwt, loginUser.user.id, loginUser.user.currentLoginTime, loginUser.user.currentLoginIp)
}
- @EventLogRecord(EventLog.Event.LOGOUT)
- override fun logout(token: String): Boolean {
- val loginUser = WebUtil.getLoginUser() ?: let { throw TokenHasExpiredException() }
-
- return redisUtil.delObject("${SecurityProperties.jwtIssuer}_login_${loginUser.user.id}:" + token)
- }
-
- override fun renewToken(token: String): TokenVo {
- val loginUser = WebUtil.getLoginUser() ?: let { throw TokenHasExpiredException() }
-
- val oldRedisKey = "${SecurityProperties.jwtIssuer}_login_${loginUser.user.id}:" + token
- redisUtil.delObject(oldRedisKey)
- val jwt = JwtUtil.createJwt(WebUtil.getLoginUserId().toString())
-
- jwt ?: let {
- throw RuntimeException("Login failed")
+ private fun verifyCaptcha(captchaCode: String) {
+ try {
+ val siteverifyResponse = turnstileApi.siteverify(captchaCode)
+ if (!siteverifyResponse.success) {
+ throw InvalidCaptchaCodeException()
+ }
+ } catch (e: Exception) {
+ throw InvalidCaptchaCodeException()
}
-
- val redisKey = "${SecurityProperties.jwtIssuer}_login_${loginUser.user.id}:" + jwt
- redisUtil.setObject(
- redisKey, loginUser, SecurityProperties.redisTtl, SecurityProperties.redisTtlUnit
- )
-
- return TokenVo(jwt)
}
}
\ 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 6e4642e..5488731 100644
--- a/src/main/resources/mapper/permission/UserMapper.xml
+++ b/src/main/resources/mapper/permission/UserMapper.xml
@@ -6,6 +6,7 @@
t_user.username as user_username,
t_user.password as user_password,
t_user.verify as user_verify,
+ t_user.forget as user_forget,
t_user.locking as user_locking,
t_user.expiration as user_expiration,
t_user.credentials_expiration as user_credentials_expiration,
@@ -128,6 +129,7 @@
select t_user.id as user_id,
t_user.username as user_username,
t_user.verify as user_verify,
+ t_user.forget as user_forget,
t_user.locking as user_locking,
t_user.expiration as user_expiration,
t_user.credentials_expiration as user_credentials_expiration,
@@ -179,6 +181,7 @@
select t_user.id as user_id,
t_user.username as user_username,
t_user.verify as user_verify,
+ t_user.forget as user_forget,
t_user.locking as user_locking,
t_user.expiration as user_expiration,
t_user.credentials_expiration as user_credentials_expiration,
@@ -227,6 +230,7 @@
t_user.username as user_username,
t_user.verify as user_verify,
t_user.locking as user_locking,
+ t_user.forget as user_forget,
t_user.expiration as user_expiration,
t_user.credentials_expiration as user_credentials_expiration,
t_user.enable as user_enable,
@@ -284,6 +288,7 @@
+
@@ -299,7 +304,8 @@
-
+
@@ -307,7 +313,8 @@
-
+