Optimize authentication api
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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?
|
||||
)
|
||||
val password: String?
|
||||
) : CaptchaCodeParam()
|
||||
@@ -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?
|
||||
)
|
||||
) : CaptchaCodeParam()
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user