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