Springへの登録 – reCAPTCHAを統合する
1概要
この記事では、リンクを続ける:/spring-security-registration[Spring Securityの登録]シリーズでは、登録プロセスに
Google
reCAPTCHA
を追加して、人間とボットを区別します。
2 GoogleのreCAPTCHA
を統合する
GoogleのreCAPTCHAウェブサービスを統合するには、まずサイトをサービスに登録し、そのライブラリをページに追加してから、ユーザーのキャプチャ応答をウェブサービスで確認する必要があります。
https://www.google.com/recaptcha/adminで私たちのサイトを登録しましょう。登録プロセスはWebサービスにアクセスするための
site-key
と
secret-key
を生成します。
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. ウィジェットを表示する
シリーズのチュートリアルを基にして、Googleのライブラリを含めるように
registration.html
を変更します。
登録フォームの中に、属性
data-sitekey
に
site-key
が含まれることを期待するreCAPTCHAウィジェットを追加します。
ウィジェットは送信時にリクエストパラメータ
g-recaptcha-response
を追加します** :
<!DOCTYPE html>
<html>
<head>
...
<script src='https://www.google.com/recaptcha/api.js'></script>
</head>
<body>
...
<form action="/" 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を使用して
captcha response
を検証するためにサーバー側の要求が行われます。
エンドポイントは、クエリパラメータ
secret
、
response
、および__ ** remoteipを使用して、URL
https://www.google.com/recaptcha/api/siteverifyでHTTPリクエストを受け入れます。スキーマを持つjsonレスポンス
:
{
"success": true|false,
"challenge__ts": timestamp,
"hostname": string,
"error-codes":[...]}
3.1. ユーザーの回答を取得する
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. 検証サービス
取得したキャプチャ応答は最初にサニタイズする必要があります。単純な正規表現が使用されています。
応答が合法的に見える場合は、
secret-key
、
captcha response
、およびクライアントの
IPアドレス
を使用して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. 検証の客観化
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
}
暗黙のとおり、
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つの失敗したキャプチャ応答に制限する基本キャッシュを実装できます。
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. 検証サービスのリファクタリング
クライアントが試行回数の制限を超えた場合は、最初にキャッシュが中止されます。それ以外の場合、失敗した
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結論
この記事では、GoogleのreCAPTCHAライブラリを登録ページに統合し、キャプチャ応答をサーバーサイドの要求で検証するサービスを実装しました。
このチュートリアルの完全な実装はhttps://github.com/eugenp/spring-security-registration/tree/master[githubプロジェクト]で利用可能です – これはMavenベースのプロジェクトなのでインポートと実行が簡単であるべきですそのまま。