Toyokumo Tech Blog

トヨクモ株式会社の開発本部のブログです。

Clojure Ring Middleware大全

これはClojure Advent Calendar 2019の21日目の記事です。

そして、こちらの記事の続きにもなっています。

tech.toyokumo.co.jp

本記事では、RingのMiddlewareについて解説していきます。 基本的に上記の記事を理解いただいた前提で話を進めていこうと思います。

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-Typetext/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/csvContent-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-Typeapplication/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-Typemultipart/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"が設定される

更新履歴

おわりに

概ね代表的なRing Middlewareについては紹介したように思いますが、また別な有用なものがあれば追記していこうと思います。

次回は状態管理を入れて、アプリケーション全体の構成を作る方法について紹介しようと思います。 またその時には実用的なサンプルアプリも紹介できればと思います。

それでは良いお年を!


トヨクモ株式会社ではClojureを書きたいエンジニア、PHP/Vue.jsを書きたいエンジニア、技術が好きなエンジニアを募集しております。

よろしければ採用ページをご覧ください。