Wicket の
ボタン連打で
前提. 使用している Wicket, EclipseLink の Version
- Wicket 7.5
<dependency> <groupId>org.apache.wicket</groupId> <artifactId>wicket-core</artifactId> <version>7.5.0</version> </dependency>
エラーの 発生している フォーム
Wicket Form に
で
エラーの 発生から 考える 実装時に 考慮しなければならない ポイント(個人 調べ)
以下の
各考慮ポイントを
AjaxButton に
対しての 連打対策 (client 側) 現在の 実装だと 連打可能なので、 連打が できないようにする 必要が あります。
これはclient 側へ 実装します。 AjaxButton に
対しての 連打対策 (server 側) client 側の 対応だけだと、 直接 Submit には 対応できないので、
サーバ側のプログラムで、 連続リクエストに 対する 対応を 実施します。 2
[2] Wicket はForm の URL が 自動生成されるので、 この 攻撃自体が 結構難しいかと 思いますが、
不可能ではないため対策を して おきます。
AjaxButton に 対しての 連打対策
以下の
AjaxButton#updateAjaxAttributes
を
以下の
Validation の
実行後に、 確認ダイアログを 出力する。 確認ダイアログ実行後は、
処理完了まで、 ボタンは 非活性化する。
Validation の 実行後に、 確認ダイアログを 出力する。
Validation の<wbr>後に<wbr>確認ダイアログを<wbr>出したい
ですが、
Validation のadd(new AjaxFormValidatingBehavior("blur", Duration.ONE_SECOND));
で
Button クリック前の
この
Qiita のonPrecondition()
で
@Override protected void updateAjaxAttributes(AjaxRequestAttributes attributes) { super.updateAjaxAttributes(attributes); final AjaxCallListener ajaxCallListener = new AjaxCallListener(); ajaxCallListener.onPrecondition("return confirm('" + getString("messageMailSendConfirm") + "');"); attributes.getAjaxCallListeners().add(ajaxCallListener); }
確認ダイアログ実行後は、 処理完了まで、 ボタンは 非活性化する。
Request 送信直前 onBeforeSend
で
Request 送信完了後onComplete
で
するようにしました。
Validation で
エラー発生時も、
UI 的には
@Override protected void updateAjaxAttributes(AjaxRequestAttributes attributes) { super.updateAjaxAttributes(attributes); // AjaxButton は (というかButtonは) setOutputMarkupId(true) なので、特に呼び出さなくてOK final AjaxCallListener ajaxCallListener = new AjaxCallListener(); ajaxCallListener.onPrecondition("return confirm('" + getString("messageMailSendConfirm") + "');"); ajaxCallListener.onBeforeSend("$('#" + getMarkupId() + "').prop('disabled',true);"); ajaxCallListener.onComplete("$('#" + getMarkupId() + "').prop('disabled',false);$('#" + getMarkupId() + "').focus();"); attributes.getAjaxCallListeners().add(ajaxCallListener); }
以下の
Github で、
そちらの
補足. AjaxCallListener の メソッド呼び出し順序
AjaxCallListener
には、
実行順序が
感覚的には、
メソッド名 | 呼び出し順序 | 説明 |
---|---|---|
onInit(final CharSequence init) | 1 | AjaxButton のクリック直後に呼び出されます。 |
onBefore(final CharSequence before) | 2 | onInit() の呼び出し後に呼び出されます。 |
onPrecondition(final CharSequence before) | 3 | onBefore() 呼び出し後に呼び出されます。確認ダイアログを挿入すると、キャンセルクリックで後処理の中断ができます。 |
onPrecondition(final CharSequence before) | 4 | onBefore() 呼び出し後に呼び出されます。確認ダイアログを挿入すると、キャンセルクリックで後処理の中断ができます。 |
onBeforeSend(final CharSequence beforeSend) | 5 | onPrecondition() の呼び出し後に呼び出されます。リクエスト送信直前 |
onAfter(final CharSequence beforeSend) | 5 | onBeforeSend() の呼び出し後に呼び出されます。リクエスト送信直後 |
onSuccess(final CharSequence beforeSend) | 6 | onAfter() の呼び出し後、リクエストが成功した場合に呼び出されます。 |
onFailure(final CharSequence beforeSend) | 6 | onAfter() の呼び出し後、リクエストが失敗した場合に呼び出されます。 |
onComplete(final CharSequence beforeSend) | 7 | onSuccess() 、onFailure() の呼び出し後、呼び出されます。 |
onDone(final CharSequence beforeSend) | 8 | onComplete() の呼び出し後、呼び出されます。また、onPrecondition() がFalse の場合にも呼び出しが行われます。 |
補足. TextField の EnterKey クリックで、 Submit するのを 防ぐ
AjaxPreventSubmitBehavior.java をTextField
のBehavior
と
ので、TextField
に
// eMail TextField eMail = new RequiredTextField("email"); eMail.setLabel(Model.of(getString("eMailLabelText"))); eMail.add(StringValidator.maximumLength(150)); eMail.add(EmailAddressValidator.getInstance()); eMail.add(new AjaxPreventSubmitBehavior()); add(eMail);
サーバ側の Submit URL に 対する 連打対策
続いて Submit URL に
DOS アタックを 防ぐ
Dos Attack 的な
AP Server? は
この
また、
そちらの
DOS FIlter を Wicket に 適用する
通常のorg.eclipse.jetty.servlets.DoSFilter
に
Ajax Request に
[3] Servlet Filter と
この
以下、
- Application.java
// RequestCycleListener の登録 getRequestCycleListeners().add(new DoSPreventRequestCycleListener());
DoSPreventRequestCycleListener.java
onBeginRequest で、
実装を 行いましたが、 throw しても、 エラーと して 認識されず、 onRequestHandlerResolved で 処理を 行うようにしました。 Ajax リクエストでない
場合のみ、 Exceptionを スロー、 Ajax の 処理は、 AjaxButton内で 記載するようにしました。
実は、onRequestHandlerResolved
内で記載してもうまく いくかもしれません。 HTTP ERROR CODE 429 は、
Too Many Requests
です。
DoSFilter のデフォルトの エラーコードが、 429 の ため 踏襲しています。 チェックエラー時は、
httpServletRequest.removeAttribute("DoSFilter.Tracker");
で属性削除しています。
これは、削除しないと、 無限ループに 陥り、 StackOverFlowが 発生した ためです。 4
[4] 処理として、 不安定な 処理かと、、 もっと いい 方法が あるかもしれません。
import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.cycle.AbstractRequestCycleListener; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.http.WebRequest; import org.apache.wicket.request.http.flow.AbortWithHttpErrorCodeException; import javax.servlet.http.HttpServletRequest; /** * DoSPreventRequestCycleListener */ public class DoSPreventRequestCycleListener extends AbstractRequestCycleListener { @Override public void onBeginRequest(RequestCycle cycle) { // Do Nothing... } @Override public void onRequestHandlerResolved(RequestCycle cycle, IRequestHandler handler) { // Cast WebRequest WebRequest webRequest = (WebRequest) cycle.getRequest(); // Ajax リクエストではない通常のリクエストの場合、DoSFilter.Trackerが設定されている場合、Denyする。 if (!webRequest.isAjax()) { HttpServletRequest httpServletRequest = (HttpServletRequest) webRequest.getContainerRequest(); Object tracker = httpServletRequest.getAttribute("DoSFilter.Tracker"); if (tracker != null) { httpServletRequest.removeAttribute("DoSFilter.Tracker"); throw new AbortWithHttpErrorCodeException(429, "Too Many Requests"); } } } }
- AjaxButton.java
- Form に
追加した AjaxButtonの 処理で、 onSubmit() で、 DoSFilter.Tracker
が存在したら、 エラーと する 処理を 追加しました。
- Form に
AjaxButton sendMessage = new AjaxButton("sendMessage", Model.of(getString("messageButtonLabelText"))) { private static final long serialVersionUID = -8204140666393922700L; @Override protected void onSubmit(AjaxRequestTarget target, Form<?> form) { HttpServletRequest httpServletRequest = (HttpServletRequest) form.getRequest().getContainerRequest(); Object tracker = httpServletRequest.getAttribute("DoSFilter.Tracker"); if (tracker != null) { httpServletRequest.removeAttribute("DoSFilter.Tracker"); error(getString("messageMailDoubleSendError")); target.add(form); return; } // メール送信要求を登録 ContactMailTaskManage contactMailTaskManage = (ContactMailTaskManage) form.getModelObject(); mailTaskService.execute(contactMailTaskManage); // Formに対してのオペレーション token.regenerateToken(); success(getString("messageMailSendSucceed")); target.add(form); } }
DoSFilter の
定義値 Wicket 側で 制御させる ために、 プロパティ値を 以下のような 設定と しました。 delayMs
を0 に する。 delayMs
を0 に する ことで、 リクエストを 遅延させる 処理も 行わず、 Filter で Exception を 送出しない 設定と なります。
この際、 属性 DoSFilter.Tracker
が設定される ため、 この 属性値の 有無で、 DoS検知に ヒットしたかどうかを、 判断するようにしました。
単純な 2重送信を 防止する
連打防止ではなく、
[5] 戻る
- SynchTokenField
public class SynchTokenField extends HiddenField { private static final long serialVersionUID = 5172245669993637549L; private String token; /** * Construct. * * @param id */ public SynchTokenField(String id) { super(id); } @Override protected void onInitialize() { super.onInitialize(); setType(String.class); setRequired(true); setModel(Model.of(regenerateToken())); add(new IValidator<String>() { @Override public void validate(IValidatable<String> validatable) { log.info("validatable.getValue() = {} || token = {}", validatable.getValue(), token); if (token == null || !token.equals(validatable.getValue())) { ValidationError validation = new ValidationError(getString("messageMailDoubleSendError")); error(validation); } } }); } protected final void onComponentTag(final ComponentTag tag) { super.onComponentTag(tag); tag.put("value", token); } public String regenerateToken() { token = UUID.randomUUID().toString(); return token; } }
- AjaxButton#onSubmit() onSubmit()内で、
メイン処理終了後に、 regenerateToken()
でtokenを 再生成します。
2度押しの場合、 2度目は token
が異なるので、 validation エラーと なります。
タイミング的に、 2回走る 可能性は ありえるので、 同期化は 行った ほうが よさそうです。 6
[6] 要は、以下の コードだと バグり そうに 思ってます。。も うちょっと いい 感じに できないかなと
@Override protected void onSubmit(AjaxRequestTarget target, Form<?> form) { HttpServletRequest httpServletRequest = (HttpServletRequest) form.getRequest().getContainerRequest(); Object tracker = httpServletRequest.getAttribute("DoSFilter.Tracker"); if (tracker != null) { httpServletRequest.removeAttribute("DoSFilter.Tracker"); error(getString("messageMailDoubleSendError")); target.add(form); return; } // メール送信要求を登録 ContactMailTaskManage contactMailTaskManage = (ContactMailTaskManage) form.getModelObject(); mailTaskService.execute(contactMailTaskManage); // Formに対してのオペレーション token.regenerateToken(); success(getString("messageMailSendSucceed")); target.add(form); }
以下の
記事を 参考に 作成しました。
まとめ
Wicket で、
通常の
リクエストでは、 PRG パターンで、 F5 更新は 防げるが、 戻る ボタンクリックで 再送信、 ボタン連打は 防げないので、 考慮が 必要。
また、Submitされた 側での 制御よりも、 戻ってきたら、 [有効期限切れです。 ]に したほうが いいかもしれない。 Ajax リクエストだと、
より、 Wicket の 制御で 守られないので、 独自で 実装が 必要。 DosFilter などを
使うと、 Wicketの Filterの 処理と ぶつかる、 Wicket の Ajax の 部品の 想定外の 処理と ぶつかって、 制御が 若干面倒くさい。 Wicket6 から、
CsrfPreventionRequestCycleListener.java が 追加されて、 これは このような 用途にも 使えるかもしれない。 Form での
Token発行は、 基本的に、 Stateless な ページで 使う もののような 気は するが、 Submit時に、 tokenを Nullに しておけば、
戻った後は、 Session上の tokenは Nullだけど、 HTML上の tokenは あるになって、 戻る 制御は 効きそうな 気はする。
だらだらと
コメント