Registro no Spring - Integrar reCAPTCHA
1. Visão geral
Neste artigo, continuaremos a sérieSpring Security Registration adicionandoGooglereCAPTCHA ao processo de registro para diferenciar humanos de bots.
2. Integração do reCAPTCHA do Google
Para integrar o serviço da web reCAPTCHA do Google, primeiro precisamos registrar nosso site com o serviço, adicionar sua biblioteca à nossa página e, em seguida, verificar a resposta do captcha do usuário com o serviço da web.
Vamos registrar nosso site emhttps://www.google.com/recaptcha/admin. O processo de registro gerasite-keyesecret-key para acessar o serviço da web.
2.1. Armazenamento do par de chaves API
Armazenamos as chaves emapplication.properties:
google.recaptcha.key.site=6LfaHiITAAAA...
google.recaptcha.key.secret=6LfaHiITAAAA...
E exponha-os ao Spring usando um bean anotado com@ConfigurationProperties:
@Component
@ConfigurationProperties(prefix = "google.recaptcha.key")
public class CaptchaSettings {
private String site;
private String secret;
// standard getters and setters
}
2.2. Exibindo o widget
Com base no tutorial da série, agora modificaremosregistration.html para incluir a biblioteca do Google.
Dentro de nosso formulário de registro, adicionamos o widget reCAPTCHA que espera que o atributodata-sitekey contenhasite-key.
O widget irá anexarthe request parameter g-recaptcha-response when submitted:
...
...
3. Validação do lado do servidor
O novo parâmetro de solicitação codifica a chave do nosso site e uma string única que identifica a conclusão bem-sucedida do desafio pelo usuário.
No entanto, como não podemos discernir isso, não podemos confiar no que o usuário enviou é legítimo. Uma solicitação do lado do servidor é feita para validarcaptcha response com a API de serviço da web.
O endpoint aceita uma solicitação HTTP no URLhttps://www.google.com/recaptcha/api/siteverify, com os parâmetros de consultasecret,response eremoteip. Ele retorna uma resposta json com o esquema:
{
"success": true|false,
"challenge_ts": timestamp,
"hostname": string,
"error-codes": [ ... ]
}
3.1. Recuperar a resposta do usuário
A resposta do usuário ao desafio reCAPTCHA é recuperada do parâmetro de solicitaçãog-recaptcha-response usandoHttpServletRequeste validada com nossoCaptchaService. Qualquer exceção lançada durante o processamento da resposta interromperá o restante da lógica de registro:
public class RegistrationController {
@Autowired
private ICaptchaService captchaService;
...
@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
@ResponseBody
public GenericResponse registerUserAccount(@Valid UserDto accountDto, HttpServletRequest request) {
String response = request.getParameter("g-recaptcha-response");
captchaService.processResponse(response);
// Rest of implementation
}
...
}
3.2. Serviço de Validação
A resposta captcha obtida deve ser higienizada primeiro. Uma expressão regular simples é usada.
Se a resposta parecer legítima, fazemos uma solicitação ao serviço da web comsecret-key,captcha response eIP address do cliente:
public class CaptchaService implements ICaptchaService {
@Autowired
private CaptchaSettings captchaSettings;
@Autowired
private RestOperations restTemplate;
private static Pattern RESPONSE_PATTERN = Pattern.compile("[A-Za-z0-9_-]+");
@Override
public void processResponse(String response) {
if(!responseSanityCheck(response)) {
throw new InvalidReCaptchaException("Response contains invalid characters");
}
URI verifyUri = URI.create(String.format(
"https://www.google.com/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s",
getReCaptchaSecret(), response, getClientIP()));
GoogleResponse googleResponse = restTemplate.getForObject(verifyUri, GoogleResponse.class);
if(!googleResponse.isSuccess()) {
throw new ReCaptchaInvalidException("reCaptcha was not successfully validated");
}
}
private boolean responseSanityCheck(String response) {
return StringUtils.hasLength(response) && RESPONSE_PATTERN.matcher(response).matches();
}
}
3.3. Objetivando a Validação
Um Java-bean decorado com anotaçõesJackson encapsula a resposta de validação:
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonPropertyOrder({
"success",
"challenge_ts",
"hostname",
"error-codes"
})
public class GoogleResponse {
@JsonProperty("success")
private boolean success;
@JsonProperty("challenge_ts")
private String challengeTs;
@JsonProperty("hostname")
private String hostname;
@JsonProperty("error-codes")
private ErrorCode[] errorCodes;
@JsonIgnore
public boolean hasClientError() {
ErrorCode[] errors = getErrorCodes();
if(errors == null) {
return false;
}
for(ErrorCode error : errors) {
switch(error) {
case InvalidResponse:
case MissingResponse:
return true;
}
}
return false;
}
static enum ErrorCode {
MissingSecret, InvalidSecret,
MissingResponse, InvalidResponse;
private static Map errorsMap = new HashMap(4);
static {
errorsMap.put("missing-input-secret", MissingSecret);
errorsMap.put("invalid-input-secret", InvalidSecret);
errorsMap.put("missing-input-response", MissingResponse);
errorsMap.put("invalid-input-response", InvalidResponse);
}
@JsonCreator
public static ErrorCode forValue(String value) {
return errorsMap.get(value.toLowerCase());
}
}
// standard getters and setters
}
Como implícito, um valor verdadeiro na propriedadesuccess significa que o usuário foi validado. Caso contrário, a propriedadeerrorCodes será preenchida com o motivo.
Ohostname se refere ao servidor que redirecionou o usuário para o reCAPTCHA. Se você gerencia muitos domínios e deseja que todos compartilhem o mesmo par de chaves, pode optar por verificar a propriedadehostname você mesmo.
3.4. Falha de validação
No caso de uma falha na validação, uma exceção é lançada. A biblioteca reCAPTCHA precisa instruir o cliente a criar um novo desafio.
Fazemos isso no gerenciador de erros de registro do cliente, invocando reset no widgetgrecaptcha da biblioteca:
register(event){
event.preventDefault();
var formData= $('form').serialize();
$.post(serverContext + "user/registration", formData, function(data){
if(data.message == "success") {
// success handler
}
})
.fail(function(data) {
grecaptcha.reset();
...
if(data.responseJSON.error == "InvalidReCaptcha"){
$("#captchaError").show().html(data.responseJSON.message);
}
...
}
}
4. Protegendo os recursos do servidor
Clientes maliciosos não precisam obedecer às regras da caixa de proteção do navegador. Portanto, nossa mentalidade de segurança deve estar nos recursos expostos e como eles podem ser abusados.
4.1. Cache de tentativas
É importante entender que, ao integrar o reCAPTCHA, cada solicitação feita fará com que o servidor crie um soquete para validar a solicitação.
Embora uma abordagem mais estratificada seja necessária para uma verdadeira mitigação de DoS; Podemos implementar um cache elementar que restringe um cliente a 4 respostas de captcha com falha:
public class ReCaptchaAttemptService {
private int MAX_ATTEMPT = 4;
private LoadingCache attemptsCache;
public ReCaptchaAttemptService() {
super();
attemptsCache = CacheBuilder.newBuilder()
.expireAfterWrite(4, TimeUnit.HOURS).build(new CacheLoader() {
@Override
public Integer load(String key) {
return 0;
}
});
}
public void reCaptchaSucceeded(String key) {
attemptsCache.invalidate(key);
}
public void reCaptchaFailed(String key) {
int attempts = attemptsCache.getUnchecked(key);
attempts++;
attemptsCache.put(key, attempts);
}
public boolean isBlocked(String key) {
return attemptsCache.getUnchecked(key) >= MAX_ATTEMPT;
}
}
4.2. Refatorando o serviço de validação
O cache é incorporado primeiro pela interrupção se o cliente excedeu o limite de tentativas. Caso contrário, ao processar umGoogleResponse malsucedido, registramos as tentativas que contêm um erro com a resposta do cliente. A validação bem-sucedida limpa o cache de tentativas:
public class CaptchaService implements ICaptchaService {
@Autowired
private ReCaptchaAttemptService reCaptchaAttemptService;
...
@Override
public void processResponse(String response) {
...
if(reCaptchaAttemptService.isBlocked(getClientIP())) {
throw new InvalidReCaptchaException("Client exceeded maximum number of failed attempts");
}
...
GoogleResponse googleResponse = ...
if(!googleResponse.isSuccess()) {
if(googleResponse.hasClientError()) {
reCaptchaAttemptService.reCaptchaFailed(getClientIP());
}
throw new ReCaptchaInvalidException("reCaptcha was not successfully validated");
}
reCaptchaAttemptService.reCaptchaSucceeded(getClientIP());
}
}
5. Conclusão
Neste artigo, integramos a biblioteca reCAPTCHA do Google em nossa página de registro e implementamos um serviço para verificar a resposta do captcha com uma solicitação do lado do servidor.
A implementação completa deste tutorial está disponível emgithub project - este é um projeto baseado em Maven, portanto, deve ser fácil de importar e executar como está.