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@