ウェブUIスパム保護とCAPTCHAのサポート
GitLabアプリケーションの新しいUIエリアにスパム保護とCAPTCHAサポートを追加するアプローチは、既存のコードがどのように実装されているかによって異なります。
サポートされるリクエスト送信のシナリオ
3つの異なるシナリオがサポートされています。2つはApolloまたはAxiosのJavaScript XHR/Fetchリクエストで使用され、1つは標準的なHTMLフォームリクエストでのみ使用されます:
- JavaScriptベースの送信(おそらくVue経由)
- Apolloの使用(Fetch/XHRリクエスト経由のGraphQL API)
- Axiosの使用(Fetch/XHRリクエストによるREST API)
- 標準的な HTML フォームの送信 (HTML リクエスト)
実装の一部は、これらのシナリオのどれをサポートしなければならないかに依存します。
JavaScript XHR/Fetch リクエスト特有の実装タスク
2つのアプローチが完全にサポートされています:
- GraphQL APIを使用するApollo。
- Axios では、GraphQL API のいずれかを使用します。
フロントエンドとバックエンド間のスパムやCAPTCHA関連のデータ通信は、モデルにフィールドを追加する必要はありません。代わりに通信が処理されます:
- リクエストのカスタムヘッダ値を通して。
- レスポンスのトップレベルのJSONフィールドを通して。
スパムとCAPTCHA関連のロジックは、再利用可能なモジュールとヘルパーメソッドにきれいに抽象化されており、既存のロジックをラップし、潜在的なスパムが検出されたり、CAPTCHAの表示が必要になった場合にのみ、既存のフローを変更することができます。このアプローチにより、既存のロジックに最小限の変更を加えるだけで、スパムやCAPTCHAのサポートをアプリケーションの新しいエリアに追加することができます。フロントエンドの場合、潜在的にゼロの変更が必要です!
フロントエンドでは、ApolloはApolloLink
、AxiosはAxiosインターセプターを使用して、抽象的かつ透過的に処理されます。CAPTCHAの表示は、標準的なGitLab UI / Pajamasモーダルコンポーネントによって処理されます。関連するフロントエンドのコードはapp/assets/javascripts/captcha
にあります。
しかし、リクエストの遮断とモーダルの実際の処理は透過的であり、フォームやページに関連する JavaScript や Vue コンポーネントに強制的な変更を加えなくても、リクエストやエラーの処理に変更が必要になる場合があります。たとえば、CAPTCHA 表示の失敗やキャンセルによって、標準的なリクエストフローや UI の更新が中断される場合などです。すべてのシナリオの入念な探索テストは、潜在的な問題を発見するために重要です。
このシーケンス図は、フロントエンドの JavaScript XHR/Fetch リクエストの標準的な CAPTCHA フローを示しています:
バックエンドもまた、mixin モジュールとヘルパーメソッドによってきれいに抽象化されています。関連するバックエンドコントローラのアクション (通常はcreate
/update
だけ) に必要な主な変更は、次の 3 つです:
- Update Service クラスのコンストラクタに
perform_spam_check: true
を渡します。Create Serviceでは、デフォルトでtrue
。 - スパムチェックにより、モデルへの変更がスパムの可能性があることが示された場合:
- エラーがモデルに追加されます。
- モデルの
needs_recaptcha
プロパティが true に設定されます。
- 既存のコントローラアクションの戻り値 (レンダリングまたはリダイレクト) を、
#with_captcha_check_json_format
ヘルパーメソッドに渡されるブロックでラップし、透過的に処理します:- CAPTCHA が有効かどうかをチェックし、有効なら次のステップに進みます。
- モデルにエラーが含まれ、
needs_recaptcha
フラグが true であるかどうかをチェックします。- yesの場合: JSONレスポンスに適切なspamまたはCAPTCHAフィールドを追加し、
409 - Conflict
HTTPステータスコードを返します。 - noの場合(CAPTCHAが無効な場合、またはスパムが検出されなかった場合):ブロックで渡された標準のリクエストリターン・ロジックが実行されます。
- yesの場合: JSONレスポンスに適切なspamまたはCAPTCHAフィールドを追加し、
抽象化のおかげで、説明するよりも実装する方が簡単です。隠された詳細についてはあまり気にする必要はありません!
以下の変更を行ってください:
コントローラのアクションにサポートを追加します。
機能のフロントエンドがコントローラのアクションに直接サブミットし、GraphQL APIだけを使用しない場合は、適切なコントローラにサポートを追加する必要があります。
アクションメソッドはコントローラクラスに直接記述することもできますし、コントローラクラスに含まれるモジュールに抽象化することもできます。この例ではモジュールを使用しています。コントローラを直接変更する場合の唯一の違いは、extend ActiveSupport::Concern
が必要ないことです。
module WidgetsActions
# NOTE: This `extend` probably already exists, but it MUST be moved to occur BEFORE all
# `include` statements. Otherwise, confusing bugs may occur in which the methods
# in the included modules cannot be found.
extend ActiveSupport::Concern
include SpammableActions::CaptchaCheck::JsonFormatActionsSupport
def create
widget = ::Widgets::CreateService.new(
project: project,
current_user: current_user,
params: params
).execute
respond_to do |format|
format.json do
with_captcha_check_json_format do
# The action's existing `render json: ...` (or wrapper method) and related logic. Possibly
# including different rendering cases if the model is valid or not. It's all wrapped here
# within the `with_captcha_check_json_format` block. For example:
if widget.valid?
render json: serializer.represent(widget)
else
render json: { errors: widget.errors.full_messages }, status: :unprocessable_entity
end
end
end
end
end
end
HTML フォームリクエスト特有の実装タスク
アプリケーションの一部の内部では、JavaScript クライアントを介して GraphQL API を使用するように変換されていませんが、代わりにHTML
MIME タイプ リクエストを介した標準的な Rails HAML フォーム送信に依存しています。これらの領域では、アクションはレスポンス ボディとして事前にレンダリングされた HTML(HAML) ページを返します。残念ながら、この場合、上で説明したようなJavaScriptベースのフロントエンドサポートを使用することはできません。代わりに HAML テンプレートを使って CAPTCHA フォームのレンダリングを処理する別のアプローチを使わなければなりません。
すべてがきれいに抽象化されており、バックエンドコントローラの実装はJavaScript/JSONベースのアプローチとほぼ同じです。モジュール名とヘルパーメソッドのJSON
という単語をHTML
(適切な大文字と小文字を使用) に置き換えてください。
アクションメソッドはコントローラに直接書くこともできますし、 モジュールに書くこともできます。この例では、直接コントローラの中にあります。また、create
の代わりにupdate
メソッドを使用しています:
class WidgetsController < ApplicationController
include SpammableActions::CaptchaCheck::HtmlFormatActionsSupport
def update
# Existing logic to find the `widget` model instance...
::Widgets::UpdateService.new(
project: project,
current_user: current_user,
params: params,
perform_spam_check: true
).execute(widget)
respond_to do |format|
format.html do
if widget.valid?
# NOTE: `spammable_path` is required by the `SpammableActions::AkismetMarkAsSpamAction`
# module, and it should have already been implemented on this controller according to
# the instructions above. It is reused here to avoid duplicating the route helper call.
redirect_to spammable_path
else
# If we got here, there were errors on the model instance - from a failed spam check
# and/or other validation errors on the model. Either way, we'll re-render the form,
# and if a CAPTCHA render is necessary, it will be automatically handled by
# `with_captcha_check_html_format`
with_captcha_check_html_format { render :edit }
end
end
end
end
end