Extend アプリで CORS を設定する
注釈:本資料はAI技術を用いて翻訳されています。
Overview
別のドメイン上のブラウザベースの Web アプリケーション(例: https://mygame.com)から Extend の REST エンドポイントを呼び出す必要がある場合は、Cross-Origin Resource Sharing(CORS) を有効にします。
Extend のサンプルアプリには CORS サポートが組み込まれていますが、デフォルトでは無効になっています。有効化と設定はすべて環境変数で行い、コードの変更やリビルドは不要です。CORS が無効な間は、アプリの動作は従来どおりで、CORS ヘッダーは追加されません。
gRPC ゲートウェイを通じて REST エンドポイントを公開する Extend アプリが対象です。具体的には、Extend Service Extension と REST API サポートを備えた Extend Event Handler が該当します。
CORS を強制するのはブラウザであり、アプリ自身ではありません。影響を受けるのは、別のオリジン(スキーム + ホスト + ポート)の Web ページから送信される JavaScript のリクエストのみです。サーバー間通信やブラウザ以外のクライアントには関係しません。
このページで扱うのは自分の Extend アプリの CORS です。これは、AGS Admin Portal の Foundations > Tools and Utilities にあるネームスペースレベルの Allowed Origins (CORS) 設定とは別のものです。後者は AGS プラットフォームのエンドポイントへのクロスオリジンアクセスを制御します。一方を設定しても、もう一方には影響しません。
Before you begin
-
デプロイ済みの Extend Service Extension、または REST API サポートを備えた Extend Event Handler。
-
アプリに組み込みの CORS ミドルウェアが含まれていること。確認するには、アプリのソースコード内に次のいずれかのファイルがあるか探します。
pkg/common/cors.go— Go のサンプルアプリ。gateway/pkg/common/cors.go— C#、Java、Python のサンプルアプリ、および REST API サポートを備えた Event Handler。
どちらのファイルも存在しない場合、そのアプリは組み込みミドルウェアが追加される前のものです。代わりに CORS ミドルウェアがアプリに含まれていない場合 に従ってください。
-
AGS Admin Portal でアプリの Environment Variable Configuration にアクセスできること。
-
アプリを呼び出す正確なオリジン。
https://mygame.comとhttps://admin.mygame.comは別のオリジンです。
Step 1: Set the CORS environment variables
次の項目を Extend アプリの Environment Variable Configuration エントリとして追加します。Admin Portal での手順については、Extend アプリの環境変数とシークレット を参照してください。
| キー | 値 |
|---|---|
CORS_ENABLED | true |
CORS_ALLOWED_ORIGINS | https://mygame.com |
一般的な GET および POST リクエストであれば、これだけで十分です。その他の設定にはすべて妥当なデフォルト値が適用されます。詳細は Settings reference を参照してください。
複数のオリジンを許可するには、正確なオリジンをカンマ区切りのリストで指定します。
| キー | 値 |
|---|---|
CORS_ALLOWED_ORIGINS | https://mygame.com,https://admin.mygame.com |
クロスオリジンリクエストでブラウザに Cookie や Authorization ヘッダーを送信させるには、CORS_ALLOW_CREDENTIALS を値 true で追加します。
CORS_ALLOW_CREDENTIALS=true と CORS_ALLOWED_ORIGINS=* を組み合わせないでください。この組み合わせは CORS 仕様で明確に無効と定められており、ブラウザに拒否されてリクエストは失敗します。さらに重要なのは、その背後にある意図自体が危険だという点です。任意のオリジンからの認証情報付きリクエストを許可すると、サインイン済みのユーザーが訪れたあらゆる Web サイトが、そのユーザーとしてアプリを呼び出してレスポンスを読み取れるようになり、セッション Cookie やアクセストークンの漏洩につながります(クロスサイトリクエストフォージェリ、すなわち CSRF のリスク)。CORS_ALLOW_CREDENTIALS=true を設定する場合は必ず、CORS_ALLOWED_ORIGINS を信頼できる正確なオリジンの明示的なリストに制限してください。
Step 2: Restart and apply
アプリの設定ページで Restart and Apply をクリックします。CORS の変数は起動時にのみ読み込まれるため、新しい値はアプリの再起動時に反映されます。
Settings reference
各設定は、起動時に対応する環境変数から読み込まれます。
| 環境変数 | デフォルト | 説明 |
|---|---|---|
CORS_ENABLED | false | マスタースイッチ。false の場合、CORS ヘッダーは追加されず、他のすべての CORS 変数は無視されます。 |
CORS_ALLOWED_ORIGINS | (空) | カンマ区切りの正確なオリジン(例: https://mygame.com)。* を設定すると任意のオリジンを許可します(非推奨)。CORS_ENABLED=true の状態でこれが空の場合、アプリは CORS を無効のままにし、エラーをログに記録します。Troubleshooting を参照してください。 |
CORS_ALLOWED_METHODS | GET,POST,PUT,DELETE,PATCH,OPTIONS | 許可する HTTP メソッド。 |
CORS_ALLOWED_HEADERS | Content-Type,Authorization | ブラウザが送信できるリクエストヘッダー。アプリが読み取るカスタムヘッダーがあれば追加してください。 |
CORS_EXPOSE_HEADERS | (空) | ブラウザの JavaScript が読み取れるレスポンスヘッダー。 |
CORS_ALLOW_CREDENTIALS | false | クロスオリジンリクエストで Cookie と Authorization を許可します。正確なオリジンの指定が必要です。 |
CORS_MAX_AGE | 600 | ブラウザがプリフライトレスポンスをキャッシュできる秒数。設定値にかかわらずブラウザ側で上限が適用されます(Chromium では約 2 時間)。 |
変数に空の値を設定すると、デフォルト値にフォールバックするのではなく、デフォルト値を上書きします。たとえば CORS_ALLOWED_METHODS を空の値で追加すると、6 つのデフォルトメソッドではなく、どのメソッドも許可されなくなります。デフォルト値を維持するには、その変数を設定にまったく含めないでください。
CORS が適用されるのは、アプリの REST および HTTP の範囲、つまり Swagger UI を含む gRPC ゲートウェイのみです。メトリクスエンドポイントと生の gRPC ポートは影響を受けません。
* ではなく正確なオリジンを指定してください。ワイルドカードや制限のないオリジンと組み合わせて認証情報を有効にすることは絶対に避けてください(上記の警告を参照)。メソッドとヘッダーは、アプリが実際に使用するものだけに絞り込んでください。CORS の設定は、アプリの所有者であるあなたの責任です。
Test your configuration
まず起動ログを確認します。 CORS が有効な場合、アプリは起動時に CORS enabled という行をログに記録し、許可されたオリジンと認証情報を許可するかどうかを示します。この行は CORS が実際にヘッダーを追加しているときにのみ出力されます。この行が見つからない場合は設定が反映されていないということなので、ブラウザのデバッグに取りかかる前に、まずこれを確認してください。
デプロイ済みのアプリに対して確認します。 許可されたオリジンで配信されているページから、アプリの https:// URL を呼び出し、ブラウザの Network タブで OPTIONS プリフライトとその Access-Control-Allow-Origin レスポンスヘッダーを確認します。
fetch("https://<your-extend-app-url>/v1/your-endpoint", {
method: "GET",
headers: { "Authorization": "Bearer <token>" },
})
.then((res) => res.json())
.then(console.log);
ローカルで確認します。 アプリをローカルで実行し、curl でプリフライトを送信します。localhost を直接呼び出すことで、デプロイ済みの URL では OPTIONS を横取りする可能性がある Ingress を回避できます。
curl -i -X OPTIONS "http://localhost:8000/v1/your-endpoint" \
-H "Origin: https://mygame.com" \
-H "Access-Control-Request-Method: GET"
正しいレスポンスには Access-Control-Allow-Origin: https://mygame.com が含まれます。許可されていないオリジン(例: https://evil.com)で同じ操作を繰り返し、そのヘッダーが含まれていないことを確認してください。これにより、許可リストが確実に適用されていることが確認できます。
Troubleshooting
- CORS ヘッダーが付かず、ログに
CORS_ALLOWED_ORIGINSが空であるというエラーが出る場合:CORS_ENABLED=trueにしたものの、オリジンを指定していません。オリジンのリストが空のままではすべてのオリジンを許可してしまうため、アプリはこの場合、意図的に CORS を無効のままにします。許可したいオリジンを追加するか、意図的にすべてを許可する場合は*を設定してください。 - CORS ヘッダーも CORS のログ行もまったく出ない場合: アプリに組み込みのミドルウェアが含まれていません。CORS ミドルウェアがアプリに含まれていない場合 を参照してください。
- 設定は正しいのに CORS エラーが出る場合:
http://ではなくhttps://の URL を呼び出してください。Extend はhttp://をhttps://に301リダイレクトしますが、ブラウザはプリフライトのOPTIONSではリダイレクトに従いません。 - プリフライトの
OPTIONSが失敗する場合:CORS_ENABLED=trueになっていること、オリジンがCORS_ALLOWED_ORIGINSに含まれていること、メソッドとヘッダーが許可されていることを確認してください。 curlとブラウザでヘッダーが異なる場合: Extend の Ingress がOPTIONSを横取りしている可能性があります。許可されたオリジンから実際のブラウザでfetchを実行して確認してください。
If your app does not include the CORS middleware
このセクションに従うのは、Before you begin のファイル確認で cors.go が見つからなかった場合だけです。たとえば、ミドルウェアが追加される前のサンプルアプリをもとに作成したアプリや、ゼロから書いたアプリが該当します。ミドルウェアを自分で追加すれば、上記で説明した CORS_* 環境変数をそのまま使用できます。
環境変数の手順とは異なり、この方法ではアプリのソースコードを変更するため、作業後にアプリをリビルドして再デプロイする必要があります。既存のビルドを再デプロイするだけでは不十分です。
Extend アプリは C#、Java、Python で書かれている場合がありますが、その HTTP のエントリポイントである gRPC ゲートウェイ は常に Go で書かれています。CORS は HTTP の領域の問題なので、アプリの言語にかかわらず Go で追加します。Go のコードを自分で書く必要はありません。以下のファイルをそのまま貼り付けてください。
以下の手順は、ゲートウェイが gateway/ フォルダー内の独立したコンポーネントになっている C#、Java、Python のレイアウトを前提としています。アプリが Go で書かれている場合、ゲートウェイはアプリ自体です。右側の列のパスに従い、logrus の代わりに slog を使用してください。
| C# / Java / Python | Go | |
|---|---|---|
| CORS ファイル(Step 1) | gateway/pkg/common/cors.go | pkg/common/cors.go |
| 組み込み先のファイル(Step 3) | gateway/main.go | main.go(リポジトリルート) |
go get を実行する場所(Step 2) | gateway/ | リポジトリルート |
| ロガー | *logrus.Logger | *slog.Logger |
Go のアプリでは、cors.go 内の github.com/sirupsen/logrus のインポートを log/slog に置き換え、パラメーターを logger *slog.Logger に変更してください。logger.Error(...) と logger.Warn(...) の呼び出しは、どちらのロガーでも同じように動作します。
Step 1: Add the CORS middleware file
gateway/pkg/common/cors.go に新しいファイルを作成し、次の内容を貼り付けます。
// gateway/pkg/common/cors.go
package common
import (
"net/http"
"strings"
"github.com/rs/cors"
"github.com/sirupsen/logrus"
)
// CORSConfig holds the cross-origin resource sharing settings for the
// gRPC-Gateway HTTP entry point. Each field maps to a CORS_* environment
// variable read by LoadCORSConfigFromEnv. CORS is disabled unless Enabled
// is true, so existing deployments are unaffected until you opt in.
type CORSConfig struct {
Enabled bool
AllowedOrigins []string
AllowedMethods []string
AllowedHeaders []string
ExposeHeaders []string
AllowCredentials bool
MaxAge int
}
// Active reports whether CORS is both switched on and actually able to add
// headers, which requires at least one allowed origin.
func (c CORSConfig) Active() bool {
return c.Enabled && len(c.AllowedOrigins) > 0
}
// LoadCORSConfigFromEnv reads the CORS configuration from environment
// variables. CORS is off by default: unless CORS_ENABLED=true the returned
// config is inert and NewCORSMiddleware becomes a passthrough.
func LoadCORSConfigFromEnv() CORSConfig {
return CORSConfig{
Enabled: strings.EqualFold(GetEnv("CORS_ENABLED", "false"), "true"),
AllowedOrigins: splitAndTrim(GetEnv("CORS_ALLOWED_ORIGINS", "")),
AllowedMethods: splitAndTrim(GetEnv("CORS_ALLOWED_METHODS", "GET,POST,PUT,DELETE,PATCH,OPTIONS")),
AllowedHeaders: splitAndTrim(GetEnv("CORS_ALLOWED_HEADERS", "Content-Type,Authorization")),
ExposeHeaders: splitAndTrim(GetEnv("CORS_EXPOSE_HEADERS", "")),
AllowCredentials: strings.EqualFold(GetEnv("CORS_ALLOW_CREDENTIALS", "false"), "true"),
MaxAge: GetEnvInt("CORS_MAX_AGE", 600),
}
}
// NewCORSMiddleware returns an HTTP middleware that applies the given CORS
// config. When CORS is not usable it returns an identity middleware that adds
// no headers. When enabled it delegates to rs/cors, which answers preflight
// OPTIONS requests and sets the Vary header.
func NewCORSMiddleware(cfg CORSConfig, logger *logrus.Logger) func(http.Handler) http.Handler {
passthrough := func(next http.Handler) http.Handler { return next }
if !cfg.Enabled {
return passthrough
}
// rs/cors treats an empty AllowedOrigins list as "allow every origin".
// Arriving there by omission is a misconfiguration rather than an intent,
// so refuse to enable CORS instead of silently exposing the API to any
// origin. Allowing everything stays possible, but only explicitly via
// CORS_ALLOWED_ORIGINS=*.
if len(cfg.AllowedOrigins) == 0 {
logger.Error("CORS_ENABLED=true but CORS_ALLOWED_ORIGINS is empty; leaving CORS disabled because an empty origin list would allow every origin. Set explicit origins (e.g. https://mygame.com), or '*' to allow all intentionally")
return passthrough
}
if cfg.AllowCredentials && containsWildcard(cfg.AllowedOrigins) {
logger.Warn("insecure CORS configuration: CORS_ALLOW_CREDENTIALS=true combined with a wildcard origin '*' is invalid per the CORS spec and will be rejected by browsers; specify exact origins (e.g. https://mygame.com) instead")
}
c := cors.New(cors.Options{
AllowedOrigins: cfg.AllowedOrigins,
AllowedMethods: cfg.AllowedMethods,
AllowedHeaders: cfg.AllowedHeaders,
ExposedHeaders: cfg.ExposeHeaders,
AllowCredentials: cfg.AllowCredentials,
MaxAge: cfg.MaxAge,
})
return c.Handler
}
// splitAndTrim splits a comma-separated list, trims whitespace from each
// item, and drops empty entries.
func splitAndTrim(s string) []string {
if strings.TrimSpace(s) == "" {
return []string{}
}
parts := strings.Split(s, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
if trimmed := strings.TrimSpace(p); trimmed != "" {
result = append(result, trimmed)
}
}
return result
}
// containsWildcard reports whether the origins list contains "*".
func containsWildcard(origins []string) bool {
for _, o := range origins {
if o == "*" {
return true
}
}
return false
}
cors.go は、同じパッケージ内の GetEnv および GetEnvInt ヘルパーを呼び出します。ほとんどのサンプルアプリでは、これらは pkg/common/utils.go にすでに定義されています。ゲートウェイの pkg/common フォルダーにこれらがない場合は、先に追加してください。追加しないと cors.go はコンパイルできません。
Step 2: Add the CORS dependency
このファイルは rs/cors パッケージを使用します。gateway/ フォルダー内で、次のコマンドを実行します。
go get github.com/rs/cors@v1.11.1
これにより、依存関係が go.mod と go.sum に追加されます。両方のファイルをコミットしてください。
Step 3: Wire it into the gateway
gateway/main.go で newGRPCGatewayHTTPServer 関数を見つけます。この関数は loggedMux を構築し、それを Handler として設定しています。loggedMux を CORS ミドルウェアでラップし、Handler にはラップ後のハンドラーを設定してください。こうすることで CORS が最も外側の層になり、ブラウザのプリフライト(OPTIONS)リクエストがゲートウェイに到達する前に処理されます。
loggedMux := loggingMiddleware(logger, mux)
// Add CORS middleware as the outermost layer so that browser preflight
// (OPTIONS) requests are answered here before reaching the gateway mux.
// It is inert unless CORS_ENABLED=true, so behavior is unchanged by default.
corsConfig := common.LoadCORSConfigFromEnv()
corsHandler := common.NewCORSMiddleware(corsConfig, logger)(loggedMux)
if corsConfig.Active() {
logger.WithFields(logrus.Fields{
"allowedOrigins": corsConfig.AllowedOrigins,
"allowCredentials": corsConfig.AllowCredentials,
}).Info("CORS enabled")
}
return &http.Server{
Addr: addr,
Handler: corsHandler, // was: loggedMux
// ...
}
NewCORSMiddleware と LoadCORSConfigFromEnv は common パッケージにあります。これは main.go 全体ですでに使われている common. プレフィックスのパッケージなので、新しいインポートは不要です。エディターがパッケージが見つからないと報告する場合は、gateway/ 内で go mod tidy を実行してください。
Step 4: Build, deploy, and enable CORS
- 変更がコンパイルできることを確認します。
gateway/内(Go のアプリの場合はリポジトリルート)でgo build ./...を実行してください。ここでコンパイルエラーが出る場合は、貼り付けたcors.goのタイプミスが原因であることがほとんどです。 - 通常の Extend のビルドおよびデプロイ手順を使用して、更新したアプリをビルドしてデプロイします。
- Step 1: Set the CORS environment variables の説明どおりに CORS 環境変数を設定し、Restart and Apply をクリックします。