知識庫 / Spring / Spring Security RSS 訂閱

Spring 註冊 – 集成 reCAPTCHA

Spring Security
HongKong
6
02:49 PM · Dec 06 ,2025

1. 概述

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

2. 集成 Google reCAPTCHA

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

請在 https://www.google.com/recaptcha/admin 處註冊我們的網站。註冊過程會生成一個 站點密鑰 和一個 密鑰,用於訪問 Web 服務。

2.1. 存儲 API 密鑰對

我們存儲密鑰位於 application.properties 中:

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

將他們暴露給 Spring,使用帶有 @ConfigurationProperties 註解的 Bean:

@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,帶有查詢參數 secret, response, 和 remoteip。 它返回一個具有以下模式的 JSON 響應:

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

3.1. 獲取用户響應

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

首先需要對獲取到的驗證碼響應進行清理(sanitize)。使用簡單的正則表達式。

如果響應看起來有效,我們將會向 Web 服務發送請求,其中包含 密鑰驗證碼響應和客户端的 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();
    }
}
<h3><strong>3.3. 對象化驗證</strong></h3>
<p>使用 <em >Jackson</em> 註解裝飾的 Java Bean 封裝了驗證響應。</p>
@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);
        }
        ...
    }
}

4. 保護服務器資源

惡意客户端無需遵守瀏覽器沙箱的規則。因此,我們的安全理念應側重於暴露的資源以及它們可能被濫用的方式。

4.1. 嘗試緩存

需要理解的是,通過集成 reCAPTCHA,每次請求都會導致服務器創建一個套接字以驗證請求。

雖然要實現真正的 DoS 緩解需要更復雜的層次化方法,但我們可以實現一個基本的緩存機制,限制客户端最多嘗試 4 次無效的 reCAPTCHA 響應:

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;
    }
}
<h3><strong>4.2. 重新構建驗證服務</strong></h3
<p>首先,通過中止請求,如果客户端已超過嘗試限制。否則,在處理未成功的 <em >GoogleResponse</em> 時,我們會記錄包含錯誤的嘗試以及客户端的響應。成功的驗證會清除嘗試緩存:</p>
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. 集成 Google reCAPTCHA V3

Google 的 reCAPTCHA v3 與之前的版本不同,它不需要任何用户交互。它只是為我們發送的每個請求提供一個評分,並允許我們為我們的 Web 應用程序採取最終行動。

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

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

5.1. 更新 application.propertiesCaptchaSettings

在註冊後,我們需要使用新的鍵值和我們選擇的閾值值更新 application.properties 文件。

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

需要注意的是,設為 0.5 的閾值是一個默認值,可以通過分析實際閾值在 Google 管理控制枱 中的值來動態調整。

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

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

5.2. 前端集成

我們將修改 registration.html 以包含 Google 提供的庫,並添加我們的站點密鑰。

在我們的註冊表單中,我們添加一個隱藏字段,用於存儲從調用 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. 服務器端驗證

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

響應 JSON 對象將包含兩個額外的屬性:

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

評分基於用户的交互,取值範圍在 0 (極有可能是機器人) 到 1.0 (極有可能是人類) 之間。

Action 是 Google 引入的一個新概念,以便我們在同一網頁上執行大量的 reCAPTCHA 請求。

每次執行 reCAPTCHA v3 時,必須指定 Action 屬性,並且必須驗證響應中 Action 屬性的值與預期名稱是否相符。

5.4. 獲取響應令牌

<em>response</em> 請求參數中通過 <em>HttpServletRequest</em> 獲取 reCAPTCHA v3 響應令牌,並使用我們的 <em>CaptchaService</em> 進行驗證。 機制與上面在 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. 重新設計驗證服務

重新設計的 <em >CaptchaService</em> 驗證服務類包含一個 <em >processResponse</em> 方法,類似於先前版本中的 <em >processResponse</em> 方法,但它會檢查 <em >GoogleResponse</em><em >action</em><em >score</em> 參數。

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 版本中,JavaScript 客户端中沒有 reset 方法可調用。

我們仍然將採用上述相同的實現方式來保護服務器資源。

5.6. 更新 GoogleResponse

我們需要將新的屬性 scoreaction 添加到 Java Bean GoogleResponse 中:

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