Approuter 잘못 설정하면 큰일 — xs-app.json 라우트 구성 #shorts #SAP #CAP

Moderator · 조회 3

TL;DR — 5분 안에 알아둘 것

SAP BTP에서 CAP for Node.js 애플리케이션을 운영할 때, 사용자 요청은 항상 Approuter를 거쳐 백엔드 서비스로 전달됩니다. Approuter는 단순한 리버스 프록시가 아니라 BTP의 XSUAA와 연동되어 인증/인가의 첫 관문 역할을 수행합니다. 이 글에서는 xs-app.json의 라우트 구성을 기준으로, 잘못된 설정이 어떻게 인증을 통째로 뚫리게 하는지, 그리고 안전한 운영을 위한 실전 패턴을 정리합니다.

사전 가정

이 글은 다음 내용에 익숙한 독자를 가정합니다.

테스트 환경

아래 버전을 기준으로 작성되었습니다. 마이너 버전은 다를 수 있으나 동작에는 큰 영향이 없습니다.

설치 명령은 일반적으로 다음과 같습니다.

npm i -g @sap/cds-dk mbt
cf install-plugin multiapps
cds add approuter,xsuaa,mta --for production

핵심 개념 — Approuter는 왜 필요한가

CAP 백엔드(srv)는 비즈니스 로직과 OData 엔드포인트를 제공하지만, 브라우저에서 직접 접근하면 다음 문제가 생깁니다. 첫째, OAuth2 흐름(로그인 리다이렉트, 콜백, 토큰 저장)을 누군가 처리해야 합니다. 둘째, UI5 정적 리소스와 API가 서로 다른 도메인이면 CORS와 CSRF 처리가 복잡해집니다. 셋째, 백엔드는 JWT만 검증해야 하는데 세션 쿠키까지 다루면 책임이 섞입니다.

Approuter는 이 세 가지를 한꺼번에 해결합니다. 사용자는 Approuter로만 들어오고, Approuter가 XSUAA로 OAuth2 인증 코드 그랜트를 수행한 뒤 JWT를 발급받아 백엔드로 전달합니다. 비유하자면 "빌딩 1층 보안 데스크"이고, CAP 서비스는 보안 카드를 가진 사람만 입장 가능한 사무실입니다. 이 데스크 설정이 곧 xs-app.json입니다.

요청 흐름은 일반적으로 다음과 같습니다.

[Browser] -> [Approuter (xs-app.json 매칭)] -> [XSUAA: OAuth2]
                                          -> [CAP srv (JWT 검증)]
                                          -> [Destination -> S/4HANA, etc.]

여기서 핵심은 매칭 우선순위입니다. routes 배열은 위에서 아래로 평가되며, 가장 먼저 매칭되는 규칙이 적용됩니다. 따라서 "source": "^/(.*)$" 같은 와일드카드를 위쪽에 두고 그 아래에 authenticationType: none을 잘못 두면, 모든 경로가 인증 없이 통과되는 사고가 발생합니다.

실전 코드 3단계

1단계: 기본 예제 — 최소 동작 가능한 xs-app.json

CAP 프로젝트 루트에서 cds add approuter를 실행하면 app/router/ 폴더가 생성됩니다. 가장 단순한 형태는 다음과 같습니다.

{
  "welcomeFile": "/index.html",
  "authenticationMethod": "route",
  "sessionTimeout": 30,
  "routes": [
    {
      "source": "^/odata/v4/(.*)$",
      "target": "/odata/v4/$1",
      "destination": "srv-api",
      "authenticationType": "xsuaa",
      "csrfProtection": true
    },
    {
      "source": "^/(.*)$",
      "target": "/$1",
      "localDir": "resources",
      "authenticationType": "xsuaa"
    }
  ]
}

app/router/package.json은 다음과 같습니다.

{
  "name": "approuter",
  "dependencies": {
    "@sap/approuter": "^14"
  },
  "scripts": {
    "start": "node node_modules/@sap/approuter/approuter.js"
  }
}

여기서 주목할 부분은 authenticationMethod: "route"입니다. 이는 라우트별로 인증 여부를 결정하겠다는 의미이며, 누락 시 기본값이 route지만 명시하는 편이 안전합니다.

2단계: 실무 시나리오 — 다중 백엔드, 스코프, 로깅

실제 프로젝트에서는 CAP 서비스 외에 외부 API, UI5 앱, 헬스체크 엔드포인트가 공존합니다. 다음 예시는 스코프 기반 인가와 destination 분기를 함께 보여줍니다.

{
  "welcomeFile": "/cockpit/index.html",
  "authenticationMethod": "route",
  "logout": {
    "logoutEndpoint": "/do/logout",
    "logoutPage": "/logout-success.html"
  },
  "routes": [
    {
      "source": "^/health$",
      "target": "/health",
      "destination": "srv-api",
      "authenticationType": "none"
    },
    {
      "source": "^/admin/(.*)$",
      "target": "/admin/$1",
      "destination": "srv-api",
      "authenticationType": "xsuaa",
      "scope": {
        "GET": "$XSAPPNAME.Admin",
        "default": "$XSAPPNAME.Admin"
      },
      "csrfProtection": true
    },
    {
      "source": "^/odata/v4/(.*)$",
      "target": "/odata/v4/$1",
      "destination": "srv-api",
      "authenticationType": "xsuaa",
      "scope": "$XSAPPNAME.User",
      "csrfProtection": true
    },
    {
      "source": "^/cockpit/(.*)$",
      "target": "/$1",
      "localDir": "resources/cockpit",
      "authenticationType": "xsuaa",
      "cacheControl": "no-cache, no-store, must-revalidate"
    },
    {
      "source": "^/logout-success.html$",
      "localDir": "resources/public",
      "authenticationType": "none"
    }
  ]
}

로깅을 강화하려면 환경 변수 XS_APP_LOG_LEVEL=debug를 설정하고, 인가 실패 추적을 위해 SAP_JWT_TRUST_ACL을 명시적으로 관리하는 편이 권장됩니다. /healthauthenticationType: none으로 두되, 반드시 백엔드에서도 해당 경로가 민감 데이터를 반환하지 않도록 점검해야 합니다.

3단계: 프로덕션 — MTA 연결, 보안 헤더, 테스트

BTP 배포에서는 mta.yaml이 destination과 서비스 바인딩을 책임집니다. Approuter 모듈은 일반적으로 다음과 같이 구성됩니다.

_schema-version: "3.3"
ID: bookshop
version: 1.0.0
modules:
  - name: bookshop-srv
    type: nodejs
    path: gen/srv
    requires:
      - name: bookshop-uaa
      - name: bookshop-db
    provides:
      - name: srv-api
        properties:
          srv-url: ${default-url}

  - name: bookshop-approuter
    type: approuter.nodejs
    path: app/router
    parameters:
      keep-existing-routes: true
    requires:
      - name: srv-api
        group: destinations
        properties:
          name: srv-api
          url: ~{srv-url}
          forwardAuthToken: true
      - name: bookshop-uaa

resources:
  - name: bookshop-uaa
    type: org.cloudfoundry.managed-service
    parameters:
      service: xsuaa
      service-plan: application
      path: ./xs-security.json

forwardAuthToken: true가 빠지면 백엔드가 JWT를 받지 못해 401이 발생합니다. 또한 xs-security.json에서 정의한 스코프 이름과 xs-app.jsonscope 값이 정확히 일치해야 합니다.

보안 헤더는 Approuter 14에서 기본 제공되지만, 커스텀 헤더는 httpHeaders 환경 변수로 추가합니다.

{
  "httpHeaders": "[{\"Strict-Transport-Security\": \"max-age=31536000; includeSubDomains\"}, {\"Content-Security-Policy\": \"default-src 'self'\"}]"
}

라우트 회귀 테스트는 supertest 또는 BTP의 cf curl로 수행할 수 있습니다. 인증 우회 여부를 점검하려면 토큰 없이 보호 경로에 접근해 401이 반환되는지 확인합니다.

// test/approuter.test.js
const request = require('supertest');
const APP = process.env.APPROUTER_URL;

test('보호 경로는 토큰 없으면 302 또는 401', async () => {
  const res = await request(APP).get('/odata/v4/CatalogService/Books').redirects(0);
  expect([302, 401]).toContain(res.status);
});

test('public 경로는 200', async () => {
  const res = await request(APP).get('/health');
  expect(res.status).toBe(200);
});

흔한 실수 / 트러블슈팅

Approuter 관련 사고는 대부분 라우트 순서destination 누락에서 발생합니다. 다음 FAQ는 실무에서 반복적으로 마주치는 사례입니다.

Q1. 와일드카드 라우트 위에 public 라우트를 두면 안 되나요?

반대로 두어야 합니다. 구체적인 라우트(예: ^/health$)를 먼저, 와일드카드(^/(.*)$)를 마지막에 둡니다. 만약 첫 번째 라우트가 ^/(.*)$이면서 authenticationType: none이라면 모든 경로가 인증 없이 통과됩니다. 이는 SAP Discovery Center나 보안 가이드에서 가장 자주 언급되는 안티패턴입니다.

Q2. 로컬에서는 동작하는데 BTP에서 401이 납니다.

대부분 forwardAuthToken: true 누락 또는 xs-security.jsonxsappname 충돌이 원인입니다. 로컬 default-env.json은 동일 환경 변수를 자동 주입하지만, MTA 배포 시에는 mta.yamlrequires 블록이 명시되어야 합니다. cf env <app>으로 VCAP_SERVICES를 확인해 XSUAA 바인딩이 정상인지 점검하세요.

Q3. CSRF 토큰 오류(403)가 자꾸 발생합니다.

CAP 서비스에 POST/PUT/DELETE를 보낼 때는 csrfProtection: true가 필요하고, 클라이언트는 먼저 x-csrf-token: fetch로 토큰을 받아 후속 요청에 포함해야 합니다. UI5 모델은 자동 처리하지만, 직접 fetch를 호출할 때는 누락되기 쉽습니다. 또한 destination 응답이 일반적으로 x-csrf-token 헤더를 통과시키는지 확인하세요.

Q4. 로그인 후 무한 리다이렉트가 발생합니다.

일반적으로 sessionTimeout이 너무 짧거나, 콜백 URL이 XSUAA의 oauth2-configuration.redirect-uris에 등록되지 않은 경우입니다. xs-security.json에 배포된 라우트 도메인을 https://*.<cf-domain>/** 형식으로 추가하는 편이 권장됩니다.

더 파볼 주제

Approuter 라우트 설계가 익숙해졌다면 다음 주제로 확장하는 편이 권장됩니다.

더 읽어볼 자료

SAP, CAP, SAP BTP는 독일 및 기타 국가에서 SAP SE의 상표 또는 등록상표입니다.

본 게시글은 btpstacks.com의 독립 학습 콘텐츠이며 SAP SE와 무관합니다.