웹 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. Apollo를 사용하는 경우 GraphQL API를 사용합니다.
  2. Axios를 사용하는 경우 또한 GraphQL API를 사용합니다.

프론트엔드와 백엔드 간의 스팸 및 CAPTCHA 관련 데이터 통신에는 모델에 추가 필드를 추가하는 대신 통신이 처리됩니다:

  • 요청에서 사용자 정의 헤더 값으로
  • 응답의 최상위 JSON 필드로

스팸 및 CAPTCHA 관련 로직은 재사용 가능한 모듈 및 도우미 메서드로 깔끔하게 추상화되어 있으며 잠재적인 스팸이 감지되거나 CAPTCHA 표시가 필요한 경우에만 기존 흐름을 변경합니다. 이 접근 방식을 사용하면 새로운 응용 프로그램 영역에 스팸 및 CAPTCHA 지원을 추가할 때 기존 로직을 최소한으로 변경할 수 있습니다. 프론트엔드의 경우 잠재적으로 변경을 요구하지 않을 수도 있습니다.

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

그러나 실제 요청 가로채기 및 모달 처리는 투명하지만, 폼이나 페이지의 관련 JavaScript 또는 Vue 컴포넌트에 의무적으로 변경 사항이 없더라도 요청 또는 오류 처리에 변경 사항이 필요할 수 있습니다. 기존 동작이 올바르게 작동하지 않을 수 있기 때문에 변경 사항이 필요합니다. 예를 들어, 실패한 또는 취소된 CAPTCHA 표시가 표준 요청 흐름이나 UI 업데이트를 중단하는 경우 등입니다. 모든 시나리오의 신중한 탐사 테스트는 잠재적인 문제를 발견하는 데 중요합니다.

다음 순서도는 프론트엔드의 JavaScript XHR/Fetch 요청에 대한 표준 CAPTCHA 흐름을 보여줍니다:

sequenceDiagram participant U as 사용자 participant V as Vue/JS 애플리케이션 participant A as ApolloLink 또는 Axios 인터셉터 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: 성공으로 응답

백엔드도 mixin 모듈 및 도우미 메서드를 통해 명확하게 추상화됩니다. 관련 백엔드 컨트롤러 액션(일반적으로 create/update 중 하나)에 대한 세 가지 주요 변경 사항은 다음과 같습니다:

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

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

다음 변경 사항을 수행합니다:

컨트롤러 액션에 지원 추가

기능의 프론트엔드가 GraphQL API만 사용하는 것이 아니라 직접 컨트롤러 액션으로 제출하고 있고 경우에 따라 GraphQL API만 사용하는 경우 지원을 추가해야 합니다.

액션 메서드는 컨트롤러 클래스에 직접 있을 수도 있고 컨트롤러 클래스에 포함된 모듈에 추상화될 수도 있습니다. 이 예시는 모듈을 사용합니다. 컨트롤러를 직접 수정하는 경우에만 차이점은 다음과 같습니다: extend ActiveSupport::Concern가 필요하지 않습니다.

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) 페이지를 응답 본문으로 반환합니다. 이러한 경우에는 앞서 설명한 JavaScript 기반 프론트엔드 지원을 사용하는 것이 불가능합니다. 대신 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