これはClojure Advent Calendar 2019の21日目の記事です。
そして、こちらの記事の続きにもなっています。
本記事では、RingのMiddlewareについて解説していきます。 基本的に上記の記事を理解いただいた前提で話を進めていこうと思います。
- Ring Middlewareについてなぜ解説が必要なのか
- Ring Middlewareとは何か
- Ring Middleware大全
- URLに応じて適用する
- リクエスト・レスポンスをロギングする
- [開発用]スタックトレースを出力する
- X-Forwarded-For ヘッダからリクエスト元IPアドレスを取得する
- X-Forwarded-Proto ヘッダからプロトコルを判別する
- HTTPで来たアクセスをHTTPSにリダイレクトする
- HSTSに対応する
- X-Content-Type-Options をレスポンスにつける
- X-Frame-Options をレスポンスにつける
- X-XSS-Protection をレスポンスにつける
- If-Modified-Since に対応する
- Content-Type がレスポンスにない場合につける
- URLの拡張子をみて Content-Type をつける
- ディレクトリ上のファイルをレスポンスする
- クラスパス上のファイルをレスポンスする
- 相対パスのリダイレクトURLを絶対パスに変換する
- Cookieを読み書きする
- フォームからPOSTされたデータやクエリパラメータを読み :params に入れる
- multipart/form-data のリクエストボディを読み :params に入れる
- :params 内のネストしたキー文字列をネストしたマップにする
- :params のキーをKeywordに変換する
- sessionに対応する
- 一度きりのsessionを使う
- CSRFへの対策をする
- 閑話休題 - Ring-Defaultsで一度に設定する
- Basic認証に対応する
- CORSに対応する
- 認証・認可を行う
- APIのリクエストに対応する
- ルーティングしやすいようにURL末尾の / を削除する
- URLとリクエストメソッドごとに異なるhandlerを呼ぶ
- Content-Security-Policy レスポンスヘッダを付与する
- 更新履歴
- おわりに
Ring Middlewareについてなぜ解説が必要なのか
まずなぜこのような記事が必要なのかについてです。
Clojureのエコシステムには、基本的にRuby on RailsやDjangoのような重畳なフレームワークはありません。 そのため、Handlerで実行する前に共通的にやっておきたい処理が複数あることは想定されるでしょうが、 それらが自動的に行われるような仕組み、または何かの設定ファイルでONにすればすべて"いい感じ"にやってくれる仕組みなどはありません。
そこで共通処理をHandlerの実行前後に足す仕組みとしてRing Middlewareが使われます。
しかしながらClojureのライブラリの考え方として、1つ1つの関心領域を小さくして、その固有の問題だけを解くようにすると言えるようなものがあります。 それは今すでに慣れている筆者にとっては非常に好ましく思いますし、ぜひClojureでのWeb開発にこれから慣れていく人にも体験して欲しいのもではありますが、 一方で、"標準的な"Webアプリケーションを作ろうと思うと多数のMiddlewareを最初から使う必要があり、必要なものを判断して組み合わせる学習コストがかかってしまいます。 筆者も最初は何がどの効果を発揮しているのかわからず困惑した経験があります。
そこで本記事では、実際に弊社の製品で使われている構成を元に、抑えておくべきと思われるRing Middlewareライブラリと、 その前に仕組みについても解説することで、これからClojureでのWeb開発を始める上でのハードルを下げることを目的としています。
Ring Middlewareとは何か
まず定義としてこのように記述されています。
Middleware are higher-level functions that add additional functionality to handlers. The first argument of a middleware function should be a handler, and its return value should be a new handler function that will call the original handler.
要は、 引数にHandlerを受け取り、新しいHandlerを返す関数ということですね。
ここでおさらいをしておくと、(Ring)Handlerとは、リクエストマップを受け取り、レスポンスマップを返す関数でした。
(defn handler [request] {:status 200 :body "Hello world!"})
つまり、「引数にHandlerを受け取り、新しいHandlerを返す」ということは上のような関数を受け取って、別の関数を返すということですね。
まずは、何もしない無意味なHandlerを書いてみます。
(defn no-mean-middleware [handler] (fn [request] (handler request)))
引数で受け取ったHandlerをそのまま使っているだけです。 適用しても何も意味はありませんが、使うにはMiddlewareの引数にHandlerを渡せばいいわけなので、このように書きます。
(no-mean-middleware handler)
もしくはThreading Macroを使えば、
(-> handler (no-mean-middleware))
こうなりますね。 これを評価した戻り値である関数をWeb Serverに渡して起動すれば、Ring Middlewareを適用したHandlerで動くことになります。
少し意味のあるMiddlewareを書いてみます。 例として、引数として受け取ったHandlerを実行する前にリクエストマップの値を出力して、Handlerを実行した後に結果を返す前にレスポンスマップを出力するMiddlewareを書いてみます。
(defn debug-middleware [handler] (fn [request] (clojure.pprint/pprint request) ; 前処理 (let [res (handler request)] (clojure.pprint/pprint res) ; 後処理 res)))
何もしない no-mean-middleware
と見比べてみると、 (handler request)
と引数でもらったhandlerを使う前後に clojure.pprint/pprint
しているだけなのがわかると思います。
これをこんな感じで適用してやると、リクエストマップの中身とレスポンスマップの中身を見ることができます。
(-> handler (no-mean-middleware) (debug-middleware))
ここまで、順を追えば簡単なことだと思いますが、実際のところこれがRing MIiddlewareのすべてです。
(handler request)
をする前に何か処理をしたり、handlerの引数であるrequestをいじったりする(前処理)(handler request)
の結果を補足して、結果をいじったりする(後処理)
これだけです。
例えば、requestの中身をみて、URLごとにリクエストボディをいじるようなことをしたり、認証をかけたりといった具合です。
もちろん引数として渡されたhandlerを使わずに無視して他のことをすることもできます。
次のMiddlewareでは /hatena
にリクエストが来たら、handlerを無視して https://www.hatena.ne.jp/ にリダイレクトします。
(defn hatena-middleware [handler] (fn [request] (if (= (:uri request) "/hatena") {:status 303 :headers {"Location" "https://www.hatena.ne.jp/"} :body ""} (handler request))))
最後にMiddlewareで適用した前処理と後処理が実行される順番ですが、 あるMiddlewareに渡されるhandlerは、別のMiddlewareの前処理と後処理を含んだものであるので、 あるMiddlewareの前処理は先に適用されたMiddlewareの前に、逆に後処理は先に適用されたMiddlewareの後に適用されます。
文章で書くと複雑ですが、Threading Macroを利用すると、次のように視覚的にわかりやすく説明可能です。
(-> handler (hatena-middleware) ; 前処理3番目、後処理1番目 (debug-middleware) ; 前処理2番目、後処理2番目 (no-mean-middleware) ; 前処理1番目、後処理3番目 )
Ring Middleware大全
ここからは、実際に使われるMiddlewareを解説していきます。
URLに応じて適用する
まず前処理・後処理として、具体的な役割をするMiddlewareの前に、 Middlewareそれ自体を条件に一致したときに特定したり・しなかったりするのを制御するものを紹介します。
正確にはこれはRing Middlewareではないですが、Middlewareと一緒に使うものなので先に紹介します。
これにはring.middleware.conditionalを使います。
if-url-starts-with
, if-url-doesnt-start-with
, if-url-matches
, if-url-doesnt-match
の4つのAPIがあります。
例えば次のようにすると、URLが /api
から始まる時だけ先ほど書いた debug-middleware
を適用することができます。
(require '[ring.middleware.conditional :as r.m.c]) (-> handler (r.m.c/if-url-starts-with ["/api"] debug-middleware))
リクエスト・レスポンスをロギングする
ring-loggerを使うと、リクエストの開始・終了や、リクエストボディ(debugレベル)をロギングすることができます。
(require '[ring.logger :as r.l]) (-> handler (r.l/wrap-with-logger))
[開発用]スタックトレースを出力する
ring.middleware.stacktrace/wrap-stacktraceを使うと、例外をキャッチして、stacktraceをHTMLで、もしくは標準出力に出してくれます。
(require '[ring.middleware.stacktrace ::as r.m.strace]) (-> handler (r.m.strace/wrap-stacktrace {:color? true}))
X-Forwarded-For
ヘッダからリクエスト元IPアドレスを取得する
IPアドレス制限の機能を作るときなど、ユーザーのIPアドレスを取得したい時があります。
X-Forwarded-Forを使って、途中にProxyがある場合を考慮しつつ、ユーザーのIPアドレスを取得します。
ring.middleware.proxy-headers/wrap-forwarded-remote-addrを使います。
(require '[ring.middleware.proxy-headers :as r.m.proxy]) (-> handler (r.m.proxy/wrap-forwarded-remote-addr))
適用すると、リクエストマップの :remote-addr
が更新されます。
X-Forwarded-Proto
ヘッダからプロトコルを判別する
X-Forwarded-Proto
を使って、例えばロードバランサーの背後のアプリでもプロトコルがHTTPなのかHTTPSなのか判別できます。
ring-sslを使います。
(require '[ring.middleware.ssl :as r.m.ssl]) (-> handler (r.m.ssl/wrap-forwarded-scheme))
適用すると、リクエストマップの :scheme
に :http
または :https
が入ります。
HTTPで来たアクセスをHTTPSにリダイレクトする
これもring-sslを使います。
(require '[ring.middleware.ssl :as r.m.ssl]) (-> handler (r.m.ssl/wrap-ssl-redirect))
これはリクエストマップの :scheme
をみて判別するので、ロードバランサとhttpで通信している場合などは、
上述の wrap-forwarded-scheme
と一緒に使うことが必要です。
HSTSに対応する
Strict-Transport-Securityに対応します。
これもring-sslを使います。
(require '[ring.middleware.ssl :as r.m.ssl]) (-> handler (r.m.ssl/wrap-hsts))
ただしこれはリバースプロキシとしてNginxなどを入れるとそこでできますので、 Clojureアプリ側でやるべきかどうかについては疑問ではあります。
X-Content-Type-Options
をレスポンスにつける
X-Content-Type-Optionsをレスポンスにつけます。
ring.middleware.x-headers/wrap-content-type-optionsを使います。
(require '[ring.middleware.x-headers :as x]) (-> handler (x/wrap-content-type-options :nosniff))
ただしこれはリバースプロキシとしてNginxなどを入れるとそこでできますので、 Clojureアプリ側でやるべきかどうかについては疑問ではあります。
X-Frame-Options
をレスポンスにつける
X-Frame-Optionsをレスポンスにつけます。
ring.middleware.x-headers/wrap-frame-optionsを使います。
(require '[ring.middleware.x-headers :as x]) (-> handler (x/wrap-frame-options :deny))
ただしこれはリバースプロキシとしてNginxなどを入れるとそこでできますので、 Clojureアプリ側でやるべきかどうかについては疑問ではあります。
X-XSS-Protection
をレスポンスにつける
X-XSS-Protectionをレスポンスにつけます。
ring.middleware.x-headers/wrap-xss-protectionを使います。
(require '[ring.middleware.x-headers :as x]) (-> handler (x/wrap-xss-protection true :block))
ただしこれはリバースプロキシとしてNginxなどを入れるとそこでできますので、 Clojureアプリ側でやるべきかどうかについては疑問ではあります。
If-Modified-Since
に対応する
If-Modified-Sinceに対応することで、 ファイルに更新がなければ、キャッシュを使うように指示できます。
ring.middleware.not-modified/wrap-not-modifiedを使います。
(require '[ring.middleware.not-modified :as r.m.nm]) (-> handler (r.m.nm/wrap-not-modified))
Content-Type
がレスポンスにない場合につける
ring.middleware.default-charset/wrap-default-charsetを使います。
例えば、レスポンスマップの Content-Type
に text/html
が設定されている時に、 text/html; charset=utf-8
と変更してくれます。
文字化け防止ですね。
(-> handler (r.m.dc/wrap-default-charset "utf-8"))
URLの拡張子をみて Content-Type
をつける
ring.middleware.content-type/wrap-content-typeを使います。
これはレスポンスマップに Content-Type
が設定されていない時に、例えばリクエストマップの :uri
が /hoge.csv
ならば text/csv
を Content-Type
に設定してくれるというものです。
動的コンテンツを返す場合というより、後述するディレクトリ上のファイルやクラスパス上のファイルを直接返す時にセットで使うものですね。
(require '[ring.middleware.content-type :as r.m.ct]) (-> handler (r.m.ct/wrap-content-type))
ディレクトリ上のファイルをレスポンスする
ring.middleware.file/wrap-fileを使います。
リクエストされたパスに一致する、サーバーのディレクトリ上のファイルをそのまま返したい時に使います。
クラウド上の使い捨て前提のサーバーにデプロイする前提だと、そのサーバー上のディレクトリのファイルに依存することはあまりないでしょうから、 今はあまり使わないものではないでしょうか。
(require '[ring.middleware.file :as r.m.f]) (-> handler (r.m.f/wrap-file "/my/static/file/root/"))
クラスパス上のファイルをレスポンスする
ring.middleware.resource/wrap-resourceを使います。
こちらはよく使うと思います。
Clojureアプリケーションはビルドして単一のJARファイルにしてそれを java -jar
で起動することが多いと思います。
その時にクラスパス上にCSSファイルやJSファイルなどを含めておいて配信するということをします。
クラスパス上のファイルパスは clojure.java.io/resource
で参照することができますが、このMiddlewareはこれを使ってリクエストに一致するファイルが存在すればそれを返してくれます。
ちゃんと Last-Modified
付きです。
root-path
を指定するので、見えて良いパス配下だけに限定することができます。
(require '[ring.middleware.resource :as r.m.r]) (-> handler (r.m.r/wrap-resource "public"))
相対パスのリダイレクトURLを絶対パスに変換する
ring.middleware.absolute-redirects/wrap-absolute-redirectsを使います。
(ring.util.response/redirect "/foo")
とすると、
{:status 302 :headers {"Location" "/foo"} :body ""}
このような相対パスのリダイレクトになりますが、これを http
から始まる絶対パスに直してくれます。
(require '[ring.middleware.absolute-redirects :as r.m.ar]) (-> handler (r.m.ar/wrap-absolute-redirects))
Cookieを読み書きする
ring.middleware.cookies/wrap-cookiesを使います。
リクエストからCookieを読み :cookies
に入れたり、レスポンスマップから :cookies
を読み Set-Cookie
ヘッダを設定してくれます。
実際のアプリケーションでは後述する wrap-session
を使ってセッションを実現し、これ単体ではあまり使うケースはないと思います。
(require '[ring.middleware.cookies :as r.m.cookie]) (-> handler (r.m.cookie/wrap-cookies))
フォームからPOSTされたデータやクエリパラメータを読み :params
に入れる
ring.middleware.params/wrap-paramsを使います。
次の2つをやってくれます。
Content-Type
がapplication/x-www-form-urlencoded
だった時に(FormタグでPOSTした時に)、bodyを読み込んで、:form-params
と:params
に入れてくれます- クエリパラメータがあれば、読み込んで
:query-params
と:params
に入れてくれます
なお、この時両方あれば :params
はマージされたものになります。
(require '[ring.middleware.params :as r.m.params]) (-> handler (r.m.params/wrap-params))
multipart/form-data
のリクエストボディを読み :params
に入れる
ring.middleware.multipart-params/wrap-multipart-paramsを使います。
Content-Type
が multipart/form-data
だった時に、bodyを読み込んで :multipart-params
と :params
に入れてくれます。
これらのキーがすでにある場合はマージされます。
bodyのファイルは、デフォルトでは一時ファイルとしてディレクトリ上におかれ、 :params
は次のキーと値が入ります。
:filename
:content-type
:tempfile
(java.io.Fileインスタンス):size
(require '[ring.middleware.multipart-params :as r.m.mparams]) (-> handler (r.m.mparams/wrap-multipart-params))
:params
内のネストしたキー文字列をネストしたマップにする
ring.middleware.nested-params/wrap-nested-paramsを使います。
これまで紹介してきた wrap-params
もしくは wrap-multipart-params
を使うと、フォームからPOSTされたデータ、もしくはクエリパラメータを :params
に集めることができます。
その時にネストしたデータを表現したい時にこのMiddlewareを使うと、
フォームのnameもしくはクエリパラメータのkeyを "foo[bar]"
のようにすることで、
:params
の中身を {"foo" {"bar" "値"}}
のように展開することができます。
できることの詳細は上のリンクを参照してください。
(require '[ring.middleware.nested-params :as r.m.nested]) (-> handler (r.m.nested/wrap-nested-params))
:params
のキーをKeywordに変換する
ring.middleware.keyword-params/wrap-keyword-paramsを使います。
wrap-params
wrap-multipart-params
を使うことで :params
フォームからPOSTされたデータ、もしくはクエリパラメータを :params
に集めることができましたが、
:params
のキーはまだStringのままで、Clojureで扱うにはKeywordであったほうが扱いやすいです。
このMiddlewareを適用すると、 :params
のキーをKeywordに変えてくれます。
ここで、 :form-params
や :query-params
などは変更されないので注意してください。
(require '[ring.middleware.keyword-params :as r.m.kw]) (-> handler (r.m.kw/wrap-keyword-params))
sessionに対応する
ring.middleware.session/wrap-sessionを使います。
これを適用するとCookieと後述するsession storeからsessionを読み込み、リクエストマップに :session
キーとしてデータを追加してくれます。
また、レスポンスマップに :session
を追加すると更新され、またその値がnilであれば削除されます。
どこにsessionデータを保存するかを制御するために、 :store
というオプションを使います。
指定しなかった場合は、メモリ上に保存されますが、そのままでいいことはあまりないでしょう。
ここでは実践的な例として、Redisに保存する例を紹介します。Redisクライアントとしてcarmineを使います。
(require '[ring.middleware.session :as r.m.session] '[taoensso.carmine.ring :as carmine]) (let [redis-conn-opts {:pool {} :spec {:host "localhsot" :port "6379" :password "foobar" :timeout-ms 1000 :db 1}} session-store (carmine/carmine-store redis-conn-opts)] (-> handler (r.m.session/wrap-session {:store session-store})))
一度きりのsessionを使う
ring.middleware.flash/wrap-flashを使います。
レスポンスマップに :flash
というキーがあれば、それを次のリクエストの時だけ、リクエストマップに :flash
というキーでデータを渡してくれます。
これはsessionに依存しているので、上記の wrap-session
と共に使うことが前提です。
(require '[ring.middleware.flash :as r.m.flash]) (-> handler (r.m.flash/wrap-flash))
CSRFへの対策をする
ring-anti-forgeryを使います。
このMiddlewareは使うだけならば簡単なのですが、このMiddlewareを入れるとGET/HEAD/OPTIONS以外のリクエスト全てで、CSRFトークンがないとエラーになるようになります。
そのため、 フォームからPOSTするときは、 ring.util.anti-forgery/anti-forgery-field
を含め、
XMLHTTPRequestする時は、 ring.middleware.anti-forgery/*anti-forgery-token*
をHTMLに含めて返し、
JS上でそれを読んだ上で、 ヘッダに x-csrf-token
として設定する必要があります。
またCSRFトークンエラーが起こった時の処理として :error-handler
にhandler関数を渡すことができます。
実践的にはこれも必要でしょう。
こちらもsessionに依存しているので、上記の wrap-session
と共に使うことが前提です。
(require '[ring.middleware.anti-forgery :as r.m.af]) (-> handler (r.m.af/wrap-anti-forgery {:error-handler (fn [_] {:status 403 :headers {} :body "Invalid csrf token"})}))
閑話休題 - Ring-Defaultsで一度に設定する
ここまで多くのMiddlewareを設定してきましたが、1つ1つrequireしていくのは大変です。
そこで ring-defaultsを使って、 wrap-forwarded-remote-addr
から wrap-anti-forgery
までを一括で適用します。
ring-defaultsにはapi-defaults、site-defaults、secure-api-defaults、secure-site-defaultsの4つのデフォルト設定が用意されていますが、 オススメなのはこれを使わずに自分で以下のようにマップを作って直接渡すことです。 なぜオススメなのかというと、結局細かいことをしようとした時にデフォルト設定をいじる必要があり、その時にassocなりで操作したのを渡していると、 結局何がどうなっているのか見えづらいためです。
(require '[ring.middleware.defaults :as defaults] '[taoensso.carmine.ring :as carmine]) (let [redis-conn-opts {:pool {} :spec {:host "localhsot" :port "6379" :password "foobar" :timeout-ms 1000 :db 1}} session-store (carmine/carmine-store redis-conn-opts) default-config {:params {:urlencoded true :multipart true :nested true :keywordize true} :cookies false :session {:flash true :cookie-attrs {:http-only true :same-site :strict :secure true} :store session-store} :security {:anti-forgery {:error-handler (fn [_] {:status 403 :headers {} :body "Invalid csrf token"})} :hsts false :ssl-redirect false} :static {:resources "public"} :responses {:not-modified-responses true :absolute-redirects true :content-types true :default-charset "utf-8"} :proxy true}] (-> handler (defaults/wrap-defaults default-config)))
Basic認証に対応する
ring-basic-authenticationを使うのが簡単です。
特定のURLだけにかけたい場合は、前述の if-url-starts-with
などと併用しましょう。
(require '[ring.middleware.basic-authentication :as r.m.basic]) (def basic-username "foo") (def basic-pass "pass") (-> handler (r.m.basic/wrap-basic-authentication (fn [username pass] (and (= username basic-username) (= pass basic-pass)))))
CORSに対応する
Ring CORSを使います。
このライブラリの良いところは、 Access-Control-Allow-Origin
に正規表現を使えるところです。
(require '[ring.middleware.cors :as r.m.cors]) (-> handler (r.m.cors/wrap-cors :access-control-allow-origin [#"https://.*example.com"] :access-control-allow-headers #{:accept :content-type :authorization} :access-control-allow-methods #{:get :put :post :delete}))
認証・認可を行う
buddy-authを使います。
buddy-authはbasic, session, token, JWT, JWEなど各種の認証に対応していますが、 詳細はドキュメントに譲るとして、 ここではセッション認証のみの実例を以下に示します。
ここではコード上のコメントでやるべきことを解説します。
(require '[ring.util.http-response :as res] '[buddy.auth.accessrules :as baa] '[buddy.auth.backends :as bab] '[buddy.auth.middleware :as bam]) (defn login-post "ログインフォームからのポストを受けるハンドラー関数だと思ってください" [{:keys [params session]}] (if-let [account (login params)] (-> (res/ok) ;; セッションに、identityというキーでログイン情報を入れます ;; buddy-authとは関係ありませんが、 :recreate をtrueとしてセッションを再作成しています (assoc :session (vary-meta (assoc session :identity account) assoc :recreate true))) (res/bad-request "login error"))) (defn- any-access [_] (baa/success)) (defn- account-access [{:keys [identity]}] (if identity (baa/success) (baa/error))) (def ^:private rules ;; ruleは先頭から順に評価される [ ;; 例えば、ログイン画面誰でもアクセス可能にする {:uris ["/login"] :handler any-access} ;; 例えば、APIはクライアント側で認証が切れていることを検知したら401を返す {:pattern #"\A/api.*\z" :handler account-access :on-error (fn [_ _] (res/unauthorized))} ;; 例えば、その他の全てのパスはsession認証している必要があることにし、 ;; 認証されていなければログイン画面に飛ばす {:pattern #"\A/.*\z" :handler account-access :on-error (fn [_ _] (res/see-other "/login"))}]) (defn- session-authfn "上のlogin-postでセッションにidentityを入れると、 次のアクセス時にidentityがセッションに入ってくる。 identityがセッションにある場合にこの関数が呼ばれ、その中身が正しいものか 検証した上で、正しければ、identityを返す。 ここでidentityを改変することも可能。" [identity] ;; 例えば、パスワードは変更されていないか?などの検証をここでする identity) (let [session-backend (bab/session {:authfn session-authfn})] (-> handler ;; 認可 ;; 上のルールに指定した挙動をさせる (baa/wrap-access-rules {:rules rules}) ;; 認証 ;; backendは複数指定可能 ;; 例えば、セッションかtokenでの認証を許可するなどする ;; if-url-starts-with などと併用すれば、URLごとに処理を分けることもできる (bam/wrap-authentication session-backend)))
APIのリクエストに対応する
Muuntajaを使うのがオススメです。
リクエストマップの Content-Type
をみて、bodyをパースし、 :params
に入れてくれます。
またレスポンスマップも同様に適切にbodyを変換してくれます。
ただしこのMiddlewareを通す前にレスポンスマップに Content-Type
を設定してしまうと、
動作しないことに注意が必要です。
(require '[muuntaja.middleware :as muu] '[ring.util.http-response :as res]) (defn hoge-api [_] ;; handlerではこれだけで返すようにする (res/ok {:data "foobar"})) (-> handler ;; こっちは :body-params を :params にマージしてくれるもの (muu/wrap-params) ;; こっちがパースして、 :body-param に設定してくれるもの (muu/wrap-format))
ルーティングしやすいようにURL末尾の /
を削除する
これはライブラリを使わず自分で書きます。
リクエストマップの :uri
が /foo/
のように末尾に /
に入っていることで、ルーティングがうまく機能しないケースがあります。
そこで次のようなMiddlewareを使うことで、この問題を吸収します。
(defn trailing-slash [path] (if (and (string? path) (not= path "/") (clojure.string/ends-with? path "/")) (subs path 0 (dec (count path))) path)) (defn wrap-trailing-slash "URLから末尾の'/'を削除" [handler] (fn [req] (-> req (update :uri r/trailing-slash) (update :path-info r/trailing-slash) handler))) (-> handler (wrap-trailing-slash))
URLとリクエストメソッドごとに異なるhandlerを呼ぶ
最後に、ルーティングとそれぞれのhandlerとを結合させる方法を紹介して、本記事を終わりとします。
ここでは、ルーティングライブラリとしてbidiを使い、Multimethodと組み合わせた方法を紹介します。 これは実際に弊社製品で使っている方法でもあります。
(require '[ring.util.http-response :as res]) (def routes ["/" [["" :route/home] ["foo" :route/foo] [true :route/not-found]]]) ;; (bidi/match-route routes "/foo") ;; => {:handler :route/foo} (defmulti handler ;; リクエストマップの :handler キーの値でディスパッチ (fn [request] (:handler request))) (defmethod handler :route/home (-> "Hello world" ; 本来はHTMLをここで作るようにする (res/ok) (res/content-type "text/html"))) (defmethod handler :route/foo (-> "foo" (res/ok) (res/content-type "text/html"))) (defmethod handler :route/not-found [_] (res/content-type (res/not-found) "text/html")) (extend-protocol bidi.ring/Ring ;; 元々のbidiはclojure.lang.Fnかclojure.lang.Varにしか対応していないので、 ;; keywordに対応を広げておく clojure.lang.Keyword (request [kw request _] ;; kwには :route/foo のようなkeywordがくる ;; マッチしたhandlerをリクエストマップに入れておくことで、 ;; マルチメソッドのディスパッチに使う (handler (assoc request :handler kw)))) (-> (bidi.ring/make-handler routes) ;; 以下にMiddlewareが続く )
--- 2020/07/29追加 ---
Content-Security-Policy
レスポンスヘッダを付与する
ring-middleware-cspを使います。
Clojureのmap
で定義したポリシーを適切にフォーマットして、レスポンスヘッダのContent-Security-Policy
に設定します。
:report-only?
オプションを使えば、Content-Security-Policy-Report-Only
ヘッダを使うこともできます。
他にも、固定のポリシーではなく、リクエストに応じた動的なポリシーを使ったり、nonceを簡単に設定できます。
実はこのmiddlewareは弊社が作成したものです。 使いづらい点や機能の追加などがあれば、issueやPRもお待ちしています。
(require '[ring-middleware-csp.core :refer [wrap-csp]]) (def policy {:default-src :none :script-src [:self] :style-src ["https://example.com" :unsafe-inline] :report-uri "/csp-report"}) (-> handler (wrap-csp {:policy policy})) ; Content-Security-Policyに "default-src 'none';script-src 'self';style-src https://example.com 'unsafe-inline';report-uri /csp-report"が設定される
更新履歴
- 2019/12/26 初版
- 2020/07/29
Content-Security-Policy
レスポンスヘッダを付与するを追加しました。
おわりに
概ね代表的なRing Middlewareについては紹介したように思いますが、また別な有用なものがあれば追記していこうと思います。
次回は状態管理を入れて、アプリケーション全体の構成を作る方法について紹介しようと思います。 またその時には実用的なサンプルアプリも紹介できればと思います。
それでは良いお年を!
トヨクモ株式会社ではClojureを書きたいエンジニア、PHP/Vue.jsを書きたいエンジニア、技術が好きなエンジニアを募集しております。
よろしければ採用ページをご覧ください。