웹 푸시 상호 운용성의 이점

맷 곤트
조 메들리
조 메들리

Chrome에서 처음 Web Push API를 지원했을 때는 Firebase 클라우드 메시징(FCM)(이전 명칭: Google Cloud Messaging(GCM))에 의존했습니다. 이를 위해서는 독점 API를 사용해야 했습니다. 이를 통해 Chrome은 웹 푸시 프로토콜 사양이 아직 작성 중인 상황에서 개발자에게 웹 푸시 API를 제공하고 나중에 웹 푸시 프로토콜에 인증 (즉, 메시지 발신자)을 제공할 수 있었습니다. 좋은 소식은 이 중 어느 것도 더 이상 사실이 아닙니다.

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

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

FCM에서 웹 푸시 프로토콜을 지원합니다.

약간의 맥락으로 시작하겠습니다. 웹 애플리케이션이 푸시 구독에 등록되면 푸시 서비스의 URL이 제공됩니다. 서버는 이 엔드포인트를 사용하여 웹 앱을 통해 사용자에게 데이터를 전송합니다. Chrome에서는 VAPID가 없는 사용자를 구독하면 FCM 엔드포인트가 제공됩니다. VAPID는 나중에 다룰 예정입니다. FCM이 웹 푸시 프로토콜을 지원하기 전에는 URL 끝에서 FCM 등록 ID를 추출하여 헤더에 삽입해야 FCM API 요청을 할 수 있었습니다. 예를 들어 FCM 엔드포인트 https://android.googleapis.com/gcm/send/ABCD1234는 등록 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).

방법은 웹 푸시 노드 라이브러리에서 확인할 수 있습니다.

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 헤더(승인 헤더 및 암호화 키 헤더)를 사용하여 일반 웹 푸시 프로토콜 요청을 해야 합니다.

승인 헤더

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

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

<JWTHeader>.<Payload>.<Signature>

JWT 헤더

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

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

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

페이로드

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

  • 잠재고객 ('aud')
    • 푸시 서비스의 출처입니다 (사이트의 출처가 아님). 자바스크립트에서는 다음과 같은 방법으로 잠재고객을 가져올 수 있습니다. const audience = new URL(subscription.endpoint).origin
  • 만료 시간 ('exp')
    • 요청이 만료된 것으로 간주되어야 하는 시점까지 남은 시간(초)입니다. 이 시간은 반드시 요청 후 24시간 이내(UTC 기준)여야 합니다.
  • 제목 ('sub')
    • 제목은 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와 삼성 브라우저용 Opera를 계속 웹 앱 매니페스트에서 gcm_sender_id를 정의해야 하며 반환되는 FCM 엔드포인트에 승인 헤더를 추가해야 합니다.

VAPID는 이러한 독점적 요구사항에서 벗어나는 진입로를 제공합니다. VAPID를 구현하면 웹 푸시를 지원하는 모든 브라우저에서 작동합니다. 점점 더 많은 브라우저가 VAPID를 지원하므로 매니페스트에서 gcm_sender_id를 삭제할 시점을 결정할 수 있습니다.