Optimize authentication api

This commit is contained in:
2024-01-02 16:46:36 +08:00
parent 22055faca4
commit 21e9bd98c9
10 changed files with 171 additions and 108 deletions

View File

@@ -14,6 +14,7 @@ enum class ResponseCode(val code: Int) {
SYSTEM_REQUEST_ILLEGAL(BusinessCode.SYSTEM, 52), SYSTEM_REQUEST_ILLEGAL(BusinessCode.SYSTEM, 52),
SYSTEM_ARGUMENT_NOT_VALID(BusinessCode.SYSTEM, 53), SYSTEM_ARGUMENT_NOT_VALID(BusinessCode.SYSTEM, 53),
SYSTEM_INVALID_CAPTCHA_CODE(BusinessCode.SYSTEM, 54), SYSTEM_INVALID_CAPTCHA_CODE(BusinessCode.SYSTEM, 54),
SYSTEM_REQUEST_TOO_FREQUENT(BusinessCode.SYSTEM, 55),
PERMISSION_LOGIN_SUCCESS(BusinessCode.PERMISSION, 0), PERMISSION_LOGIN_SUCCESS(BusinessCode.PERMISSION, 0),
PERMISSION_PASSWORD_CHANGE_SUCCESS(BusinessCode.PERMISSION, 1), PERMISSION_PASSWORD_CHANGE_SUCCESS(BusinessCode.PERMISSION, 1),

View File

@@ -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")

View File

@@ -58,6 +58,11 @@ class ExceptionHandler {
ResponseResult.fail(ResponseCode.SYSTEM_ARGUMENT_NOT_VALID, errorMessage, null) 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 -> { is InsufficientAuthenticationException -> {
logger.debug(e.localizedMessage, e) logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.PERMISSION_UNAUTHORIZED, e.localizedMessage, null) ResponseResult.fail(ResponseCode.PERMISSION_UNAUTHORIZED, e.localizedMessage, null)

View File

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

View File

@@ -3,6 +3,7 @@ package top.fatweb.oxygen.api.param.permission
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern import jakarta.validation.constraints.Pattern
import top.fatweb.oxygen.api.param.CaptchaCodeParam
/** /**
* Forget password parameters * Forget password parameters
@@ -22,4 +23,4 @@ data class ForgetParam(
@field:NotBlank(message = "Email can not be blank") @field:NotBlank(message = "Email can not be blank")
@field:Pattern(regexp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*\$", message = "Illegal email address") @field:Pattern(regexp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*\$", message = "Illegal email address")
val email: String? val email: String?
) ) : CaptchaCodeParam()

View File

@@ -2,6 +2,7 @@ package top.fatweb.oxygen.api.param.permission
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotBlank
import top.fatweb.oxygen.api.param.CaptchaCodeParam
/** /**
* Login parameters * Login parameters
@@ -29,15 +30,5 @@ data class LoginParam(
*/ */
@Schema(description = "密码", required = true) @Schema(description = "密码", required = true)
@field:NotBlank(message = "Password can not be blank") @field:NotBlank(message = "Password can not be blank")
val password: String?, val password: String?
) : 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")
val captchaCode: String?
)

View File

@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Size import jakarta.validation.constraints.Size
import top.fatweb.oxygen.api.param.CaptchaCodeParam
/** /**
* Register parameters * Register parameters
@@ -45,4 +46,4 @@ data class RegisterParam(
@field:NotBlank(message = "Password can not be blank") @field:NotBlank(message = "Password can not be blank")
@field:Size(min = 10, max = 30) @field:Size(min = 10, max = 30)
val password: String? val password: String?
) ) : CaptchaCodeParam()

View File

@@ -3,6 +3,7 @@ package top.fatweb.oxygen.api.param.permission
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size import jakarta.validation.constraints.Size
import top.fatweb.oxygen.api.param.CaptchaCodeParam
/** /**
* Retrieve password parameters * Retrieve password parameters
@@ -32,4 +33,4 @@ data class RetrieveParam(
@field:NotBlank(message = "New password can not be blank") @field:NotBlank(message = "New password can not be blank")
@field:Size(min = 10, max = 30) @field:Size(min = 10, max = 30)
val password: String? val password: String?
) ) : CaptchaCodeParam()

View File

@@ -67,6 +67,8 @@ class AuthenticationServiceImpl(
@EventLogRecord(EventLog.Event.REGISTER) @EventLogRecord(EventLog.Event.REGISTER)
@Transactional @Transactional
override fun register(request: HttpServletRequest, registerParam: RegisterParam): RegisterVo { override fun register(request: HttpServletRequest, registerParam: RegisterParam): RegisterVo {
verifyCaptcha(registerParam.captchaCode!!)
val user = User().apply { val user = User().apply {
username = registerParam.username username = registerParam.username
password = passwordEncoder.encode(registerParam.password) password = passwordEncoder.encode(registerParam.password)
@@ -98,6 +100,12 @@ class AuthenticationServiceImpl(
user.verify ?: throw NoVerificationRequiredException() 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 = user.verify =
"${ "${
LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli() LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()
@@ -110,30 +118,6 @@ class AuthenticationServiceImpl(
} ?: throw AccessDeniedException("Access Denied") } ?: 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) @EventLogRecord(EventLog.Event.VERIFY)
@Transactional @Transactional
override fun verify(verifyParam: VerifyParam) { override fun verify(verifyParam: VerifyParam) {
@@ -161,8 +145,19 @@ class AuthenticationServiceImpl(
@Transactional @Transactional
override fun forget(request: HttpServletRequest, forgetParam: ForgetParam) { override fun forget(request: HttpServletRequest, forgetParam: ForgetParam) {
verifyCaptcha(forgetParam.captchaCode!!)
val user = userService.getUserWithPowerByAccount(forgetParam.email!!) val user = userService.getUserWithPowerByAccount(forgetParam.email!!)
user ?: let { throw UserNotFoundException() } 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 = "${ val code = "${
LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli() LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()
}-${UUID.randomUUID()}-${UUID.randomUUID()}-${UUID.randomUUID()}" }-${UUID.randomUUID()}-${UUID.randomUUID()}-${UUID.randomUUID()}"
@@ -170,6 +165,95 @@ class AuthenticationServiceImpl(
sendRetrieveMail(user.username!!, request.remoteAddr, code, forgetParam.email) 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) { private fun sendRetrieveMail(username: String, ip: String, code: String, email: String) {
val velocityContext = VelocityContext().apply { val velocityContext = VelocityContext().apply {
put("appName", SettingsOperator.getAppValue(BaseSettings::appName, "氧工具")) 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) { private fun sendPasswordChangedMail(username: String, ip: String, email: String) {
val velocityContext = VelocityContext().apply { val velocityContext = VelocityContext().apply {
put("appName", SettingsOperator.getAppValue(BaseSettings::appName, "氧工具")) 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 { private fun login(request: HttpServletRequest, account: String, password: String): LoginVo {
val usernamePasswordAuthenticationToken = val usernamePasswordAuthenticationToken =
UsernamePasswordAuthenticationToken(account, password) UsernamePasswordAuthenticationToken(account, password)
@@ -292,29 +332,14 @@ class AuthenticationServiceImpl(
return LoginVo(jwt, loginUser.user.id, loginUser.user.currentLoginTime, loginUser.user.currentLoginIp) return LoginVo(jwt, loginUser.user.id, loginUser.user.currentLoginTime, loginUser.user.currentLoginIp)
} }
@EventLogRecord(EventLog.Event.LOGOUT) private fun verifyCaptcha(captchaCode: String) {
override fun logout(token: String): Boolean { try {
val loginUser = WebUtil.getLoginUser() ?: let { throw TokenHasExpiredException() } val siteverifyResponse = turnstileApi.siteverify(captchaCode)
if (!siteverifyResponse.success) {
return redisUtil.delObject("${SecurityProperties.jwtIssuer}_login_${loginUser.user.id}:" + token) throw InvalidCaptchaCodeException()
}
} catch (e: Exception) {
throw InvalidCaptchaCodeException()
} }
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)
} }
} }

View File

@@ -6,6 +6,7 @@
t_user.username as user_username, t_user.username as user_username,
t_user.password as user_password, t_user.password as user_password,
t_user.verify as user_verify, t_user.verify as user_verify,
t_user.forget as user_forget,
t_user.locking as user_locking, t_user.locking as user_locking,
t_user.expiration as user_expiration, t_user.expiration as user_expiration,
t_user.credentials_expiration as user_credentials_expiration, t_user.credentials_expiration as user_credentials_expiration,
@@ -128,6 +129,7 @@
select t_user.id as user_id, select t_user.id as user_id,
t_user.username as user_username, t_user.username as user_username,
t_user.verify as user_verify, t_user.verify as user_verify,
t_user.forget as user_forget,
t_user.locking as user_locking, t_user.locking as user_locking,
t_user.expiration as user_expiration, t_user.expiration as user_expiration,
t_user.credentials_expiration as user_credentials_expiration, t_user.credentials_expiration as user_credentials_expiration,
@@ -179,6 +181,7 @@
select t_user.id as user_id, select t_user.id as user_id,
t_user.username as user_username, t_user.username as user_username,
t_user.verify as user_verify, t_user.verify as user_verify,
t_user.forget as user_forget,
t_user.locking as user_locking, t_user.locking as user_locking,
t_user.expiration as user_expiration, t_user.expiration as user_expiration,
t_user.credentials_expiration as user_credentials_expiration, t_user.credentials_expiration as user_credentials_expiration,
@@ -227,6 +230,7 @@
t_user.username as user_username, t_user.username as user_username,
t_user.verify as user_verify, t_user.verify as user_verify,
t_user.locking as user_locking, t_user.locking as user_locking,
t_user.forget as user_forget,
t_user.expiration as user_expiration, t_user.expiration as user_expiration,
t_user.credentials_expiration as user_credentials_expiration, t_user.credentials_expiration as user_credentials_expiration,
t_user.enable as user_enable, t_user.enable as user_enable,
@@ -284,6 +288,7 @@
<id property="id" column="user_id"/> <id property="id" column="user_id"/>
<result property="username" column="user_username"/> <result property="username" column="user_username"/>
<result property="verify" column="user_verify"/> <result property="verify" column="user_verify"/>
<result property="forget" column="user_forget"/>
<result property="locking" column="user_locking"/> <result property="locking" column="user_locking"/>
<result property="expiration" column="user_expiration"/> <result property="expiration" column="user_expiration"/>
<result property="credentialsExpiration" column="user_credentials_expiration"/> <result property="credentialsExpiration" column="user_credentials_expiration"/>
@@ -299,7 +304,8 @@
</resultMap> </resultMap>
<resultMap id="userWithInfoMap" type="user" extends="userBaseMap"> <resultMap id="userWithInfoMap" type="user" extends="userBaseMap">
<association property="userInfo" resultMap="top.fatweb.oxygen.api.mapper.permission.UserInfoMapper.userInfoMap"/> <association property="userInfo"
resultMap="top.fatweb.oxygen.api.mapper.permission.UserInfoMapper.userInfoMap"/>
</resultMap> </resultMap>
<resultMap id="userWithPowerInfoMap" type="user" extends="userWithInfoMap"> <resultMap id="userWithPowerInfoMap" type="user" extends="userWithInfoMap">
@@ -307,7 +313,8 @@
<collection property="modules" resultMap="top.fatweb.oxygen.api.mapper.permission.ModuleMapper.moduleMap"/> <collection property="modules" resultMap="top.fatweb.oxygen.api.mapper.permission.ModuleMapper.moduleMap"/>
<collection property="menus" resultMap="top.fatweb.oxygen.api.mapper.permission.MenuMapper.menuMap"/> <collection property="menus" resultMap="top.fatweb.oxygen.api.mapper.permission.MenuMapper.menuMap"/>
<collection property="funcs" resultMap="top.fatweb.oxygen.api.mapper.permission.FuncMapper.funcMap"/> <collection property="funcs" resultMap="top.fatweb.oxygen.api.mapper.permission.FuncMapper.funcMap"/>
<collection property="operations" resultMap="top.fatweb.oxygen.api.mapper.permission.OperationMapper.operationMap"/> <collection property="operations"
resultMap="top.fatweb.oxygen.api.mapper.permission.OperationMapper.operationMap"/>
</resultMap> </resultMap>
<resultMap id="userWithRoleInfoMap" type="user" extends="userWithInfoMap"> <resultMap id="userWithRoleInfoMap" type="user" extends="userWithInfoMap">