Spring 註冊 – 集成 reCAPTCHA

Spring Security
Remote
1
11:03 PM · Nov 29 ,2025

1. 概述

在本教程中,我們將繼續 Spring Security 註冊系列,通過添加 Google reCAPTCHA 到註冊流程中,以區分人類和機器人。

2. 集成 Google 的 reCAPTCHA

要集成 Google 的 reCAPTCHA Web 服務,首先需要將我們的網站註冊到該服務上,將他們的庫添加到我們的頁面,然後使用 Web 服務驗證用户的 captcha 響應。

請在 https://www.google.com/recaptcha/admin 註冊我們的網站。註冊過程會生成一個 site-keysecret-key,用於訪問 Web 服務。

2.1. 存儲 API 密鑰對

我們存儲密鑰在 application.properties 中:

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

並將它們通過使用帶有 @ConfigurationProperties 註解的 Bean 暴露給 Spring:

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

    private String site;
    private String secret;

    // standard getters and setters
}

2.2. 顯示 Widget

基於該系列教程,我們將修改 registration.html 以包含 Google 的庫。

在我們的註冊表單中,我們添加 reCAPTCHA Widget,該 Widget 期望 data-sitekey 屬性包含 site-key

當提交表單時,Widget 會添加 請求參數 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. 服務器端驗證

新請求參數編碼了我們的站點密鑰和用户成功完成挑戰的唯一字符串。

但是,由於我們無法自行判斷這一點,因此我們不能信任用户提交的內容是否合法。向服務器端發出請求,以使用 Web 服務 API 驗證 驗證碼響應

端點接受 HTTP 請求,URL 為 https://www.google.com/recaptcha/api/siteverify,帶有查詢參數 secretresponseremoteip

它返回具有以下模式的 JSON 響應:


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

3.1. 獲取用户的響應

用户對 reCAPTCHA 挑戰的響應從請求參數 g-recaptcha-response 中使用 HttpServletRequest 檢索,並使用我們的 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. 驗證服務

應首先對獲取到的驗證碼響應進行清理。使用正則表達式進行簡單的檢查。

如果響應看起來合法,我們然後使用 secret-keycaptcha response 和客户端的 IP address 向 Web 服務發出請求:


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. 對象化驗證

Java 豆,帶有 Jackson 註解進行裝飾,封裝驗證響應:


@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
}

如所暗示的,success 屬性中的真值表示用户已驗證。否則,errorCodes 屬性將填充原因。

hostname 指向將用户重定向到 reCAPTCHA 的服務器。如果您管理許多域名,並且希望它們都使用同一密鑰對,則可以自行驗證 hostname 屬性。

3.4. 驗證失敗

在驗證失敗的情況下,會拋出異常。 reCAPTCHA 庫需要指示客户端創建一個新的挑戰。

我們通過在客户端的註冊錯誤處理程序中調用庫的 grecaptcha 組件的 reset 方法來做到這一點:


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);
        }
        ...
    }
}

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;
    }
}

we record the attempts containing an error with the client’s response. Successful validation clears the attempts cache:">

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());
    }
}

谷歌的 reCAPTCHA v3 與之前的版本不同,它不需要任何用户交互。它只是為我們發送的每個請求提供一個分數,並允許我們決定對於我們 Web 應用程序採取的最終操作。

再次強調,要集成谷歌的 reCAPTCHA 3,我們首先需要註冊我們的網站,添加他們的庫到我們的頁面,然後驗證令牌響應與 Web 服務。

因此,我們應該在 https://www.google.com/recaptcha/admin/create 註冊我們的網站,並在選擇 reCAPTCHA v3 後,我們會獲得新的密鑰和站點密鑰。

5.1. Updating application.properties and CaptchaSettings

註冊後,我們需要使用新的密鑰和我們選擇的閾值更新 application.properties

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

請注意,將閾值設置為 0.5 只是一個默認值,並且可以通過分析實際閾值在 谷歌管理控制枱 中對其進行調整。

接下來,讓我們更新我們的 CaptchaSettings 類:

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

    // standard getters and setters
}

5.2. Front-End Integration

現在我們將修改 registration.html 以包含谷歌的庫,並使用我們的站點密鑰。

在我們的註冊表單中,我們添加一個隱藏字段,用於存儲從調用 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();
});
</script>
</head>
</body>
</html>

5.3. Server-Side Validation

我們需要對響應令牌與 Web 服務 API 進行驗證,就像在服務器端 reCAPTCHA 驗證中一樣。

響應 JSON 對象將包含兩個附加屬性:

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

分數基於用户的交互,並且是 0(極有可能是一個機器人)到 1.0(極有可能是一個人類)之間的值。

Action 是谷歌引入的一個新概念,以便我們可以在同一個 Web 頁面上執行多個 reCAPTCHA 請求。

在執行 reCAPTCHA v3 時,必須指定 Action 的值,並且必須驗證 Action 值的有效性。

5.4. Retrieve the Response Token

reCAPTCHA v3 的響應令牌從請求參數 response 中使用 HttpServletRequest 檢索,並與我們的 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

經過重構的 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());
    }
}

如果驗證失敗,我們將拋出異常,但請注意,v3 中沒有 reset 方法,可以在 JavaScript 客户端調用。

我們仍然會使用之前版本中用於保護服務器資源的相同實現。

5.6. Updating the GoogleResponse Class

我們需要將新的屬性 scoreaction 添加到 GoogleResponse Java 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. 結論

在本文中,我們已將 Google 的 reCAPTCHA 庫集成到我們的註冊頁面中,並實施了使用服務器端請求驗證 reCAPTCHA 響應的服務。

稍後,我們使用 Google 的 reCAPTCHA v3 庫升級了註冊頁面,並發現註冊表單變得更簡潔,因為用户無需再執行任何操作。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.