Web UI 스팸 방지 및 CAPTCHA 지원

GitLab 애플리케이션의 새 UI 영역에 스팸 방지 및 CAPTCHA 지원을 추가하는 방법은 기존 코드가 구현된 방식에 따라 다릅니다.

지원되는 요청 제출 시나리오

세 가지 다른 시나리오가 지원됩니다. 두 가지는 JavaScript XHR/Fetch 요청과 함께 사용되며 Apollo 또는 Axios 중 하나를 사용하고, 하나는 표준 HTML 폼 요청과 함께만 사용됩니다:

  1. JavaScript 기반 제출 (Vue를 통해 가능)
    1. Apollo 사용 (Fetch/XHR 요청을 통한 GraphQL API)
    2. Axios 사용 (Fetch/XHR 요청을 통한 REST API)
  2. 표준 HTML 폼 제출 (HTML 요청)

구현의 일부는 지원해야 하는 시나리오에 따라 다릅니다.

JavaScript XHR/Fetch 요청에 특화된 구현 작업

두 가지 접근 방식이 완전히 지원됩니다:

  1. GraphQL API를 사용한 Apollo.
  2. GraphQL API를 사용한 Axios.

프런트엔드와 백엔드 간의 스팸 및 CAPTCHA 관련 데이터 통신에는 모델에 추가 필드를 추가하지 않고도 처리하는 데 필요한 접근 방식은 다음과 같습니다:

  • 요청의 사용자 지정 헤더 값 활용
  • 응답의 최상위 JSON 필드 활용

스팸 및 CAPTCHA 관련 로직은 재사용 가능한 모듈과 도우미 메서드로 깔끔하게 추상화되어 있어 기존 로직을 수정하지 않고 새로운 애플리케이션 영역에 스팸 및 CAPTCHA 지원을 추가할 수 있습니다. 프런트엔드의 경우에는 전혀 변경이 필요 없을 수도 있습니다!

프런트엔드에서는 Apollo의 경우 ApolloLink, Axios의 경우 Axios 인터셉터를 사용하여 추상적이고 투명하게 처리됩니다. CAPTCHA 표시는 표준 GitLab UI / Pajamas 모달 구성 요소를 통해 처리됩니다. 관련 프런트엔드 코드는 app/assets/javascripts/captcha에서 찾을 수 있습니다.

그러나 실질적인 요청 가로채기와 모달 처리는 투명하지만, 폼 또는 페이지의 관련 자바스크립트 또는 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: 모델 저장 V->>A: 요청 A->>G: 요청 G--xA: 오류 및 스팸/CAPTCHA 관련 필드가 포함된 응답 A->>U: 모달에서 CAPTCHA 표시 U->>A: 유효한 CAPTCHA 응답 획들을 위해 CAPTCHA 해결 A->>G: 유효한 CAPTCHA 응답 및 헤더의 SpamLog ID를 포함한 요청 G-->>A: 성공 응답 A-->>V: 성공 응답

백엔드도 모듈과 도우미 메서드를 통해 깔끔하게 추상화되어 있습니다. 관련 백엔드 컨트롤러 작업에 필요한 세 가지 주요 변경은 다음과 같습니다(일반적으로 create/update임):

  1. Update 서비스 클래스 생성자에 perform_spam_check: true를 전달합니다. Create 서비스 클래스에서는 기본적으로 true로 설정됩니다.
  2. 스팸 확인이 모델의 변경이 가능성이 있는 것으로 나타나면:
    • 모델에 오류가 추가됩니다.
    • 모델의 needs_recaptcha 속성이 true로 설정됩니다.
  3. 존재하는 컨트롤러 액션 반환 값(렌더링 또는 리디렉션)을 #with_captcha_check_json_format 도우미 메서드에 전달된 블록으로 래핑합니다. 이 도우미 메서드는 다음을 투명하게 처리합니다:
    1. CAPTCHA가 활성화되어 있는지 확인하고, 그렇다면 다음 단계를 계속합니다.
    2. 모델에 오류가 포함되어 있고 needs_recaptcha 플래그가 true인지 확인합니다.
      • 예: JSON 응답에 적절한 스팸 또는 CAPTCHA 필드를 추가하고 409 - Conflict HTTP 상태 코드를 반환합니다.
      • 아니요( CAPTCHA가 비활성화되어 있거나 스팸이 감지되지 않은 경우): 블록 내에서 실행된 표준 요청 반환 로직을 실행합니다.

추상화 덕분에 설명하는 것보다 구현하는 것이 더 간단합니다. 숨겨진 세부 정보에 대해 걱정할 필요가 없습니다!

다음 변경 사항을 적용하세요:

컨트롤러 액션에 지원 추가

기능의 프런트엔드가 컨트롤러 액션에 직접 제출하고 GraphQL API만 사용하는 것이 아니라면, 적절한 컨트롤러에 지원을 추가해야 합니다.

액션 메서드는 컨트롤러 클래스에 직접 있을 수 있고, 또는 컨트롤러 클래스에 포함된 모듈에서 추상화될 수 있습니다. 여기 예시에서는 모듈을 사용합니다. 컨트롤러를 직접 수정하는 경우에만 다음과 같이 다릅니다: extend ActiveSupport::Concern가 모든 include 문 전에 발생하도록 이동해야 하며, 그렇지 않으면 포함된 모듈의 메서드를 찾을 수 없는 혼란스러운 버그가 발생할 수 있습니다.

module WidgetsActions
  # 참고: 이미 존재할 수 있는 이 `extend`, 그러나 모든 `include` 문 전에 발생해야 합니다. 그렇지 않으면 포함된 모듈에 있는 메서드가 찾아지지 않거나 혼란스러운 버그가 발생할 수 있습니다.
  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
          # 액션에 대한 기존 `render json: ...`(또는 래퍼 메서드) 및 관련 로직. 모델이 유효한지 여부에 따라 다른 렌더링이 있을 수 있습니다. 이 모든 것이 여기 `with_captcha_check_json_format` 블록 안으로 래핑됩니다. 예를 들어:
          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 사용 대신 표준 Rails HAML 폼 제출을 활용하고 있어 HTML MIME 타입 요청을 통해 사전 렌더링된 HTML (HAML) 페이지를 응답 본문으로 반환합니다. 불행하게도, 이 경우 위의 설명한 자바스크립트 기반 프런트엔드 지원을 사용하는 것은 불가능합니다. 대신 HAML 템플릿을 통해 CAPTCHA 폼을 렌더링하는 대체 접근 방식을 사용해야 합니다.

여전히 모든 것이 깔끔하게 추상화되어 있고, 백엔드 컨트롤러에서의 구현은 거의 JavaScript/JSON 기반 접근 방식과 동일합니다. 모듈명과 도우미 메서드에 있는 JSONHTML로 대체하면 됩니다(알맞은 경우에).

액션 메서드는 컨트롤러에 직접 있을 수도 있고, 모듈에 추상화될 수도 있습니다. 이 예시에서는 컨트롤러에 직접 있으며, create 대신 update 메서드를 사용합니다:

class WidgetsController < ApplicationController
  include SpammableActions::CaptchaCheck::HtmlFormatActionsSupport

  def update
    # `widget` 모델 인스턴스를 찾기 위한 기존 로직...
    ::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?
          # 참고: `spammable_path`는 `SpammableActions::AkismetMarkAsSpamAction` 모듈에서 필요하며, 위의 지침에 따라 이미 구현되어 있어야 합니다. 이를 중복되는 라우트 헬퍼 호출을 피하기 위해 여기서 재사용합니다.
          redirect_to spammable_path
        else
          # 여기에 도달했다면 모델 인스턴스에 오류가 있었음 - 스팸 확인 실패나 기타 유효성 검사 오류 중 하나에서. 어쨌든, 폼을 다시 렌더링할 것이고, CAPTCHA 렌더가 필요하다면 `with_captcha_check_html_format`에서 자동으로 처리될 것입니다.
          with_captcha_check_html_format { render :edit }
        end
      end
    end
  end
end