ウェブUIスパム保護とCAPTCHAのサポート

GitLabアプリケーションの新しいUIエリアにスパム保護とCAPTCHAサポートを追加するアプローチは、既存のコードがどのように実装されているかによって異なります。

サポートされるリクエスト送信のシナリオ

3つの異なるシナリオがサポートされています。2つはApolloまたはAxiosのJavaScript XHR/Fetchリクエストで使用され、1つは標準的なHTMLフォームリクエストでのみ使用されます:

  1. JavaScriptベースの送信(おそらくVue経由)
    1. Apolloの使用(Fetch/XHRリクエスト経由のGraphQL API)
    2. Axiosの使用(Fetch/XHRリクエストによるREST API)
  2. 標準的な HTML フォームの送信 (HTML リクエスト)

実装の一部は、これらのシナリオのどれをサポートしなければならないかに依存します。

JavaScript XHR/Fetch リクエスト特有の実装タスク

2つのアプローチが完全にサポートされています:

  1. GraphQL APIを使用するApollo。
  2. 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 フローを示しています:

sequenceDiagram participant U as User participant V as Vue/JS Application participant A as ApolloLink or Axios Interceptor participant G as GitLab API U->>V: Save model V->>A: Request A->>G: Request G--xA: Response with error and spam/CAPTCHA related fields A->>U: CAPTCHA presented in modal U->>A: CAPTCHA solved to obtain valid CAPTCHA response A->>G: Request with valid CAPTCHA response and SpamLog ID in headers G-->>A: Response with success A-->>V: Response with success

バックエンドもまた、mixin モジュールとヘルパーメソッドによってきれいに抽象化されています。関連するバックエンドコントローラのアクション (通常はcreate/update だけ) に必要な主な変更は、次の 3 つです:

  1. Update Service クラスのコンストラクタにperform_spam_check: true を渡します。Create Serviceでは、デフォルトでtrue
  2. スパムチェックにより、モデルへの変更がスパムの可能性があることが示された場合:
    • エラーがモデルに追加されます。
    • モデルのneeds_recaptcha プロパティが true に設定されます。
  3. 既存のコントローラアクションの戻り値 (レンダリングまたはリダイレクト) を、#with_captcha_check_json_format ヘルパーメソッドに渡されるブロックでラップし、透過的に処理します:
    1. CAPTCHA が有効かどうかをチェックし、有効なら次のステップに進みます。
    2. モデルにエラーが含まれ、needs_recaptcha フラグが true であるかどうかをチェックします。
      • yesの場合: JSONレスポンスに適切なspamまたはCAPTCHAフィールドを追加し、409 - Conflict HTTPステータスコードを返します。
      • noの場合(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