buddyとは
buddyはClojureのユーザ認証ライブラリです。主にAndrey Antukh氏により開発が進められているOSSであり、2条項BSDライセンスで配布されています。ClojureでWebアプリ開発を行うときに利用できます。
同種の認証ライブラリとしてはfriendが有名ですが、buddyのほうがシンプルにまとまっている印象を受けます。
以前、テンクーチームの一員として参加したClojure Cup(Clojure/ClojureScriptの48時間プログラミングコンテスト)で制作したニュースアプリ news.async でも使われています。
インストール
Leiningenを使っているなら、project.clj
のdependenciesに以下を加えましょう。
[buddy "1.3.0"]
上記は、buddyを構成するbuddy-core, buddy-sign, buddy-hashers, buddy-authを全て依存解決してくれます。
[buddy/buddy-core "1.3.0"]
[buddy/buddy-auth "2.0.0"]
のように、コンポーネントごとに解決することも可能です。
なお、buddyはJDK7/8のみの対応なのでご注意ください。
使い方
buddyにはローレベルなハッシュアルゴリズムなども含まれていますが、本稿では使用機会が多いであろうRing/Compojureでbuddyを利用する方法を紹介します。その他詳細な使用方法は公式のドキュメントを参照ください。
アクセスルール
どのリクエストに対して認証を行うか、というルールを記述します。
下記のようなマップを用意すると、/admin/
で始まるルートに対して認証をかけます。
{:pattern #"^/admin/.*"
:handler admin-access}
RingベースのWebアプリの場合、wrap-access-rules
を使ってルールを適用します。
(ns foo.handler
(:require [buddy.auth :refer [authenticated?]]
[buddy.auth.accessrules :refer [wrap-access-rules]]))
(def app
(let [rules [{:pattern #"^/admin/.*" :handler authenticated?}]]
(-> app-routes
(wrap-access-rules {:rules rules :policy :allow}))))
認証と承認
buddyではAuthentication(認証)とAuthorization(承認)が明確に分けられています。
認証プロセスのバックエンドとしてはHttp Basic, Session, Token, SignedTokenが用意されています。
認証プロセスを適用するにはwrap-authentication
とwrap-authorization
を使用します。
(ns foo.handler
(:require [buddy.auth.backends.session :refer [session-backend]]
[buddy.auth.middleware :refer [wrap-authentication wrap-authorization]]))
(def app
(let [backend (session-backend {:unauthorized-handler unauthorized-handler})]
(-> app-routes
(wrap-authentication backend)
(wrap-authorization backend))))
実例
実際にCompojureを使ってユーザ認証を行うだけのWebアプリを作ってみましょう。
デモ
作ったアプリをHerokuにデプロイしておきました。また、ソースコードはGithubに公開してあります。
"To Secure Area" ボタンを押すと、ログインしていなければログインページに飛びます。適当なユーザ名・パスワードを入力して "Log in" ボタンを押すと、/admin
が見られるようになります。"Logged in as [ユーザ名]" という表示がされているはずです。"Log out" を押すとログアウトします。
レシピ
それでは上記デモのWebアプリを順番に作っていきましょう。Herokuで動かすために必要な部分は省略するので、Github上のソースとは異なる部分があります。
まず、Compojureのテンプレートを使って、Leiningenプロジェクトを作成します。
$ lein new compojure cljac14-buddy
project.clj
を開いて、buddyとページ作成に使用するHiccupを依存関係に追加します。
:dependencies [[org.clojure/clojure "1.8.0"]
[compojure "1.5.1"]
[ring/ring-defaults "0.2.1"]
[buddy "1.3.0"] ; <= 追加
[hiccup "1.0.5"]] ; <= 追加
準備ができたので、handler.clj
に処理を書いていきます。まず必要なライブラリを読み込みます。
(ns cljac14_buddy.core.handler
(:require [compojure.core :refer :all]
[compojure.route :as route]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[ring.middleware.anti-forgery :refer [*anti-forgery-token*]] ; <=
[ring.util.response :refer [redirect]] ; <=
[buddy.auth :refer [authenticated?]] ; <=
[buddy.auth.backends.session :refer [session-backend]] ; <= 追加
[buddy.auth.middleware :refer [wrap-authentication ; <=
wrap-authorization]] ; <=
[buddy.auth.accessrules :refer [wrap-access-rules]] ; <=
[hiccup.core :refer [html]])) ; <=
次にルーティングを定義します。/admin
にアクセスしたとき、認証されていなければログインページにリダイレクトするようにします。その他にログアウトのためのルートも準備しておきましょう。
(defroutes app-routes
(GET "/" [] "Hello, buddy!")
(GET "/admin" req (admin req)) ; <=
(GET "/login" req (login-get req)) ; <= 追加
(POST "/login" req (login-post req)) ; <=
(GET "/logout" [] (logout)) ; <=
(route/not-found "Not Found"))
app
にはアクセスルールとして/admin
を指定します。また、認証プロセスのバックエンドとしてSessionを利用します。すでに述べたようにwrap-access-rules
, wrap-authentication
, wrap-authorization
を用いて適用していきます。
(def app
(let [rules [{:pattern #"^/admin$" :handler authenticated?}]
backend (session-backend {:unauthorized-handler unauthorized-handler})]
(-> app-routes
(wrap-access-rules {:rules rules :policy :allow})
(wrap-authentication backend)
(wrap-authorization backend)
(wrap-defaults site-defaults))))
/admin
にアクセスしたとき、承認済みでなければunauthorized-handler
が呼ばれます。unauthorized-handler
では、認証されていなければログインページにリダイレクトするようにします。認証後に元のページに戻ってこられるよう、URLにnext
パラメータを付加しておきます。
(defn unauthorized-handler
[req meta]
(if (authenticated? req)
(redirect "/")
(redirect (format "/login?next=%s" (:uri req)))))
各リクエストに対するレスポンスを記述していきます。
adminページではログインユーザ名が表示されるようにしておきましょう。
(defn admin
[req]
(str "Logged in as " (name (get-in req [:session :identity]))))
ログインページにGETリクエストがきたときはログインフォームを表示します。
(defn login-get
[req]
(html
[:form {:method "post"}
[:input {:type "text" :name "username"}]
[:input {:type "password" :name "password"}]
[:input {:type "hidden" :name "__anti-forgery-token" :value *anti-forgery-token*}]
[:input {:type "submit"}]]))
ここではHiccupを用いてレンダリングしています。__anti-forgery-token
はCSRFを防ぐRing-Anti-Forgeryのトークンです。submitボタンが押されると/login
にPOSTリクエストが送信されます。
バックエンドにセッションを利用する場合、request :session :identity
にトークンを保存します。
今回は単純にユーザ名を保存しておきます。
(defn login-post
[req]
(let [username (get-in req [:form-params "username"])]
(-> (redirect (get-in req [:query-params "next"] "/"))
(assoc-in [:session :identity] (keyword username)))))
これだけで認証は終了です。
元のページ (admin) にリダイレクトされ、自分の名前が表示されていれば成功です。
せっかくなので、ログアウトもできるようにします。
ログアウトは簡単で、request :session
を空にしてしまうだけです。
(defn logout
[]
(-> (redirect "/")
(assoc :session {})))
おわりに
本稿では、buddyを用いてRing/CompojureベースのWebアプリにユーザ認証を導入する方法を紹介しました。今回は説明のため非常に簡単なWebアプリを作りましたが、実際のWebアプリで利用するにはパスワードをハッシュ化してデータベースに保存したり、セッションの永続化を行ったりといったことが必要になります。
実を言うと直前まで別ネタで書こうとしていたため、少し説明が不十分かもしれませんが、お役に立てば幸いです。