前回Docker Composureを使って作成したCompojureプロジェクトに、curlからPOSTするとInvalid anti-forgery token
が発生してしまいました。デフォルトでCSRF対策用トークンを追加しないとPOSTはできない仕様になっています。開発中はanti-forgery
を無効にした方が簡単です。いろいろ調べるとStackOverflowにあった方法でうまくいきました。
Invalid anti-forgery token
以下のようにPOST用ハンドラをhandler.cljに作成してcurlでPOSTするとInvalid anti-forgery token
が発生します。
$ curl -X POST -d "name=masato" localhost:3000/greeting
<h1>Invalid anti-forgery token</h1>
StackOverflowでも議論されていました。以下のサイトを参考に作業していきます。
プロジェクト
前回と同じようにDocker Composeでプロジェクトを管理します。
$ cd ~/clojure_apps
$ tree .
.
├── Dockerfile
├── cookies
├── docker-compose.yml
└── m2
Dockerホストの作業ユーザーと同じuidのユーザーをDockerコンテナにも作成します。MavenのjarファイルはVOLUMEに指定してDockerホストにマップしてコンテナ間で共有します。
FROM clojure
MAINTAINER Masato Shimizu <ma6ato@gmail.com>
WORKDIR /usr/src/app
RUN apt-get update && apt-get install sudo net-tools && \
rm -rf /var/lib/apt/lists/*
RUN adduser --disabled-password --gecos '' --uid 1000 docker && \
adduser docker sudo && \
echo 'docker ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \
mkdir /home/docker/.m2 && \
chown -R docker:docker /usr/src/app /home/docker/.m2
VOLUME /home/docker/.m2
USER docker
RUN lein
ENTRYPOINT ["lein"]
ローカルにClojureのイメージをビルドします。
$ docker build -t clojure .
docker-compose.ymlのworking_dir
ディレクティブはCompojureプロジェクトを作成する前はコメントアウトしておきます。
lein: &defaults
image: clojure
volumes:
- .:/usr/src/app
- ./m2:/home/docker/.m2
# working_dir: /usr/src/app/hello-world
ports:
- "3000:3000"
- "3449:3449"
cljslein:
<<: *defaults
ports:
- "9000:9000"
lein new
でcompojureのプロジェクトを作成します。
$ cd ~/clojure_apps
$ docker-compose run --rm --service-ports lein new compojure hello-world
作成したCompojureプロジェクトをworking_dir
ディレクティブに指定します。
lein: &defaults
image: clojure
volumes:
- .:/usr/src/app
- ./m2:/home/docker/.m2
working_dir: /usr/src/app/hello-world
ports:
- "3000:3000"
- "3449:3449"
cljslein:
<<: *defaults
ports:
- "9000:9000"
project.cljファイルにはJSON作成のcheshireを依存関係に追加します。
(defproject hello-world "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:min-lein-version "2.0.0"
:dependencies [[org.clojure/clojure "1.6.0"]
[compojure "1.3.1"]
[ring/ring-defaults "0.1.2"]
[cheshire "5.4.0"]]
:plugins [[lein-ring "0.8.13"]]
:ring {:handler hello-world.handler/app}
:profiles
{:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
[ring-mock "0.1.5"]]}})
GET "/"ハンドラでCSRFトークンを出力するように変更します。トークンはRing-Anti-Forgeryの*anti-forgery-token*
を評価します。動的なvarのスペシャル変数です。セッションに保存しているcookie-storeのキーはランダムで適当に作成しました。
またJSONでレスポンスを返すためにcheshireのgenerate-stringを使います。
(ns hello-world.handler
(:require [compojure.core :refer :all]
[compojure.route :as route]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[ring.middleware.session :refer [wrap-session]]
[ring.middleware.anti-forgery :refer :all]
[ring.middleware.session.cookie :refer (cookie-store)]
[cheshire.core :refer :all]))
(defn greeting-handler [request]
(let [name (get-in request [:params :name])]
(str "Hi, " name)))
(defroutes app-routes
(GET "/" [] (generate-string {:csrf-token
*anti-forgery-token*}))
(POST "/greeting" [] greeting-handler)
(route/not-found "Not Found"))
(def app
(-> app-routes
(wrap-defaults site-defaults)
(wrap-session {:cookie-attrs {:max-age 3600}
:store (cookie-store {:key "ahY9poQuaghahc7I"})})))
Ringサーバーをheadlessで起動します。
$ cd ~/clojure_apps
$ docker-compose run --rm --service-ports lein ring server-headless
...
SelectChannelConnector@0.0.0.0:3000
Started server on port 3000
最初に用意したハンドラを実行してCSRFトークンを出力します。
$ curl -X GET --cookie-jar cookies "http://localhost:3000/"
{"csrf-token":"FxyHMRjb5I9wwxskEo2h2uXhtU/CNUo38xLDTa/2fJp7QhZ/Wo7hi4zRbey9yUZgRKfe3y1uS66K8+kA"}
JSONで取得したcsfr-tokenをHTTPヘッダのX-CSRF-Token
に指定してcurlからPOSTすると成功します。
$ curl -X POST \
--cookie cookies \
-F "name=masato" \
-H "X-CSRF-Token: FxyHMRjb5I9wwxskEo2h2uXhtU/CNUo38xLDTa/2fJp7QhZ/Wo7hi4zRbey9yUZgRKfe3y1uS66K8+kA" \
"http://localhost:3000/greeting"
Hi, masato
anti-forgeryを無効にする場合
CSRF対策を無効にする場合は、site-defaults
のanti-forgeryキーの値をfalseにして起動します。
...
(def app (wrap-defaults app-routes (assoc-in site-defaults [:security :anti-forgery] false)))