Nginx + mod_lua で認証フィルタを作ってみる

画像サーバーなどでログインチェックを Apache + mod_perl で実装していましたが、古代のクライアントへの対応はもう不要だろうということで、mod_perl やめたいし、もっとシンプルな実装にできそうだから nginx + mod_lua を試してみようとやってみました。

キャッシュとして memcached を使いたかったので https://github.com/agentzh/lua-resty-memcached も込で全部入れてくれる OpenResty をインストールしました。

OpenResty のインストール

$ tar xvf ngx_openresty-1.4.2.9.tar.gz
$ cd ngx_openresty-1.4.2.9
$ ./configure --prefix=/opt/ngx_openresty-1.4.2.9 --with-luagit
$ make
$ sudo make install

Lua モジュールについては HttpLuaModule ( http://wiki.nginx.org/HttpLuaModule ) にドキュメントがあります。Lua でコンテンツを返す場合は content_by_lua, content_by_lua_file を使いますが、認証処理を行うには access_by_lua, access_by_lua_file を使います。他にも header_filter_by… や rewrite_by…, body_filter_by… があります。 *_file の方は Lua を別ファイルとして読み込みます、compile 済みのファイルを指定することも可能です。 *_file でない方は Nginx の設定ファイルに直接コードを書きます。

今回は認証フィルタの話なので access_by_lua* についてのみ。

簡単な例

例えば、IE からのアクセスを拒否したい(403 Forbidden を返したい)という場合は次のように書けます(これだけなら Lua 不要ですよね、たぶん。Nginxはまだ詳しく知らない)。途中で ngx.exit せずに最後までいくか、途中で ngx.exit(ngx.OK) で終了すればアクセスは許可されます。

location / {
    access_by_lua '
        ngx.log(ngx.DEBUG, "User-Agent: ", ngx.var.http_user_agent)
        if string.match(ngx.var.http_user_agent, "MSIE") then
            ngx.exit(ngx.HTTP_FORBIDDEN)
        end
    ';
}

別サーバーへ問い合わせる

ログイン済みかどうかを確認するためには cookie を確認すると思います。LuaModule で Foo という名前の cookie の値を取得するには ngx.var.cookie_Foo とします。変数に入れるには

local cookie_value = ngx.var.cookie_Foo

とします。Cookie 名が変数に入っている場合は次のようにすることで取り出せます。

local cookie_name = "Foo"
local cookie_value = ngx.var["cookie_" .. cookie_name]

取り出した cookie の値からそのリクエストが有効かどうかを判定するために、別のサーバーに問い合わせる必要があります。memcached に入ってるなら直接そのサーバーに問い合わせるという方法もありますね。でもアプリで Consistent Hashing とかしてると困りますね。twemproxy 使ってれば大丈夫ですかね。
でも今回は Web サーバーに GET で問い合わせる方法を説明します。

ngx.location.capture() を使うことで、別の URI へリクエストを出すことができます。が、ngx.location.capture(“http://example.com/auth?session=XXXX”) などと直接別のサーバーを指定することはできません。
これをどうするかというと lua-nginx-module の紹介 ならびに Nginx+Lua+Redisによる動的なリバースプロキシの実装案 – hibomaのはてなダイアリー で紹介されているように

upstream _session_server {
    server session.example.com:80;
}
server {
    listen 80;
    server_name localhost;
    location / {
        access_by_lua '
            local sessid = ngx.var.cookir_SESSION_COOKIE
            if sessid then
                local res = ngx.location.capture("/_auth/session?sessid=" .. sessid)
                if res.status == 200 and string.match(res.body, "OK") then
                    ngx.exit(ngx.OK)
                end
            end
            ngx.exit(ngx.HTTP_FORBIDDEN)            
        ';
    }
    location /_auth {
        internal;
        rewrite ^/[^/]*/(.*) /$1 break;
        proxy_pass http://_session_server;
    }
}

てな感じで、/_auth などを経由して proxy させることができます。internal 指定しておくことで直接ブラウザからアクセスされないようにできます。ngx.location.capture() は subrequest として /_auth へアクセスするため、これへは access_by_lua が適用されません。

/ へアクセスしたら2回の問い合わせが発生してなんだろう?って悩んだが / でチェックした後に、内部的に /index.html へアクセスし直すのでした。

Nginx ほぼ初めてだから location / で access_by_lua 設定したら、他の location で認証が効かなくてハマった。全体に適用するなら location の外に設定すべきですね。

memcached を使う

毎回別のサーバーに問い合わせるのはよろしくないので、ローカルの memcached にキャッシュさせたいと思います。/aaa へのアクセスに memcached からの GET aaa の結果をそのまま返すという利用法であれば MemcachdModule でできるようですが、今回の用途では https://github.com/agentzh/lua-resty-memcached の方がマッチしそうでした。

使い方はドキュメントに書いてあるとおりで、

local memcached = require "resty.memcached"
local memc, err = memcached:new()
memc:set_timeout(1000) -- 1 sec
memc:connect("127.0.0.1", 11211)
memc:set("dog", 32)
local res, flags, err = memc:get("dog")
memc:set_keepalive(10000, 100)
-- memc:close()

な感じです、エラー処理を端折ってますが、使うときにはちゃんと書きましょう。
set() は3番目の引数に expire (秒) を、4番目に flag を指定できます。
close() の代わりに set_keepalive() を使うことでその connection を connection pool に入れることができます。毎度毎度接続・切断を繰り返すのはよろしくないので pool しましょう。1番目の引数が idle timeout (ミリ秒) で2番目の引数が pool の最大値です。

コンパイル

Lua のコンパイルは OpenResty で一緒にインストールされた luajit を使います。

$ luajit/bin/luajit -b auth-filter.lua auth-filter.luac

この .luac を access_by_lua_file で指定することが可能です。

Lua の文法

ちょっとだけ。今のところ Nginx でサポートされているのは Lua 5.1 のようです。
http://www.lua.org/manual/5.1/manual.html

コメントは SQL と同じかな?「–」から行末までがコメントとなります。

配列は PHP と似ていて、連想配列だけみたいです。table と呼ぶみたいです。添字は0ではなく1から始まります。#table で要素数が得られます、ただし、欠番や値がnullの要素があると、その前まででの数が返ります。ipairs(), pairs() というイテレータがあります。ipairs() が配列用のようです。1から#table まで loop させられます。pairs() は key と value が返ります。ipairs では欠番以降を処理できませんが、pairs() であれば全部処理できます。

文字列連結は「..」です。

変数のスコープはデフォルトが Global なので、Perl の「my」のように「local」で宣言しましょう。

if, for, while, function の終了は end です。loop から抜けるのは break で、function が値を戻すのは return.

local t = { "A", "B" }
table.insert(t, "C")
table.remove(t, 2)
table.concat(t, ",")
for i, v in ipairs(t) do
    ngx.log(ngx.DEBUG, string.format("[%d]: %s", i, v))
end
local i = 1
while i <= #t do
    ngx.log(ngx.DEBUG, string.format("[%d]: %s", i, t[i]))
    i = i + 1 -- += や ++ は無い
end
local t = { a="A", b="B", c="C" }
for k, v in pairs(t) do -- 順序は保証されない
    ngx.log(ngx.DEBUG, k, ": ", v)
    if k == "A" then
        ngx.log(ngx.DEBUG, "not A")
    else
        ngx.log(ngx.DEBUG, "not A")
    end
end

それでは良い Lua 生活を〜〜

コメント