웹 푸시 상호 운용성의 이점

Joe Medley
Joe Medley

Chrome이 처음 Web Push API를 지원했을 때는 FCM (이전 명칭: Google 클라우드 메시징 (GCM)) 푸시 서비스를 사용했습니다. 이를 위해서는 자체적인 API를 사용해야 했습니다. 이를 통해 Chrome은 웹 푸시 프로토콜 사양이 아직 작성 중일 때 개발자에게 웹 푸시 API를 제공하고 나중에 웹 푸시 프로토콜에 사양이 없을 때 인증 (메시지 발신자는 메시지 발신자)을 제공할 수 있었습니다. 좋은 소식은 둘 다 더 이상 사실이 아닙니다.

FCM / GCM, Chrome은 이제 표준 웹 푸시 프로토콜을 지원하지만 발신자 인증은 VAPID를 구현하여 실행할 수 있습니다. 즉, 웹 앱에 더 이상 'gcm_sender_id'가 필요하지 않습니다.

이 도움말에서는 먼저 FCM과 함께 웹 푸시 프로토콜을 사용하도록 기존 서버 코드를 변환하는 방법을 설명합니다. 다음으로 클라이언트 코드와 서버 코드 모두에서 VAPID를 구현하는 방법을 보여드리겠습니다.

FCM에서 웹 푸시 프로토콜 지원

먼저 약간의 맥락을 살펴보겠습니다. 웹 애플리케이션이 푸시 구독에 등록하면 푸시 서비스의 URL이 제공됩니다. 서버는 이 엔드포인트를 사용하여 웹 앱을 통해 사용자에게 데이터를 전송합니다. Chrome에서는 VAPID 없이 사용자를 구독하면 FCM 엔드포인트가 제공됩니다. VAPID에 대해서는 나중에 다룹니다. FCM에서 웹 푸시 프로토콜을 지원하기 전에는 FCM API 요청을 하기 전에 URL 끝에서 FCM 등록 ID를 추출하여 헤더에 넣어야 했습니다. 예를 들어 https://android.googleapis.com/gcm/send/ABCD1234의 FCM 엔드포인트의 등록 ID는 'ABCD1234'입니다.

이제 FCM에서 웹 푸시 프로토콜을 지원하므로 엔드포인트를 그대로 두고 URL을 웹 푸시 프로토콜 엔드포인트로 사용할 수 있습니다. (이것은 Firefox는 물론, 앞으로의 모든 브라우저와 호환될 것입니다.)

VAPID를 자세히 살펴보기 전에 서버 코드가 FCM 엔드포인트를 올바르게 처리하는지 확인해야 합니다. 다음은 노드의 푸시 서비스에 요청하는 예입니다. FCM의 경우 요청 헤더에 API 키가 추가됩니다. 다른 푸시 서비스 엔드포인트의 경우에는 필요하지 않습니다. 버전 52 이전의 Chrome, Opera Android, 삼성 브라우저의 경우에도 웹 앱의 manifest.json에 'gcm_sender_id'를 포함해야 합니다. API 키와 발신자 ID는 요청을 보내는 서버가 실제로 수신 사용자에게 메시지를 보낼 수 있는지 확인하는 데 사용됩니다.

const headers = new Headers();
// 12-hour notification time to live.
headers.append('TTL', 12 * 60 * 60);
// Assuming no data is going to be sent
headers.append('Content-Length', 0);

// Assuming you're not using VAPID (read on), this
// proprietary header is needed
if(subscription.endpoint
    .indexOf('https://android.googleapis.com/gcm/send/') === 0) {
    headers.append('Authorization', 'GCM_API_KEY');
}

fetch(subscription.endpoint, {
    method: 'POST',
    headers: headers
})
.then(response => {
    if (response.status !== 201) {
    throw new Error('Unable to send push message');
    }
});

FCM / GCM의 API가 변경되었으므로 구독을 업데이트할 필요가 없습니다. 서버 코드를 변경하여 위와 같이 헤더를 정의하기만 하면 됩니다.

서버 식별을 위한 VAPID 소개

VAPID는 '자발적 애플리케이션 서버 식별'의 멋진 새 별칭입니다. 이 새로운 사양은 기본적으로 앱 서버와 푸시 서비스 간의 핸드셰이크를 정의하고 푸시 서비스에서 어느 사이트가 메시지를 전송하는지 확인할 수 있도록 합니다. VAPID를 사용하면 푸시 메시지를 보내기 위한 FCM 전용 단계를 피할 수 있습니다. Firebase 프로젝트, gcm_sender_id 또는 Authorization 헤더가 더 이상 필요하지 않습니다.

프로세스는 매우 간단합니다.

  1. 애플리케이션 서버가 공개 키/비공개 키 쌍을 생성합니다. 공개 키는 웹 앱에 제공됩니다.
  2. 사용자가 푸시를 수신하기로 선택하면 공개 키를 subscription() 호출의 옵션 객체에 추가합니다.
  3. 앱 서버가 푸시 메시지를 보낼 때 공개 키와 함께 서명된 JSON 웹 토큰을 포함합니다.

각 단계를 자세히 살펴보겠습니다.

공개 키/비공개 키 쌍 만들기

저는 암호화를 매우 잘 안합니다. 따라서 VAPID 공개/비공개 키 형식에 관한 사양의 관련 섹션은 다음과 같습니다.

애플리케이션 서버는 P-256 곡선을 통해 타원 곡선 디지털 서명(ECDSA)과 함께 사용할 수 있는 서명 키 쌍을 생성하고 유지해야 합니다(SHOULD).

web-push 노드 라이브러리에서 이 방법을 확인할 수 있습니다.

function generateVAPIDKeys() {
    var curve = crypto.createECDH('prime256v1');
    curve.generateKeys();

    return {
    publicKey: curve.getPublicKey(),
    privateKey: curve.getPrivateKey(),
    };
}

공개 키로 구독

VAPID 공개 키로 Chrome 사용자의 푸시를 구독하려면 subscription() 메서드의 applicationServerKey 매개변수를 사용하여 공개 키를 Uint8Array로 전달해야 합니다.

const publicKey = new Uint8Array([0x4, 0x37, 0x77, 0xfe, …. ]);
serviceWorkerRegistration.pushManager.subscribe(
    {
    userVisibleOnly: true,
    applicationServerKey: publicKey
    }
);

결과 구독 객체의 엔드포인트를 검사하여 작동 여부를 알 수 있고 출처가 fcm.googleapis.com이면 작동하는 것입니다.

https://fcm.googleapis.com/fcm/send/ABCD1234

푸시 메시지 보내기

VAPID를 사용하여 메시지를 보내려면 두 개의 추가 HTTP 헤더, 즉 승인 헤더와 Crypto-Key 헤더로 일반적인 웹 푸시 프로토콜 요청을 해야 합니다.

승인 헤더

Authorization 헤더는 앞에 'WebPush '가 있는 서명된 JSON 웹 토큰 (JWT)입니다.

JWT는 JSON 객체를 두 번째 당사자와 공유하는 방법으로, 보내는 사람이 여기에 서명할 수 있고 수신 당사자는 예상 발신자가 보낸 사람인지 확인할 수 있습니다. JWT의 구조는 암호화된 세 개의 문자열로, 그 사이에 하나의 점으로 결합되어 있습니다.

<JWTHeader>.<Payload>.<Signature>

JWT 헤더

JWT 헤더에는 서명에 사용되는 알고리즘 이름과 토큰 유형이 포함됩니다. VAPID의 경우 다음과 같아야 합니다.

{
    "typ": "JWT",
    "alg": "ES256"
}

그런 다음 base64 URL로 인코딩되어 JWT의 첫 번째 부분이 형성됩니다.

페이로드

페이로드는 다음을 포함하는 또 다른 JSON 객체입니다.

  • 잠재고객 ('aud')
    • 사이트의 출처가 아닌 푸시 서비스의 출처입니다. JavaScript에서는 const audience = new URL(subscription.endpoint).origin를 이용해 잠재고객을 가져올 수 있습니다.
  • 만료 시간 ('exp')
    • 요청이 만료된 것으로 간주되어야 할 때까지의 시간(초)입니다. 요청 시점으로부터 24시간 이내여야 합니다(UTC 기준).
  • 제목 ('하위')
    • 제목은 URL 또는 mailto: URL이어야 합니다. 이는 푸시 서비스가 메시지 발신자에게 연락해야 하는 경우에 대비해 연락처를 제공합니다.

페이로드의 예는 다음과 같습니다.

{
    "aud": "http://push-service.example.com",
    "exp": Math.floor((Date.now() / 1000) + (12 * 60 * 60)),
    "sub": "mailto: my-email@some-url.com"
}

이 JSON 객체는 base64 URL로 인코딩되며 JWT의 두 번째 부분을 구성합니다.

서명

서명은 인코딩된 헤더와 페이로드를 점과 조인한 다음 앞서 만든 VAPID 비공개 키를 사용하여 결과를 암호화한 결과입니다. 결과 자체는 헤더에 마침표로 추가되어야 합니다.

헤더와 페이로드 JSON 객체를 가져와 이 서명을 생성하는 여러 라이브러리가 있으므로 이에 대한 코드 샘플은 보여주지 않겠습니다.

서명된 JWT는 앞에 'WebPush '가 추가된 승인 헤더로 사용되며 다음과 유사합니다.

WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTQ2NjY2ODU5NCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlbW9AZ2F1bnRmYWNlLmNvLnVrIn0.Ec0VR8dtf5qb8Fb5Wk91br-evfho9sZT6jBRuQwxVMFyK5S8bhOjk8kuxvilLqTBmDXJM5l3uVrVOQirSsjq0A

이와 관련해 몇 가지 사항을 주목하세요. 첫째, 승인 헤더에는 문자 그대로 'WebPush'라는 단어가 포함되어 있고 그 뒤에 공백과 JWT가 와야 합니다. 또한 JWT 헤더, 페이로드, 서명을 구분하는 점이 표시됩니다.

Crypto-Key 헤더

승인 헤더뿐만 아니라 VAPID 공개 키를 Crypto-Key 헤더에 앞에 p256ecdsa=가 추가된 base64 URL 인코딩 문자열로 추가해야 합니다.

p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaNndIo

암호화된 데이터가 포함된 알림을 보내는 경우 이미 Crypto-Key 헤더를 사용하고 있으므로 애플리케이션 서버 키를 추가하려면 위의 콘텐츠를 추가하기 전에 세미콜론을 추가하기만 하면 다음과 같은 결과가 발생합니다.

dh=BGEw2wsHgLwzerjvnMTkbKrFRxdmwJ5S_k7zi7A1coR_sVjHmGrlvzYpAT1n4NPbioFlQkIrTNL8EH4V3ZZ4vJE;
p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaN

이러한 변화의 현실

VAPID를 사용하면 Chrome에서 푸시를 사용하기 위해 더 이상 GCM 계정에 가입할 필요가 없으며, Chrome과 Firefox 모두에서 동일한 코드 경로를 사용하여 사용자를 구독하고 사용자에게 메시지를 보낼 수 있습니다. 둘 다 표준을 따릅니다.

단, Chrome 51 이하에서는 Android용 Opera 및 삼성 브라우저에서 웹 앱 매니페스트에 gcm_sender_id를 정의해야 하며 반환되는 FCM 엔드포인트에 승인 헤더를 추가해야 합니다.

VAPID는 이러한 독점적인 요구사항에서 벗어나는 단계적 확장을 제공합니다. VAPID를 구현하면 웹 푸시를 지원하는 모든 브라우저에서 작동합니다. VAPID를 지원하는 브라우저가 늘어남에 따라 매니페스트에서 gcm_sender_id를 삭제할 시점을 결정할 수 있습니다.