Add captcha module (#49)
* Add captcha module * Rename siteverify -> siteVerify
This commit is contained in:
parent
7313b43dff
commit
dfa3a75f4c
|
|
@ -0,0 +1,15 @@
|
|||
plugins {
|
||||
id("kotlin")
|
||||
id("kotlin-spring")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation(project(":common-spring-web"))
|
||||
|
||||
implementation(project(":logger-spring"))
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||
implementation("org.springframework.boot:spring-boot-starter-aop")
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.captcha.annotations
|
||||
|
||||
import ru.touchin.captcha.dto.enums.CaptchaScore
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
annotation class Captcha(
|
||||
val action: String,
|
||||
val minScore: CaptchaScore = CaptchaScore.AVERAGE
|
||||
)
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package ru.touchin.captcha.aspects
|
||||
|
||||
import org.aspectj.lang.annotation.Aspect
|
||||
import org.aspectj.lang.annotation.Before
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.context.request.RequestContextHolder
|
||||
import org.springframework.web.context.request.ServletRequestAttributes
|
||||
import ru.touchin.captcha.annotations.Captcha
|
||||
import ru.touchin.captcha.exceptions.CaptchaResponseMissingException
|
||||
import ru.touchin.captcha.services.CaptchaService
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
class CaptchaSiteVerifyAspect(private val captchaService: CaptchaService) {
|
||||
|
||||
@Throws(Throwable::class)
|
||||
@Before("@annotation(captcha)")
|
||||
fun captchaSiteVerify(captcha: Captcha) {
|
||||
val currentRequestAttributes = RequestContextHolder.currentRequestAttributes()
|
||||
as? ServletRequestAttributes
|
||||
?: throw IllegalStateException("unable to get current request attributes")
|
||||
|
||||
val captchaResponse = currentRequestAttributes.request.getHeader(CAPTCHA_HEADER_NAME)
|
||||
?: throw CaptchaResponseMissingException()
|
||||
|
||||
captchaService.verify(captchaResponse)
|
||||
.validateAction(captcha.action)
|
||||
.validateScore(captcha.minScore.value)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val CAPTCHA_HEADER_NAME = "X-Captcha-Response"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package ru.touchin.captcha.dto
|
||||
|
||||
import ru.touchin.captcha.exceptions.CaptchaActionMismatchException
|
||||
import ru.touchin.captcha.exceptions.CaptchaScoreBelowMinimumException
|
||||
|
||||
data class CaptchaVerificationResult(val score: Double, val action: String) {
|
||||
|
||||
fun validateScore(minScore: Double): CaptchaVerificationResult {
|
||||
if (score < minScore) {
|
||||
throw CaptchaScoreBelowMinimumException(score)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
fun validateAction(actionToValidate: String): CaptchaVerificationResult {
|
||||
if (action != actionToValidate) {
|
||||
throw CaptchaActionMismatchException(actionToValidate)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.captcha.dto.enums
|
||||
|
||||
enum class CaptchaScore(val value: Double) {
|
||||
|
||||
WEAK(0.2),
|
||||
AVERAGE(0.5),
|
||||
STRONG(0.8)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.captcha.dto.response
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
data class CaptchaSiteVerifyResponse(
|
||||
val score: Double,
|
||||
val action: String,
|
||||
val success: Boolean,
|
||||
@JsonProperty("challenge_ts")
|
||||
val challengeTs: ZonedDateTime
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.captcha.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class CaptchaActionMismatchException(
|
||||
action: String
|
||||
) : CommonException("invalid captcha action $action")
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.captcha.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class CaptchaResponseMissingException : CommonException("missing captcha response header")
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.captcha.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class CaptchaScoreBelowMinimumException(
|
||||
score: Double,
|
||||
) : CommonException("captcha score below minimum $score")
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.captcha.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class CaptchaSiteVerifyFailureException(
|
||||
description: String?
|
||||
) : CommonException(description)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.captcha.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class CaptchaUnknownActionException(
|
||||
action: String
|
||||
) : CommonException("unknown captcha action $action")
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package ru.touchin.captcha.properties
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.ConstructorBinding
|
||||
import java.net.URI
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties(prefix = "captcha")
|
||||
data class CaptchaProperties(
|
||||
val uri: URI,
|
||||
val secret: String,
|
||||
val actions: List<String>
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.captcha.services
|
||||
|
||||
import ru.touchin.captcha.dto.CaptchaVerificationResult
|
||||
|
||||
interface CaptchaService {
|
||||
|
||||
fun verify(response: String): CaptchaVerificationResult
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package ru.touchin.captcha.services.impl
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import ru.touchin.captcha.dto.CaptchaVerificationResult
|
||||
import ru.touchin.captcha.exceptions.CaptchaUnknownActionException
|
||||
import ru.touchin.captcha.properties.CaptchaProperties
|
||||
import ru.touchin.captcha.services.CaptchaService
|
||||
import ru.touchin.captcha.webclients.CaptchaWebClient
|
||||
import ru.touchin.logger.spring.annotations.AutoLogging
|
||||
import ru.touchin.logger.spring.annotations.LogValue
|
||||
|
||||
@Service
|
||||
class CaptchaServiceImpl(
|
||||
private val captchaWebClient: CaptchaWebClient,
|
||||
private val captchaProperties: CaptchaProperties,
|
||||
) : CaptchaService {
|
||||
|
||||
@LogValue
|
||||
@AutoLogging(tags = ["CAPTCHA", "CAPTCHA_VERIFICATION"])
|
||||
override fun verify(response: String): CaptchaVerificationResult {
|
||||
val siteVerifyResponse = captchaWebClient.siteVerify(response)
|
||||
|
||||
if (siteVerifyResponse.action !in captchaProperties.actions) {
|
||||
throw CaptchaUnknownActionException(siteVerifyResponse.action)
|
||||
}
|
||||
|
||||
return CaptchaVerificationResult(
|
||||
score = siteVerifyResponse.score,
|
||||
action = siteVerifyResponse.action,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package ru.touchin.captcha.webclients
|
||||
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import ru.touchin.captcha.properties.CaptchaProperties
|
||||
import ru.touchin.captcha.dto.response.CaptchaSiteVerifyResponse
|
||||
import ru.touchin.captcha.exceptions.CaptchaSiteVerifyFailureException
|
||||
import ru.touchin.common.spring.web.webclient.BaseLogWebClient
|
||||
import ru.touchin.common.spring.web.webclient.dto.RequestLogData
|
||||
import ru.touchin.common.spring.web.webclient.logger.WebClientLogger
|
||||
|
||||
private const val SITE_VERIFY_PATH = "/recaptcha/api/siteverify"
|
||||
|
||||
@Component
|
||||
class CaptchaWebClient(
|
||||
webClientLogger: WebClientLogger,
|
||||
webClientBuilder: WebClient.Builder,
|
||||
private val captchaProperties: CaptchaProperties,
|
||||
) : BaseLogWebClient(webClientLogger, webClientBuilder) {
|
||||
|
||||
override fun getWebClient(): WebClient {
|
||||
return getWebClientBuilder(captchaProperties.uri.toString()).build()
|
||||
}
|
||||
|
||||
fun siteVerify(response: String): CaptchaSiteVerifyResponse {
|
||||
return getWebClient().post()
|
||||
.uri {
|
||||
it.path(SITE_VERIFY_PATH)
|
||||
it.queryParam("response", response)
|
||||
it.queryParam("secret", captchaProperties.secret)
|
||||
|
||||
it.build()
|
||||
}
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange(
|
||||
clazz = CaptchaSiteVerifyResponse::class.java,
|
||||
requestLogData = RequestLogData(
|
||||
uri = SITE_VERIFY_PATH,
|
||||
logTags = listOf("CAPTCHA_SITEVERIFY"),
|
||||
method = HttpMethod.POST,
|
||||
)
|
||||
)
|
||||
.block()!!
|
||||
.also(this::validateSuccess)
|
||||
}
|
||||
|
||||
private fun validateSuccess(captchaSiteVerifyResponse: CaptchaSiteVerifyResponse) {
|
||||
if (!captchaSiteVerifyResponse.success) {
|
||||
throw CaptchaSiteVerifyFailureException("Captcha siteverify request did not succeed")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ include("common-measure")
|
|||
include("common-measure-spring")
|
||||
include("common-geo")
|
||||
include("common-geo-spatial4j-spring")
|
||||
include("captcha")
|
||||
include("logger")
|
||||
include("logger-spring")
|
||||
include("logger-spring-web")
|
||||
|
|
|
|||
Loading…
Reference in New Issue