1. 概述
本教程是 Spring Security 注册系列的延续,我们将通过集成 Google reCAPTCHA 来增强注册流程,有效区分人类用户和恶意机器人。
2. 集成 Google reCAPTCHA
要集成 Google reCAPTCHA 服务,需完成三个关键步骤:注册站点获取密钥、在页面引入库文件、通过服务端验证用户响应。
2.1 获取并存储 API 密钥
首先在 Google reCAPTCHA 管理台 注册站点,获取 site-key 和 secret-key:
# application.properties
google.recaptcha.key.site=6LfaHiITAAAA...
google.recaptcha.key.secret=6LfaHiITAAAA...
通过 @ConfigurationProperties
注解的 bean 暴露配置:
@Component
@ConfigurationProperties(prefix = "google.recaptcha.key")
public class CaptchaSettings {
private String site;
private String secret;
// 标准 getter/setter
}
2.2 前端展示验证码组件
修改 registration.html
模板,在表单中添加 reCAPTCHA 组件:
<!DOCTYPE html>
<html>
<head>
<script src='https://www.google.com/recaptcha/api.js'></script>
</head>
<body>
<form method="POST" enctype="utf8">
<!-- 其他表单字段 -->
<!-- reCAPTCHA 组件 -->
<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>
</form>
</body>
</html>
⚠️ 组件会自动在提交时附加 g-recaptcha-response
参数
3. 服务端验证机制
3.1 提取用户响应
在控制器中获取验证码响应并调用验证服务:
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); // 验证失败会抛出异常
// 后续注册逻辑
}
}
3.2 实现验证服务
验证服务需完成三步:响应净化、API 调用、结果解析:
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) {
// 1. 响应净化
if (!responseSanityCheck(response)) {
throw new InvalidReCaptchaException("响应包含非法字符");
}
// 2. 构建 API 请求
URI verifyUri = URI.create(String.format(
"https://www.google.com/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s",
getReCaptchaSecret(), response, getClientIP()));
// 3. 发送请求并解析结果
GoogleResponse googleResponse = restTemplate.getForObject(verifyUri, GoogleResponse.class);
if (!googleResponse.isSuccess()) {
throw new ReCaptchaInvalidException("reCAPTCHA 验证失败");
}
}
private boolean responseSanityCheck(String response) {
return StringUtils.hasLength(response) && RESPONSE_PATTERN.matcher(response).matches();
}
}
3.3 响应对象封装
使用 Jackson 注解封装 Google API 响应:
@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() {
if (errorCodes == null) return false;
return Arrays.stream(errorCodes)
.anyMatch(e -> e == ErrorCode.InvalidResponse || e == ErrorCode.MissingResponse);
}
static enum ErrorCode {
MissingSecret, InvalidSecret,
MissingResponse, InvalidResponse;
private static Map<String, ErrorCode> 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());
}
}
// 标准 getter/setter
}
3.4 验证失败处理
前端 JavaScript 处理验证失败场景:
register(event) {
event.preventDefault();
var formData = $('form').serialize();
$.post(serverContext + "user/registration", formData, function(data) {
if (data.message == "success") {
// 成功处理逻辑
}
})
.fail(function(data) {
grecaptcha.reset(); // 重置验证码
if (data.responseJSON.error == "InvalidReCaptcha") {
$("#captchaError").show().html(data.responseJSON.message);
}
});
}
4. 服务端资源防护
4.1 实现尝试次数限制
使用 Guava Cache 限制客户端失败次数:
public class ReCaptchaAttemptService {
private static final int MAX_ATTEMPT = 4;
private LoadingCache<String, Integer> attemptsCache;
public ReCaptchaAttemptService() {
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) {
attemptsCache.put(key, attemptsCache.getUnchecked(key) + 1);
}
public boolean isBlocked(String key) {
return attemptsCache.getUnchecked(key) >= MAX_ATTEMPT;
}
}
4.2 集成防护机制
重构验证服务,加入尝试次数检查:
public class CaptchaService implements ICaptchaService {
@Autowired private ReCaptchaAttemptService reCaptchaAttemptService;
@Override
public void processResponse(String response) {
// 检查是否超过尝试限制
if (reCaptchaAttemptService.isBlocked(getClientIP())) {
throw new InvalidReCaptchaException("客户端超过最大尝试次数");
}
// ... 原有验证逻辑 ...
GoogleResponse googleResponse = restTemplate.getForObject(verifyUri, GoogleResponse.class);
if (!googleResponse.isSuccess()) {
if (googleResponse.hasClientError()) {
reCaptchaAttemptService.reCaptchaFailed(getClientIP());
}
throw new ReCaptchaInvalidException("reCAPTCHA 验证失败");
}
reCaptchaAttemptService.reCaptchaSucceeded(getClientIP());
}
}
5. 升级 reCAPTCHA v3
5.1 配置更新
在 application.properties
添加阈值配置:
google.recaptcha.key.site=6LefKOAUAAAAAE...
google.recaptcha.key.secret=6LefKOAUAAAA...
google.recaptcha.key.threshold=0.5
更新 CaptchaSettings
类:
@Component
@ConfigurationProperties(prefix = "google.recaptcha.key")
public class CaptchaSettings {
// ... 其他属性
private float threshold;
// 标准 getter/setter
}
5.2 前端集成
修改 registration.html
实现无感验证:
<!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(token) {
$('#response').val(token);
$('form').submit();
});
</script>
</body>
</html>
5.3 服务端验证增强
验证服务需检查新增的 score
和 action
字段:
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 验证失败");
}
reCaptchaAttemptService.reCaptchaSucceeded(getClientIP());
}
}
5.4 更新响应对象
在 GoogleResponse
中添加新字段:
@JsonPropertyOrder({
"success", "score", "action",
"challenge_ts", "hostname", "error-codes"
})
public class GoogleResponse {
// ... 其他属性
@JsonProperty("score")
private float score;
@JsonProperty("action")
private String action;
// 标准 getter/setter
}
6. 总结
本文实现了以下关键功能: ✅ 集成 reCAPTCHA v2 到 Spring Security 注册流程 ✅ 实现服务端验证机制 ✅ 添加客户端尝试次数限制 ✅ 升级到无感验证的 reCAPTCHA v3
完整实现代码可在 GitHub 获取。踩坑提示:生产环境建议结合 CDN 和速率限制实现更全面的防护。