grpc-webでCookieを使う
By kkoudev
一昨年grpc-webが正式リリースされたこともあり、grpc-webでAPIを作成するケースも少なからず増えてきているのではないかと思います。
それまではHTTP通信でgRPCを扱うケースではgrpc-gatewayを使うことが多かったかと思いますが、grpc-gatewayはRESTのエンドポイントを別途定義する必要があったり、
クライアントコードの自動生成には対応していないということもあり、(swaggerの定義は書き出せるのでやろうと思えばそこから作ることは可能)
せっかくgRPCを使っている割にはあまりメリットを享受しづらいという問題がありました。
その点grpc-webはクライアントコードの自動生成ができるのと、RESTのエンドポイントは不要なのでgRPC本来のメリットを享受しやすいかと思います。
(余談ですが、Envoyを使っていればgrpc-gatewayを使わずとも gRPC-JSON transcoder というFilterを使えばエンドポイントは自動生成できるので、クライアントコード生成を考慮しないのであればこちらもおすすめです)
ちなみにgrpc-webは別実装(improbable-eng/grpc-web)があるのですが、今回の記事では公式のgrpc-web (grpc/grpc-web)を使うケースで紹介します。
Cookieを使う理由
ブラウザからAPI通信を行う場合、セッションIDやアクセストークンといった情報はHttpOnlyの1st party Cookieに持たせることが推奨されているかと思います。
これは特に ITP 2.3 を見据えると、IndexedDBやLocalStorageの保持期間も7日間に制限されてしまうこともあり、
長時間ログインを実現するサイトであれば尚更HttpOnlyな 1st party Cookieを利用するしかブラウザ側にセッションIDやアクセストークンを保持する方法がなくなってきています。
(HttpOnlyな1st party CookieはITP 2.3でも保持期間をサーバーサイドで決められる)
そうなると、gRPCサーバー側でSet-Cookieをする必要が出てくるということになるのですが、
CookieはそもそもHTTPの規格なのでgRPCサーバーでどのように実現するのか、という疑問が浮かんでくるかと思います。
この辺、実装自体はそんなに難しくはないものの、grpc-webのドキュメントにもExampleにもまとめられていないため、
今回利用方法を紹介します。
Set-Cookieを行う (gRPCサーバー側の対応)
grpc-webはEnvoyをProxyサーバーとしてgRPCサーバーとの通信を実現します。
grpc-webを使う際のEnvoyの設定は公式リポジトリにExampleがあるのでそちらを参考にしてみてください。
Set-Cookieをする方法は非常に簡単で、gRPCサーバー側でレスポンスヘッダのMetadataに対して「set-cookie」というキー名でCookieの値を文字列で渡してあげるだけでOKです。
GoのgRPCサーバーを例にすると、以下のようなコードで実現できます。
gRPCサーバーでSet-Cookieする例
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"net/http"
)
//
// SetCookie Cookieを設定する
//
func SetCookie(ctx context.Context, sessionID string) {
// Cookieを作成する
// (7日間の有効期限を例にする)
cookie := http.Cookie{
Name: "sessionID",
Value: sessionID,
Path: "/",
Domain: ".example.com",
MaxAge: 604800,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
// Metadataの作成する
md := make(metadata.MD, 1)
// Cookieを設定する
md.Set("set-cookie", cookie.String())
// レスポンスヘッダに設定する
grpc.SetHeader(ctx, md)
}
サーバーサイドへCookieを送る (クライアント側の対応)
クライアントからCookieを送るには、protocを使って生成されたgrpc-webのクライアントに withCredentials: true
のオプションを設定してあげる必要があります。
例を出すと以下のようなコードになります。
クライアントからサーバへCookieを送信する例
const client = new EchoServiceClient('https://grpc-web.example.com', null, {
withCredentials: true
});
あとは上記コードで作成した client を使って通信処理を行えば Cookie がサーバー側へ送信されます。
クライアントから送信されたCookieの取得 (gRPCサーバー側の対応)
クライアントから送られたCookieをgRPCサーバー側で取得する方法です。
これもMetadataに「cookie」というキー名でCookieが送られるので、それをパースするだけになります。
以下にGoを使った際の例を紹介します。
import (
"context"
"errors"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"net/http"
)
//
// ParseMetadataCookieSessionID メタデータに設定されているCookie情報のセッションIDを取得する
//
func ParseMetadataCookieSessionID(ctx context.Context) (sessionID string, err error) {
// リクエスト情報のMetadataを取得する
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errors.New("cannot get metadata.")
}
// Cookie情報を取得する
vs := md["cookie"]
if len(vs) == 0 {
return nil, errors.New("no cookie.")
}
rawCookie := vs[0]
// Cookieがない場合
if len(rawCookie) == 0 {
// 空を返す
return nil, nil
}
// Cookie情報をパースする
parser := &http.Request{Header: http.Header{"cookie": []string{rawCookie}}}
// 指定された名前のCookieを取得する
cookie, err := parser.Cookie("sessionID")
// エラーの場合
if err != nil {
// エラーを返す
return nil, err
}
// 取得したCookieのセッションIDを返す
return cookie.Value, nil
}
これで送信されたCookieからセッションIDを取り出して、ログインユーザーの識別等が出来るようになります。
まとめ
grpc-webでCookieを扱う例を紹介させていただきました。
これによりRESTful APIを使う場合と同様にCookieを使ったログイン処理等の実現がgRPCでも出来るようになるので、参考になれば幸いです。