Optimize register api. Add verify Turnstile captcha to login api.

This commit is contained in:
2023-12-29 17:55:44 +08:00
parent 94512ccd2b
commit 22055faca4
15 changed files with 189 additions and 8 deletions

View File

@@ -0,0 +1,28 @@
package top.fatweb.oxygen.api.config
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import retrofit2.converter.jackson.JacksonConverterFactory
/**
* Jackson configuration
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Configuration
class JacksonConfig {
@Bean
fun jacksonConverterFactory(): JacksonConverterFactory {
val mapper = JsonMapper.builder()
.findAndAddModules()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.serializationInclusion(JsonInclude.Include.NON_NULL)
.build()
return JacksonConverterFactory.create(mapper)
}
}

View File

@@ -40,9 +40,12 @@ class AuthenticationController(
*/
@Operation(summary = "注册")
@PostMapping("/register")
fun register(@Valid @RequestBody registerParam: RegisterParam): ResponseResult<RegisterVo> = ResponseResult.success(
fun register(
request: HttpServletRequest,
@Valid @RequestBody registerParam: RegisterParam
): ResponseResult<RegisterVo> = ResponseResult.success(
ResponseCode.PERMISSION_REGISTER_SUCCESS,
data = authenticationService.register(registerParam)
data = authenticationService.register(request, registerParam)
)

View File

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

View File

@@ -0,0 +1,10 @@
package top.fatweb.oxygen.api.exception
/**
* Invalid captcha code exception
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
* @see RuntimeException
*/
class InvalidCaptchaCodeException : RuntimeException("Invalid captcha code")

View File

@@ -151,6 +151,11 @@ class ExceptionHandler {
ResponseResult.fail(ResponseCode.PERMISSION_ACCOUNT_NEED_RESET_PASSWORD, e.localizedMessage, null)
}
is InvalidCaptchaCodeException -> {
logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.SYSTEM_INVALID_CAPTCHA_CODE, e.localizedMessage, null)
}
is BadSqlGrammarException -> {
logger.debug(e.localizedMessage, e)

View File

@@ -0,0 +1,33 @@
package top.fatweb.oxygen.api.http
import com.github.lianjiatech.retrofit.spring.boot.core.RetrofitClient
import org.springframework.stereotype.Service
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
import top.fatweb.oxygen.api.http.entity.turnstile.SiteverifyResponse
import top.fatweb.oxygen.api.properties.ServerProperties
/**
* Turnstile http request api
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Service
@RetrofitClient(baseUrl = "https://challenges.cloudflare.com/turnstile/v0/")
interface TurnstileApi {
/**
* Turnstile post verify captcha code
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
* @see SiteverifyResponse
*/
@FormUrlEncoded
@POST("siteverify")
fun siteverify(
@Field("response") captchaCode: String,
@Field("secret") secret: String = ServerProperties.turnstileSecretKey
): SiteverifyResponse
}

View File

@@ -0,0 +1,48 @@
package top.fatweb.oxygen.api.http.entity.turnstile
import com.fasterxml.jackson.annotation.JsonProperty
import java.time.LocalDateTime
/**
* Turnstile verify captcha code response
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
data class SiteverifyResponse(
/**
* Is success
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@JsonProperty("success")
val success: Boolean,
/**
* Challenge time
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@JsonProperty("challenge_ts")
val challengeTs: LocalDateTime?,
/**
* Hostname
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@JsonProperty("hostname")
val hostname: String?,
/**
* Error codes list
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@JsonProperty("error-codes")
val errorCodes: List<String>?
)

View File

@@ -29,5 +29,15 @@ data class LoginParam(
*/
@Schema(description = "密码", required = true)
@field:NotBlank(message = "Password can not be blank")
val password: String?
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?
)

View File

@@ -48,6 +48,14 @@ object ServerProperties {
*/
val startupTime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
/**
* Turnstile secret key
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
lateinit var turnstileSecretKey: String
fun buildZoneDateTime(zoneId: ZoneId = ZoneId.systemDefault()): ZonedDateTime =
LocalDateTime.parse(buildTime).atZone(ZoneId.of("UTC")).withZoneSameInstant(zoneId)
}

View File

@@ -24,7 +24,7 @@ interface IAuthenticationService {
* @see RegisterParam
* @see RegisterVo
*/
fun register(registerParam: RegisterParam): RegisterVo
fun register(request: HttpServletRequest, registerParam: RegisterParam): RegisterVo
/**
* Send verify email

View File

@@ -19,6 +19,7 @@ import top.fatweb.oxygen.api.entity.permission.User
import top.fatweb.oxygen.api.entity.permission.UserInfo
import top.fatweb.oxygen.api.entity.system.EventLog
import top.fatweb.oxygen.api.exception.*
import top.fatweb.oxygen.api.http.TurnstileApi
import top.fatweb.oxygen.api.param.permission.*
import top.fatweb.oxygen.api.properties.SecurityProperties
import top.fatweb.oxygen.api.service.api.v1.IAvatarService
@@ -56,6 +57,7 @@ class AuthenticationServiceImpl(
private val authenticationManager: AuthenticationManager,
private val passwordEncoder: PasswordEncoder,
private val redisUtil: RedisUtil,
private val turnstileApi: TurnstileApi,
private val userService: IUserService,
private val userInfoService: IUserInfoService,
private val avatarService: IAvatarService
@@ -64,7 +66,7 @@ class AuthenticationServiceImpl(
@EventLogRecord(EventLog.Event.REGISTER)
@Transactional
override fun register(registerParam: RegisterParam): RegisterVo {
override fun register(request: HttpServletRequest, registerParam: RegisterParam): RegisterVo {
val user = User().apply {
username = registerParam.username
password = passwordEncoder.encode(registerParam.password)
@@ -85,7 +87,9 @@ class AuthenticationServiceImpl(
sendVerifyMail(user.username!!, user.verify!!, registerParam.email!!)
return RegisterVo(userId = user.id)
val loginVo = this.login(request, registerParam.username!!, registerParam.password!!)
return RegisterVo(token = loginVo.token, userId = loginVo.userId)
}
@Transactional
@@ -244,8 +248,21 @@ 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(loginParam.account, loginParam.password)
UsernamePasswordAuthenticationToken(account, password)
val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken)
authentication ?: let {
throw RuntimeException("Login failed")

View File

@@ -12,6 +12,17 @@ import io.swagger.v3.oas.annotations.media.Schema
*/
@Schema(description = "注册返回参数")
data class RegisterVo(
/**
* Token
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(
description = "Token",
example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkYTllYjFkYmVmZDQ0OWRkOThlOGNjNzZlNzZkMDgyNSIsInN1YiI6IjE3MDk5ODYwNTg2Nzk5NzU5MzgiLCJpc3MiOiJGYXRXZWIiLCJpYXQiOjE2OTY1MjgxMTcsImV4cCI6MTY5NjUzNTMxN30.U2ZsyrGk7NbsP-DJfdz9xgWSfect5r2iKQnlEsscAA8"
) val token: String,
/**
* User ID
*