Linuxを日常的に使う実験ブログ

Haskell × Servantで作ったWeb APIにCORS設定を行う方法

 2023-10-12

 2023-10-13

 プログラミング

今回はHaskell × Servant使ったウェブアプリでCORS設定を行う方法です。このブログのバックエンドはServantで作られていますが、CORSの設定について調べるのに少し手間取ったので、個人的なメモを兼ねてここでまとめておこうと思います。

Servantとは

Servantを一言で説明するのはなかなか困難なのですが、Web APIをHaskellの型で記述することのできるツールであり、ServantのDSLで記述されたWEB APIの挙動を確認したり、ドキュメント化したりするツール郡の総称でもあります。Haskellの型でWEB APIを宣言するだけでWarpを使用したAPIアプリケーションを構築できるためWEBフレームワークとして扱われることもあります。

Haskellの型でAPIを表現しておくことでOpenAPI形式で自動的にドキュメント化できる点もServantでAPIを作成する魅力の一つだと思います。Servantの魅力については別の機会で触れられればと考えています。

CORSとは

CORSは(Cross Origins Resource Share)の略で異なるオリジン間でリソースの共有する仕組みです。オリジンとはhttpsやhttpなどのプロトコル、example.com等のドメイン、8080や3000等のポートで定義され、全てが一致した場合に同一オリジンと判定されます。通常、ブラウザはセキュリティ上の理由から異なるオリジン間でのHTTPリクエストを制限します。

CSRF(Cross Site Request Forgery)を防ぐ手段として各ブラウザは同一オリジンポリシーを実装するようになり、クライアントはクライアントのURLと同じオリジンにのみ、リソースをリクエストできるよう制限されています。しかし、これはAPIによる非同期通信が常套手段となった現代では足かせとして不便な面もありました。そこで、異なるオリジン間で安全にリソースを共有する仕組みが作られました。

GET以外のサーバーに破壊的な副作用をもたらす複雑なリクエストに関してはOPTIONメソッドでプリフライトリクエストして許可されている挙動を取得しに行きます。その上でCORSの許可があれば再度リクエストを行います。ちなみに、プリフライトが発生しないリクエストは古いCORS仕様書では「単純リクエスト」呼ばれていましたが、現在のFetch仕様書ではこのこの言葉は使われていません。

SPAでアプリケーションを作る場合、DBへのCRUDはAPIへのリクエストで行われるので、アプリケーションとサーバーでオリジンが異なる場合はサーバー側でCORSヘッダーを設定しないとCORSエラーとなってしまいます。

ServantでCORS設定をする

ServantはWEBサーバーとして機能する際にWaiアプリケーションとして動作します。WaiとはWebアプリケーションとWebサーバー間の共通プロトコルです。ServantはWaiの実装であるWebサーバーであるWarpで動作させWaiアプリケーションとして動作します。Servantで作成したAPIにCORS制御をする場合はwai-corsを用います。wai-corsはServantだけでなくWaiのプロトコルで動作するアプリケーションには全て適用可能です。

wai-colsでCORSの設定をするにはCorsResourcePolicy型で定義します。CorsResourcePolicyコンストラクタを使用して設定を定義しても良いですが、simpleCorsResourcePolicyが用意してあるので、これをカスタマイズするのが簡単です。simpleCorsResourcePolicyの定義は以下のとおりです。corsOriginsNothingを適用することでサーバー側はAccess-Control-Allow-Origin: *で返すことになります。もし特定のオリジンのみのアクセスを許可するにはMaybe ([Origin], Bool)型でオリジンのリストと資格情報を送信すべきかを指定します。

simpleCorsResourcePolicyの実装

simpleCorsResourcePolicy CorsResourcePolicy
simpleCorsResourcePolicy = CorsResourcePolicy
    { corsOrigins = Nothing
    , corsMethods = simpleMethods
    , corsRequestHeaders = []
    , corsExposedHeaders = Nothing
    , corsMaxAge = Nothing
    , corsVaryOrigin = False
    , corsRequireOrigin = False
    , corsIgnoreFailures = False
    }

simpleMethods =
    [ "GET"
    , "HEAD"
    , "POST"
    ]

ServantでのCORS設定例

ここまで色々説明してきましたが、実際のコードを見ていただいた方が理解が進むかもしれません。ここでサンプルコードを示します。実際に動かしたコードではないので、インポート部分は不足があるかもしれません。また、APIの詳細部分については省略をしています。

ポイントとしてはMiddleware型がtype Middleware = Application -> Applicationで定義されている点です。Middleware型にApplicationを引数として渡すことでApplicaion型になることです。これによって複数のMiddlewareをApplicationに適用していくことが可能になります。

サンプルコード

{-# LANGUAGE OverloadedStrings #-}

import Network.Wai
import Network.Wai.Handler.Warp ( run )
import Network.Wai.Middleware.Cors
import Servant
import Data.Text

{-
Servant CORS setting sample
-}

main :: IO ()
main = run 8080 app

app :: Application
app = policy $ serve api server

api :: Proxy API
api = Proxy

server :: Server API
server =
  userGET 
  :<|> ...
  :<|> ...
  where
    userGET = ...

type API =
  "users" :> QueryParam "status" Text :> '[JSON] [User]
  :<|> ...
  :<|> ...

policy :: Middleware
policy = cors $ const $ Just myCorsResourcePolicy

myCorsResourcePolicy :: CorsResourcePolicy
myCorsResourcePolicy = simpleCorsResourcePolicy
  { corsMethods = methods
  , corsRequestHeaders = headers
  , corsOrigins = origins
  }
  where
    methods :: [HTTP.Method]
    methods = [ "GET"
              , "HEAD"
              , "POST"
              , "PUT"
              , "DELETE"
              ]
    headers :: [HTTP.HeaderName]
    headers =
      [ "Content-Type"
      , "Authorization"
      , "Accept-Encoding"
      ]

    origins :: Maybe ([Origin], Bool)
    origins = Just ([ "http://example.com" ], False)

まとめ

ServantでAPIを作成する際にネックとなるCORSの設定についてサンプルコードとともに解説をしてみました。本番環境だけでなく、開発環境でもCORSエラーが起こり対策する必要がある場合もあります。この記事が参考になれば幸いです。