웹 UI 스팸 방지 및 CAPTCHA 지원

GitLab 애플리케이션의 새로운 UI 영역에 스팸 방지 및 CAPTCHA 지원을 추가하는 접근 방식은 기존 코드가 어떻게 구현되었는지에 따라 달라집니다.

요청 제출 지원 시나리오

세 가지 다른 시나리오가 지원됩니다. 두 가지는 JavaScript XHR/Fetch 요청과 함께 사용되며, 하나는 표준 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 지원을 추가할 수 있습니다. 프론트엔드의 경우, 잠재적으로 별도의 변경이 필요하지 않을 수도 있습니다!

프론트엔드에서는 ApolloLink를 통해 추상적으로 처리되며, Axios의 경우 요청을 Intercept합니다. 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 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: 성공 응답

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

  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인지 확인합니다.
      • 예: 예: CAPTCHA가 비활성화되어 있거나 스팸이 감지되지 않았을 경우 JSON 응답에 적절한 스팸 또는 CAPTCHA 필드를 추가하고 409 - Conflict HTTP 상태 코드를 반환합니다.
      • 아니요 (CAPTCHA가 비활성화되어 있거나 스팸이 감지되지 않았을 경우): 블록 안에 있는 표준 요청 반환 로직이 실행됩니다.

추상화 덕분에 설명하는 것보다 구현하는 것이 더 직관적입니다. 숨겨진 세부 정보에 대해 너무 걱정할 필요가 없습니다!

다음 변경 사항을 수행하세요:

컨트롤러 액션에 지원 추가

기능의 프론트엔드가 컨트롤러 액션에 직접 제출하는 경우 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 폼 요청에 특화된 구현 작업

애플리케이션의 일부 영역은 GraphQL API를 통한 JavaScript 클라이언트 대신 표준 Rails HAML 폼 제출 및 HTML MIME 타입 요청을 사용하도록 변환되지 않았습니다. 해당 영역에서는 액션이 미리 렌더링된 HTML (HAML) 페이지를 응답 본문으로 반환합니다. 유감스럽게도 이 경우에는 위에서 설명한 JavaScript 기반 프론트엔드 지원 중 어떤 것도 사용할 수 없습니다. 대신, CAPTCHA 폼의 렌더링을 HAML 템플릿을 사용하여 처리해야 합니다.

여전히 모든 것은 깔끔하게 추상화되어 있으며, 백엔드 컨트롤러 내의 구현은 사실상 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?
          # 참고: `SpammableActions::AkismetMarkAsSpamAction` 모듈에서 필요한 `spammable_path`
          # 는 이미 위에서의 지침에 따라 이 컨트롤러에 구현되어 있어야 합니다. 중복된 라우트 헬퍼
          # 호출을 피하기 위해 여기서 재사용되었습니다.
          redirect_to spammable_path
        else
          # 여기에 도달했다면 모델 인스턴스에 오류가 있었습니다 - 스팸 확인 실패로부터의 오류 및/또는
          # 모델의 다른 유효성 검사 오류가 있었습니다. 이에 따라 폼을 다시 렌더링하고, CAPTCHA
          # 렌더가 필요한 경우 `with_captcha_check_html_format`에 의해 자동으로 처리될 것입니다.
          with_captcha_check_html_format { render :edit }
        end
      end
    end
  end
end