Access-Control-Allow-Origin は一つしか指定できないの?動作環境によって動的に変更したい場合はどうすればいいの?年収は?彼女はいるの?調べてみました!

こんにちは!去年の今日はクリスマスキャンプでメンバーのみんなと一緒に最高の日々を過ごしていたのに、今年は卒論と一緒にクリスマスを過ごす羽目になっているひむらです!助けて!

 

この記事は Life is Tech ! Mentors - Qiita Advent Calendar 2024 - Qiita の18日目の記事です(遅刻)。

qiita.com

 

用語の誤用や概念の間違った説明などをしている場合は指摘していただけると幸いです!

 

さて、まず本題の前に、現在の WWW (World Wide Web) における通信がどのように行われており、どのようなルールがあるのか改めて見ていきます!

 

SOP について

SOP (Same Origin Policy) というのは、World Wide Web Consortium (W3C) というウェブ標準を開発している団体がウェブ上のセキュリティ向上を目的として設定したルールで、ざっくりまとめると「異なるオリジンから取得されたドキュメント間の通信を許可しない」というルールです。

An origin is defined by the scheme, host, and port of a URL. Generally speaking, documents retrieved from distinct origins are isolated from each other. For example, if a document retrieved from http://example.com/doc.html tries to access the DOM of a document retrieved from https://example.com/target.html, the user agent will disallow access because the origin of the first document, (http, example.com, 80), does not match the origin of the second document (https, example.com, 443).

 

ちょっとややこしいので分かりやすいように家で例えます!

家には何もしないと鍵などもかかっていないため、誰でも入り放題で、セキュリティ的に危険な状態です。*1

そこで、W3C が家の前に SOP という番犬を配置することを義務付けました!

番犬は家の主である自分のことだけは通してくれますが、それ以外の人がやってくると問答無用で噛みつき、エラーを返します。

これで家の安全は守られたわけです。良かったね

 

しかしこれでは、フロントエンドとバックエンドを別のオリジンで管理するアプリケーションや、いろいろなオリジンにまたがる大規模なアプリケーションはもちろん、Google Map API などの公開 API を第三者に使ってもらいたい、というようなユースケースも実現できなくなってしまいます。

 

CORS について

このままだと番犬 SOP が何でもかんでも噛みつくせいで、オリジンをまたがるアプリケーションは作れません。そんなときは、CORS というルールを使うことで番犬 SOP をなだめることができます。

 

CORS (Cross-Origin Resource Sharing) とは、「HTTP ヘッダーを用いて指定された条件を満たすリクエストであれば、異なるオリジン間の通信であってもアクセスを許可する」という仕組みです。

 

同じく家で例えると、知り合いや家族は家に入ってきてもらって問題ないので、番犬 SOP に知り合いや家族の顔を覚えてもらうことで彼らのアクセスを許可する、ということです。

 

SOP は信頼されていない外部のオリジンがアクセスしてくることを制限するためのルールだったので、逆に言うと信頼されている外部のオリジンのアクセスは制限する必要がないわけです。ということで、信頼されている外部のオリジンの情報を伝えておくことで、そいつのアクセスだけは許可してあげる、という考え方なんですね。

 

Access-Control-Allow-Origin について

長々とややこしい仕組みについて話してきましたが、実は本題の Access-Control-Allow-Origin というのは CORS の条件を指定するために用いられる HTTP ヘッダーの一つだったのです!

 

Access-Control-Allow-Origin とはどのような条件を指定するための HTTP ヘッダーなのかと言いますと、「アクセスを許可する外部のオリジンを指定する」ためのヘッダーです!

 

Access-Control-Allow-Origin は一つしか指定できないの?

複数のオリジンからのアクセスを許可したいケースというのは結構色々あると思います。素朴に考えれば Access-Control-Allow-Origin: http://hoge.com, http://fuga.com のようにヘッダーを指定すれば複数のオリジンを設定できそうですよね。

 

ということで試してみます。

The 'Access-Control-Allow-Origin' header contains multiple values, but only one is allowed.

怒られてしまいました。エラーメッセージを読むと、オリジンを複数指定するのはだめと書いてあります。実際、MDN にも

Specifies a single origin. If the server supports clients from multiple origins, it must return the origin for the specific client making the request.

という記述があり、オリジンを指定する場合は一つのみに留める必要があるようです。僕は最初素朴にやってしっかりハマりました。

 

では複数のオリジンを許可することはできないのか、というと実はそういうわけではなく、Access-Control-Allow-Origin: * とすることで任意のオリジンに対してアクセスを許可することが可能です。

ただ、資格情報(Cookie など)を含むリクエストに対しては使えなかったり、セキュリティ的によろしくなかったりと色々と渋いので、本当に任意のオリジンに対してアクセスを許可する必要がある場合(公開 API など)以外は使わないほうが良いと思います。

 

ということで結論としては、ワイルドカード(*)で複数指定できるが、あまり積極的に使いたくはない、という感じの温度感です。

 

特定の複数のオリジンからのアクセスのみを許可したい場合はどうすればよいのか

ワイルドカード(*)で指定するのがよろしくないのなら、じゃあ一体どうすればいいんだ!ということなのですが、結論から言うとアクセスしてきたリクエストのヘッダーを読み取り、手元で保持している許可して良いオリジンのリストと照合して OK が出たら Access-Control-Allow-Origin にリクエスト元のオリジンを乗せて返す、というロジックを実装すればよいです。

 

多分言葉で説明するよりも実際の実装例を見る方が早いと思うので、改めてコードで説明をします(AI メンターの端くれとして Python の FastAPI で実装してみています!大したコードではないので Ruby でも Go でもなんでも再現できると思います)。


from fastapi import FastAPI, Request, Response, status
from starlette.middleware.base import BaseHTTPMiddleware

ALLOWED_ORIGINS = ["http://hoge.com", "http://fuga.com"]
app = FastAPI()


class CORSMiddleware(BaseHTTPMiddleware):
    async def dispatch(
        self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
    ) -> Response:
        origin = request.headers.get("origin")
        if origin in ALLOWED_ORIGINS:
            response.headers["Access-Control-Allow-Origin"] = origin
            response.headers["Access-Control-Allow-Credentials"] = "true"
            response.headers["Access-Control-Allow-Methods"] = (
                "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT"
            )
            response.headers["Access-Control-Allow-Headers"] = "Content-Type"

        return await call_next(request)


app.add_middleware(CORSMiddleware)

ポイントは、ALLOWED_ORIGINS のような形で許可するオリジンのリストを保持しておいて、リクエストを受け取ったらリクエストヘッダーの中にある origin というヘッダーの値を取得し、origin が ALLOWED_ORIGINS に含まれていれば origin を Access-Control-Allow-Origin ヘッダーの値としてセットして返す、というところです。

 

これにより、Access-Control-Allow-Origin ヘッダーに含まれる origin の数が一つのみであるという制約を満たしながら、複数のオリジンを許可する機構を作ることができました!他にもっといい方法があるぜ、という場合は教えていただけると嬉しいです!

 

現在株式会社 Life is Tech! では大学生インターンの募集をしています。ぜひご興味のある方は下のリンクからアクセスしてみてください!説明会だけでも大丈夫です!楽しいよ!

leaders.life-is-tech.com

 

また、中高生の皆さんはぜひキャンプスクールなどで一緒にプログラミングを学んでみませんか?楽しいよ!

camp.life-is-tech.com

life-is-tech.com

 

参考

www.w3.org

fetch.spec.whatwg.org

developer.mozilla.org

*1:セキュリティのために鍵を掛けるアプローチもありますが、ここでは説明しません。セッション、トークン認証とかで調べると良いかも