diff --git a/pom.xml b/pom.xml
index 7bdce6c..133a5d1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -121,6 +121,11 @@
spring-security-test
test
+
+ com.github.lianjiatech
+ retrofit-spring-boot-starter
+ 3.0.3
+
org.jetbrains.kotlin
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/config/JacksonConfig.kt b/src/main/kotlin/top/fatweb/oxygen/api/config/JacksonConfig.kt
new file mode 100644
index 0000000..b3518e0
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/config/JacksonConfig.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/AuthenticationController.kt b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/AuthenticationController.kt
index 4c47343..d443136 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/AuthenticationController.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/controller/permission/AuthenticationController.kt
@@ -40,9 +40,12 @@ class AuthenticationController(
*/
@Operation(summary = "注册")
@PostMapping("/register")
- fun register(@Valid @RequestBody registerParam: RegisterParam): ResponseResult = ResponseResult.success(
+ fun register(
+ request: HttpServletRequest,
+ @Valid @RequestBody registerParam: RegisterParam
+ ): ResponseResult = ResponseResult.success(
ResponseCode.PERMISSION_REGISTER_SUCCESS,
- data = authenticationService.register(registerParam)
+ data = authenticationService.register(request, registerParam)
)
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 5b6b721..d2b1dd3 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
@@ -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),
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/exception/InvalidCaptchaCodeException.kt b/src/main/kotlin/top/fatweb/oxygen/api/exception/InvalidCaptchaCodeException.kt
new file mode 100644
index 0000000..6e2b6a5
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/exception/InvalidCaptchaCodeException.kt
@@ -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")
\ 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 47d2262..333724e 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/handler/ExceptionHandler.kt
@@ -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)
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/http/TurnstileApi.kt b/src/main/kotlin/top/fatweb/oxygen/api/http/TurnstileApi.kt
new file mode 100644
index 0000000..7f28258
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/http/TurnstileApi.kt
@@ -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
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/http/entity/turnstile/SiteverifyResponse.kt b/src/main/kotlin/top/fatweb/oxygen/api/http/entity/turnstile/SiteverifyResponse.kt
new file mode 100644
index 0000000..e6e6b94
--- /dev/null
+++ b/src/main/kotlin/top/fatweb/oxygen/api/http/entity/turnstile/SiteverifyResponse.kt
@@ -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?
+)
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 2c0da76..4f8b391 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
@@ -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?
)
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/properties/ServerProperties.kt b/src/main/kotlin/top/fatweb/oxygen/api/properties/ServerProperties.kt
index 4912682..83b2835 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/properties/ServerProperties.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/properties/ServerProperties.kt
@@ -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)
}
\ No newline at end of file
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/service/permission/IAuthenticationService.kt b/src/main/kotlin/top/fatweb/oxygen/api/service/permission/IAuthenticationService.kt
index 557a8b8..a242964 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/service/permission/IAuthenticationService.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/service/permission/IAuthenticationService.kt
@@ -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
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 2bb8916..95949d9 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
@@ -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")
diff --git a/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/RegisterVo.kt b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/RegisterVo.kt
index c33581b..b15ca14 100644
--- a/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/RegisterVo.kt
+++ b/src/main/kotlin/top/fatweb/oxygen/api/vo/permission/RegisterVo.kt
@@ -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
*
diff --git a/src/main/resources/application-config-template.yml b/src/main/resources/application-config-template.yml
index 2ca9240..dbe25d5 100644
--- a/src/main/resources/application-config-template.yml
+++ b/src/main/resources/application-config-template.yml
@@ -1,4 +1,6 @@
app:
+ app-name: Oxygen Toolbox # Application name
+ turnstile-secret-key: 1x0000000000000000000000000000000AA # Turnstile secret key
admin:
# username: admin # Username of administrator
# password: admin # Default password of administrator
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index b8d5746..46ec570 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,5 +1,5 @@
app:
- app-name: FatWeb
+ app-name: Oxygen Toolbox
version: @project.version@
build-time: @build.timestamp@