Registration with Spring – Integrate reCAPTCHA – 用Spring注册 – 整合reCAPTCHA

最后修改: 2016年 8月 12日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll continue the Spring Security Registration series by adding Google reCAPTCHA to the registration process in order to differentiate humans from bots.

在本教程中,我们将继续Spring安全注册系列,在注册过程中添加GooglereCAPTCHA,以区分人类和机器人。

2. Integrating Google’s reCAPTCHA

2.整合谷歌的reCAPTCHA

To integrate Google’s reCAPTCHA web service, we first need to register our site with the service, add their library to our page, and then verify the user’s captcha response with the web service.

为了整合谷歌的reCAPTCHA网络服务,我们首先需要在该服务中注册我们的网站,将他们的库添加到我们的页面,然后用网络服务验证用户的验证码响应。

Let’s register our site at https://www.google.com/recaptcha/admin. The registration process generates a site-key and secret-key for accessing the web service.

让我们在https://www.google.com/recaptcha/admin注册我们的网站。注册过程中会产生一个网站密钥保密密钥,用于访问网络服务。

2.1. Storing the API Key-Pair

2.1.存储API密钥对

We store the keys in the application.properties:

我们在application.properties中存储键值:

google.recaptcha.key.site=6LfaHiITAAAA...
google.recaptcha.key.secret=6LfaHiITAAAA...

And expose them to Spring using a bean annotated with @ConfigurationProperties:

并使用带有@ConfigurationProperties注解的bean将它们暴露给Spring:

@Component
@ConfigurationProperties(prefix = "google.recaptcha.key")
public class CaptchaSettings {

    private String site;
    private String secret;

    // standard getters and setters
}

2.2. Displaying the Widget

2.2.显示小工具

Building upon the tutorial from the series, we’ll now modify the registration.html to include Google’s library.

在该系列教程的基础上,我们现在要修改registration.html以包括Google的库。

Inside our registration form, we add the reCAPTCHA widget which expects the attribute data-sitekey to contain the site-key.

在我们的注册表单中,我们添加了reCAPTCHA部件,它期望属性data-sitekey包含site-key

The widget will append the request parameter g-recaptcha-response when submitted:

该小组件在提交时将附加请求参数g-recaptcha-response

<!DOCTYPE html>
<html>
<head>

...

<script src='https://www.google.com/recaptcha/api.js'></script>
</head>
<body>

    ...

    <form method="POST" enctype="utf8">
        ...

        <div class="g-recaptcha col-sm-5"
          th:attr="data-sitekey=${@captchaSettings.getSite()}"></div>
        <span id="captchaError" class="alert alert-danger col-sm-4"
          style="display:none"></span>

3. Server-Side Validation

3.服务器端验证

The new request parameter encodes our site key and a unique string identifying the user’s successful completion of the challenge.

新的请求参数对我们的网站密钥和识别用户成功完成挑战的唯一字符串进行编码。

However, since we cannot discern that ourselves, we cannot trust what the user has submitted is legitimate. A server-side request is made to validate the captcha response with the web-service API.

然而,由于我们自己无法辨别,我们不能相信用户提交的东西是合法的。一个服务器端的请求被用来验证captcha响应与网络服务API。

The endpoint accepts an HTTP request on the URL https://www.google.com/recaptcha/api/siteverify, with the query parameters secret, response, and remoteip. It returns a JSON response having the schema:

该端点接受URL https://www.google.com/recaptcha/api/siteverify上的HTTP请求,查询参数为secret, response, 和remoteip。它返回一个具有模式的JSON响应。

{
    "success": true|false,
    "challenge_ts": timestamp,
    "hostname": string,
    "error-codes": [ ... ]
}

3.1. Retrieve User’s Response

3.1.检索用户的回应

The user’s response to the reCAPTCHA challenge is retrieved from the request parameter g-recaptcha-response using HttpServletRequest and validated with our CaptchaService. Any exception thrown while processing the response will abort the rest of the registration logic:

用户对reCAPTCHA挑战的响应是使用HttpServletRequest从请求参数g-recaptcha-response获取的,并通过我们的CaptchaService验证。在处理响应时抛出的任何异常将中止其余的注册逻辑。

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. Validation Service

3.2.验证服务

The captcha response obtained should be sanitized first. A simple regular expression is used.

获得的验证码响应应首先进行消毒处理。使用一个简单的正则表达式。

If the response looks legitimate, we then make a request to the web service with the secret-key, the captcha response, and the client’s IP address:

如果响应看起来是合法的,我们就用secret-keycaptcha response和客户的IP地址向网络服务发出请求。

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. Objectifying the Validation

3.3.验证的客观化

A Java bean decorated with Jackson annotations encapsulates the validation response:

一个用Jackson注解装饰的Java bean封装了验证响应。

@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<String, ErrorCode> errorsMap = new HashMap<String, ErrorCode>(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
}

As implied, a truth value in the success property means the user has been validated. Otherwise, the errorCodes property will populate with the reason.

正如所暗示的,success属性中的真值意味着用户已经被验证。否则,errorCodes属性将填入原因。

The hostname refers to the server that redirected the user to the reCAPTCHA. If you manage many domains and wish them all to share the same key pair, you can choose to verify the hostname property yourself.

hostname指的是将用户重定向到reCAPTCHA的服务器。如果你管理许多域名,并希望它们都能共享同一个密钥对,你可以选择自己验证hostname属性。

3.4. Validation Failure

3.4.验证失败

In the event of a validation failure, an exception is thrown. The reCAPTCHA library needs to instruct the client to create a new challenge.

在验证失败的情况下,会抛出一个异常。reCAPTCHA库需要指示客户端创建一个新的挑战。

We do so in the client’s registration error handler, by invoking reset on the library’s grecaptcha widget:

我们在客户端的注册错误处理程序中这样做,在库的grecaptcha部件上调用重置。

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. Protecting Server Resources

4.保护服务器资源

Malicious clients do not need to obey the rules of the browser sandbox. So our security mindset should be on the resources exposed and how they might be abused.

恶意客户端不需要遵守浏览器沙盒的规则。因此,我们的安全心态应该放在暴露的资源和它们可能被滥用的情况上。

4.1. Attempts Cache

4.1.尝试缓存

It is important to understand that by integrating reCAPTCHA, every request made will cause the server to create a socket to validate the request.

重要的是要理解,通过整合reCAPTCHA,每一个请求都会导致服务器创建一个套接字来验证请求。

While we’d need a more layered approach for a true DoS mitigation, we can implement an elementary cache that restricts a client to 4 failed captcha responses:

虽然我们需要一个更多层次的方法来实现真正的DoS缓解,但我们可以实现一个基本的缓存,将客户端限制在4个失败的验证码响应。

public class ReCaptchaAttemptService {
    private int MAX_ATTEMPT = 4;
    private LoadingCache<String, Integer> attemptsCache;

    public ReCaptchaAttemptService() {
        super();
        attemptsCache = CacheBuilder.newBuilder()
          .expireAfterWrite(4, TimeUnit.HOURS).build(new CacheLoader<String, Integer>() {
            @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. Refactoring the Validation Service

4.2.重构验证服务

The cache is incorporated first by aborting if the client has exceeded the attempt limit. Otherwise, when processing an unsuccessful GoogleResponse we record the attempts containing an error with the client’s response. Successful validation clears the attempts cache:

如果客户端超过了尝试限制,我们会首先将缓存并入,中止尝试。否则,在处理一个不成功的GoogleResponse时,我们将包含错误的尝试与客户端的响应一起记录下来。成功的验证会清除尝试的缓存。

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. Integrating Google’s reCAPTCHA v3

5.整合谷歌的reCAPTCHA v3

Google’s reCAPTCHA v3 differs from the previous versions because it doesn’t require any user interaction. It simply gives a score for each request that we send and lets us decide what final actions to take for our web application.

谷歌的reCAPTCHA v3与以前的版本不同,因为它不需要任何用户互动。它只是对我们发送的每个请求给出一个分数,让我们决定对我们的网络应用采取什么最终行动。

Again, to integrate Google’s reCAPTCHA 3, we first need to register our site with the service, add their library to our page, and then verify the token response with the web service.

同样,为了整合谷歌的reCAPTCHA 3,我们首先需要在该服务中注册我们的网站,将他们的库添加到我们的页面,然后用网络服务验证令牌响应。

So, let’s register our site at https://www.google.com/recaptcha/admin/create and after selecting reCAPTCHA v3, we’ll obtain the new secret and site keys.

因此,让我们在https://www.google.com/recaptcha/admin/create注册我们的网站,在选择reCAPTCHA v3后,我们将获得新的秘密和网站密钥。

5.1. Updating application.properties and CaptchaSettings

5.1.更新application.propertiesCaptchaSettings

After registering, we need to update application.properties with the new keys and our chosen score threshold value:

注册后,我们需要用新的键和我们选择的分数阈值更新application.properties

google.recaptcha.key.site=6LefKOAUAAAAAE...
google.recaptcha.key.secret=6LefKOAUAAAA...
google.recaptcha.key.threshold=0.5

It’s important to note that the threshold set to 0.5 is a default value and can be tuned over time by analyzing the real threshold values in the Google admin console.

值得注意的是,设置为0.5的阈值是一个默认值,可以通过分析Google管理控制台中的真实阈值来逐步调整。

Next, let’s update our CaptchaSettings class:

接下来,让我们更新我们的CaptchaSettings类。

@Component
@ConfigurationProperties(prefix = "google.recaptcha.key")
public class CaptchaSettings {
    // ... other properties
    private float threshold;
    
    // standard getters and setters
}

5.2. Front-End Integration

5.2.前端整合

We’ll now modify the registration.html to include Google’s library with our site key.

我们现在要修改registration.html,以包括Google的库和我们的网站密钥。

Inside our registration form, we add a hidden field that will store the response token received from the call to the grecaptcha.execute function:

在我们的注册表单中,我们添加了一个隐藏字段,它将存储从调用grecaptcha.execute函数时收到的响应令牌。

<!DOCTYPE html>
<html>
<head>

...

<script th:src='|https://www.google.com/recaptcha/api.js?render=${@captchaService.getReCaptchaSite()}'></script>
</head>
<body>

    ...

    <form method="POST" enctype="utf8">
        ...

        <input type="hidden" id="response" name="response" value="" />
        ...
    </form>
   
   ...

<script th:inline="javascript">
   ...
   var siteKey = /*[[${@captchaService.getReCaptchaSite()}]]*/;
   grecaptcha.execute(siteKey, {action: /*[[${T(com.baeldung.captcha.CaptchaService).REGISTER_ACTION}]]*/}).then(function(response) {
	$('#response').val(response);    
    var formData= $('form').serialize();

5.3. Server-Side Validation

5.3.服务器端验证

We’ll have to make the same server-side request seen in reCAPTCHA Server-Side Validation to validate the response token with the web service API.

我们必须进行在reCAPTCHA服务器端验证中看到的同样的服务器端请求,以便用Web服务API验证响应令牌。

The response JSON object will contain two additional properties:

响应的JSON对象将包含两个额外的属性。

{
    ...
    "score": number,
    "action": string
}

The score is based on the user’s interactions and is a value between 0 (very likely a bot) and 1.0 (very likely a human).

该分数基于用户的互动,是一个介于0(非常可能是机器人)和1.0(非常可能是人类)之间的值。

Action is a new concept that Google introduced so that we can execute many reCAPTCHA requests on the same web page.

行动是谷歌引入的一个新概念,这样我们就可以在同一个网页上执行许多reCAPTCHA请求。

An action must be specified every time we execute the reCAPTCHA v3. And, we have to verify that the value of the action property in the response corresponds to the expected name.

每次我们执行reCAPTCHA v3时都必须指定一个动作。而且,我们必须验证响应中的action属性的值与预期的名称相一致。

5.4. Retrieve the Response Token

5.4.检索响应令牌

The reCAPTCHA v3 response token is retrieved from the response request parameter using HttpServletRequest and validated with our CaptchaService. The mechanism is identical to the one seen above in the reCAPTCHA:

使用HttpServletRequestresponse请求参数中获取reCAPTCHA v3响应标记,并使用我们的CaptchaService进行验证。该机制与在reCAPTCHA中看到的上面的相同。

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("response");
        captchaService.processResponse(response, CaptchaService.REGISTER_ACTION);

        // rest of implementation
    }

    ...
}

5.5. Refactoring the Validation Service With v3

5.5.用v3重构验证服务

The refactored CaptchaService validation service class contains a processResponse method analog to the processResponse method of the previous version, but it takes care to check the action and the score parameters of the GoogleResponse:

重构后的CaptchaService验证服务类包含一个processResponse方法,类似于之前版本的processResponse方法,但是它注意检查actionscore参数的GoogleResponse>。

public class CaptchaService implements ICaptchaService {

    public static final String REGISTER_ACTION = "register";
    ...

    @Override
    public void processResponse(String response, String action) {
        ...
      
        GoogleResponse googleResponse = restTemplate.getForObject(verifyUri, GoogleResponse.class);        
        if(!googleResponse.isSuccess() || !googleResponse.getAction().equals(action) 
            || googleResponse.getScore() < captchaSettings.getThreshold()) {
            ...
            throw new ReCaptchaInvalidException("reCaptcha was not successfully validated");
        }
        reCaptchaAttemptService.reCaptchaSucceeded(getClientIP());
    }
}

In case validation fails, we’ll throw an exception, but note that with v3, there’s no reset method to invoke in the JavaScript client.

如果验证失败,我们将抛出一个异常,但请注意,在v3中,没有reset方法可以在JavaScript客户端调用。

We’ll still have the same implementation seen above for protecting server resources.

我们仍将采用所看到的上面的实现来保护服务器资源。

5.6. Updating the GoogleResponse Class

5.6.更新GoogleResponse

We need to add the new properties score and action to the GoogleResponse Java bean:

我们需要将新的属性scoreaction添加到GoogleResponseJava Bean。

@JsonPropertyOrder({
    "success",
    "score", 
    "action",
    "challenge_ts",
    "hostname",
    "error-codes"
})
public class GoogleResponse {
    // ... other properties
    @JsonProperty("score")
    private float score;
    @JsonProperty("action")
    private String action;
    
    // standard getters and setters
}

6. Conclusion

6.结论

In this article, we integrated Google’s reCAPTCHA library into our registration page and implemented a service to verify the captcha response with a server-side request.

在这篇文章中,我们将谷歌的reCAPTCHA库集成到我们的注册页面中,并实现了一个服务,用服务器端请求来验证验证码的响应。

Later, we upgraded the registration page with Google’s reCAPTCHA v3 library and saw that the registration form becomes leaner because the user doesn’t need to take any action anymore.

后来,我们用谷歌的reCAPTCHA v3库升级了注册页面,看到注册表格变得更精简,因为用户不需要再采取任何行动。

The full implementation of this tutorial is available over on GitHub.

本教程的完整实现可在GitHub上获得