Help us understand the problem. What is going on with this article?

buddyでユーザ認証

More than 1 year has passed since last update.

buddyとは

buddyはClojureのユーザ認証ライブラリです。主にAndrey Antukh氏により開発が進められているOSSであり、2条項BSDライセンスで配布されています。ClojureでWebアプリ開発を行うときに利用できます。

同種の認証ライブラリとしてはfriendが有名ですが、buddyのほうがシンプルにまとまっている印象を受けます。

以前、テンクーチームの一員として参加したClojure Cup(Clojure/ClojureScriptの48時間プログラミングコンテスト)で制作したニュースアプリ news.async でも使われています。

インストール

Leiningenを使っているなら、project.cljのdependenciesに以下を加えましょう。

project.clj
[buddy "1.3.0"]

上記は、buddyを構成するbuddy-core, buddy-sign, buddy-hashers, buddy-authを全て依存解決してくれます。

project.clj
[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-authenticationwrap-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に公開してあります。

buddy-example-s.png

"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を依存関係に追加します。

project.clj
: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に処理を書いていきます。まず必要なライブラリを読み込みます。

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にアクセスしたとき、認証されていなければログインページにリダイレクトするようにします。その他にログアウトのためのルートも準備しておきましょう。

handler.clj
(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を用いて適用していきます。

handler.clj
(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パラメータを付加しておきます。

handler.clj
(defn unauthorized-handler
  [req meta]
  (if (authenticated? req)
    (redirect "/")
    (redirect (format "/login?next=%s" (:uri req)))))

各リクエストに対するレスポンスを記述していきます。

adminページではログインユーザ名が表示されるようにしておきましょう。

handler.clj
(defn admin
  [req]
  (str "Logged in as " (name (get-in req [:session :identity]))))

ログインページにGETリクエストがきたときはログインフォームを表示します。

handler.clj
(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リクエストが送信されます。

Login page

バックエンドにセッションを利用する場合、request :session :identityにトークンを保存します。
今回は単純にユーザ名を保存しておきます。

handler.clj
(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) にリダイレクトされ、自分の名前が表示されていれば成功です。

Logged in as ...

せっかくなので、ログアウトもできるようにします。
ログアウトは簡単で、request :sessionを空にしてしまうだけです。

handler.clj
(defn logout
  []
  (-> (redirect "/")
      (assoc :session {})))

おわりに

本稿では、buddyを用いてRing/CompojureベースのWebアプリにユーザ認証を導入する方法を紹介しました。今回は説明のため非常に簡単なWebアプリを作りましたが、実際のWebアプリで利用するにはパスワードをハッシュ化してデータベースに保存したり、セッションの永続化を行ったりといったことが必要になります。

実を言うと直前まで別ネタで書こうとしていたため、少し説明が不十分かもしれませんが、お役に立てば幸いです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした