実験用データベースを作成し, それをRubyプログラムから利用する例を示す。 日本語を含む例にするため,端末とRubyプログラムをUTF-8にする。
UTF-8で日本語入力をする端末は,Emacs内で M-x shell としてシェルを起動してから C-x C-m p とタイプし,
Coding-system for output from the process: utf-8 Coding-system for input to the process: utf-8
と入力する。
この設定後データベース作成に移る。
/usr/local/bin/sqlite3 -interactive fruits.sq3 create table fruits(item, at, memo); insert into fruits values ("りんご", 150, 'ふじ'); insert into fruits values ("みかん", 30, '有田'); select * from fruits; りんご|150|ふじ みかん|30|有田
RubyからSQLite3データベースをアクセスするには sqlite3 ライブラリを用いる。
#!/usr/bin/env ruby # coding: utf-8 require 'sqlite3'
SQLite3データベースファイルをアクセスする。
db = SQLite3::Database.new("DBファイル") db.execute(SQL文)
実験用のデータベースを開き,"select *"
する例を示す。
#!/usr/bin/env ruby # coding: utf-8 require 'sqlite3' db = SQLite3::Database.new("fruits.sq3") result = db.execute("select * from fruits;") result.each do |row| puts row.join(",") end
selectした結果はイテレータになり,eachで検索結果を 1レコードずつ取り出せる(row)。rowは配列になり,カラムごとに取り出せる。
続いてinsertする例を示す。
#!/usr/bin/env ruby # coding: utf-8 require 'sqlite3' require 'kconv' db = SQLite3::Database.new("fruits.sq3") sql = "insert into fruits values (?, ?, ?)" while true STDERR.print 'Fruit: ' fr = gets break if fr==nil # 入力終端でbreak STDERR.print 'unit of price: ' pr = gets.to_i STDERR.print 'memo: ' memo = gets.chomp db.execute(sql, fr.chomp.toutf8, pr, memo) end STDERR.puts "Done"
上のプログラムにあるようにレコード挿入のSQL文は
insert into fruits values (?, ?, ?)
となっている。Rubyプログラムからは ? の
実行して2つほどデータを入れてみる(shellモードバッファで)。
./fr-2.rb Fruit: いちご unit of price: 20 memo: おとめ心 Fruit: 梨 unit of price: 120 memo: 幸水 Fruit: [C-d]Done ./fr-1.rb りんご,150,ふじ みかん,30,有田 いちご,20,おとめ心 梨,120,幸水
Rubyプログラムからレコード挿入を行なうには,insert構文を含む SQL文のデータの部分にプレースホルダ(`?')を指定し,その個数だけ 後続する引数に対応する値を記述すればよい。
CGIプログラムはWebサーバプログラムの実行権限で起動される。 そのため,他人でも書き込める設定にしなければならない。また, sqlite3は,データベースファイルを更新するときに,ファイルと同じ ディレクトリに一時ファイルを作成するので,データベースファイルを 置くディレクトリも書き込める設定にする必要がある。 ただし,CGIプログラムと同一ディレクトリにするのは危険なので, 全員書き込み可能なサブディレクトリを作成し,そこにデータベースファイルを 移動する。
chmod a+w fruits.sq3 mkdir -m 1777 tmpdb mv fruits.sq3 tmpdb
拡張子 .rb のファイルがCGIとして機能するよう .htaccess
ファイルを以下のように作成する。
AddHandler cgi-script .rb AddType "text/html; charset=utf-8" .rb Options +ExecCGI
CGIプログラムは以下の3つの構成をとる。
これらの処理を行なうプログラムの例を示す
(ソース:fruits.rb,
fruits.css
)。
例示した fruits.rb はいたずらに弱い。 完ぺきな対策は難しいが現実的には,自動的に入力フォームCGIを探しては spamを書き込んでくるロボットプログラムからの登録を防げればほぼ十分である。
そのためには,入力フォーム内に人間にしか判断できない ヒント情報を入れておき,書き込み時にそれを判定するような仕組を入れる。 一般的には画像化した文字を人間に読み取らせるCAPTCHA という自動アクセス抑止機構があるが, それよりはるかに簡易なものでも一定の抑止効果が得られる。 人気サイトでない限り,簡易対策で始めてもよい。
これらの処理を行なうプログラムの例を示す
(ソース:fruits2.rb
)。
この処理は,CGIフォームを自動的に探して適当にコメント欄らしき 所にspam文を書いて来るタイプのロボットには有効であるが, この対策法を理解して適切な値を送るようにプログラムされたものには無力である。 そのようなものにも対策するなら,以下のような処理を考える。
複雑にすればするほどspam避けの効果は高まるが,正当な利用者の 手間が増え利用意識が低下する。攻撃される確率といたずらによる 被害からの回復のコストを考えて適度な対策を選択する必要がある。
既存のレコードを修正あるいは削除する機能も欲しい。 この場合は,レコードの一覧表示のとき,各レコードに振られた idをhidden変数に添えて更新リンクを示す。そのため,dbの第1カラムに idを追加して設計する。つまり,これまで
item | at | memo |
品目 | 単価 | メモ |
だったものに識別子となるカラムidを追加し,
id | item | at | memo |
識別子 | 品目 | 単価 | メモ |
とする。既存の表にカラムを追加することはできるが,通常末尾にしか 足せないので新規に表を作る。ここでは,同じデータベース内に kudamono という表を上記のカラム構成で構築する。その上で, 既存の fruits 表にあるレコードを item カラム以降にコピーする。
/usr/local/bin/sqlite3 -interactive fruits.sq3 create table kudamono(id integer primary key autoincrement, item, at, memo); insert into kudamono(item, at, memo) select * from fruits; select * from kudamono; 1|りんご|150|ふじ 2|みかん|30|有田 3|いちご|20|おとめ心 4|梨|120|幸水
このようにinsert文の引数にselect文を入れることで, 他の表のカラムをコピーすることができる。
この表にあるレコードを一覧表示しつつ,品目名に修正リンクを,
また品目名の前に削除マークを付けるようにした
例を示す(ソース:
kudamono.rb
)。
データの修正や削除を,書き込んだユーザのみに許可する仕組を入れてみる。 このためには,各レコードに書き込み者を保持するカラムを追加するとともに, ログイン機能を付ける。
なお,ここのログイン機能は他者の乗っ取りなどの悪用にたいしては ほとんど抑止効果がない。もともとWebは誰にでもアクセスできることを 目指して設計されたものなので,厳密なユーザ認証を行なうには かなりのコストがかかる。改ざんやいたずらが全く許されないものを 扱うのであれば,Webサーバ(httpd)にはデータベースの書き込みができないよう chmod してWebインタフェースを禁止し, データベースを直接扱うコマンドラインインタフェースを利用するような 設計をするのがもっとも簡便で強固である。 どうしてもWebインタフェースでの書き込みをさせたい場合は, データベース書き込みできるユーザ権限を持つhttpdを別途起動し, VPN回線からのアクセスのみ許可するなどの方法が考えられる。 いずれにしても,外向けのWebサーバプロセスが書き込みできるような状態では データは守り切れないと心得る。
ログインさせるためにはユーザの情報が必要で,これも表で持たせる。 ここでは,以下の情報を保持するものとする。
表を設計する。
uid (integer primary key autoincrement) |
uname | pswd | name | skey |
/usr/local/bin/sqlite3 -interactive kudamono.sq3 create table user(uid integer primary key autoincrement, uname, pswd, email, name, skey);
パスワードはシステムのcrypt(3)関数を使って暗号化する。 crypt()関数は以下のように使用して暗号化文字列を得る。
文字列.crypt(SALT)
SALT は,ひとつの単語(つまりパスワード)に対する符号化文字列が
単一にならないようにするための味付け用ランダム文字列である。単一になると
符号化した文字列から,平文パスワードが容易に推測できる。
またSALTには,符号化アルゴリズムを選ばせる役割も持たせられる。
最近のシステムで広く利用できるMD5を利用する。MD5符号化を行なうには,
SALTに "$1$xxxx"
のような文字列(xxxxの部分はランダム)を指定する。
irb "foo".crypt("$1$hoge") => "$1$hoge$Z/OJvXjat3gzS6ZMyA./q1"
SALT自身が結果文字列に含まれていることに注意する。結果文字列を SALTに指定して再度crypt()関数を呼んでみる。
"foo".crypt("$1$hoge$Z/OJvXjat3gzS6ZMyA./q1")
=> "$1$hoge$Z/OJvXjat3gzS6ZMyA./q1"
SALT指定では,3つ目の$以降が無視されるので "$1$hoge"
を指定したのと同じ結果となる。
さて,ユーザ情報にパスワードを入れる場合,crypt文字列を格納する。 これは容易には元に戻せないので誰も(管理者すら)元のパスワードが分からない。 ログイン可否の判断は,ユーザが送って来たパスワードを, 格納された符号化文字列をSALTにしてcryptしたものと符号化文字列を比較する。 同じであれば,送って来たパスワードが正当なものと判断できる。
以上より,ユーザ管理に必要な処理は以下のようになる。
さらにユーザ削除機能なども必要になるだろう。
なお,今回のユーザ認識処理はそれなりの防御でよい場合の方式であり, しっかりした認証を行なう場合は暗号回線の確保や,そもそもWebを用いない 編集方式の提供などを考えるべきことを再強調しておく。
1人目のユーザはsqlite3コマンドで作ってみる。 my@add.ress を自分のメイルアドレスに置き換えて作業例を見よ。 なお,パスワードの符号化文字列は別の端末でirbを利用して得る。
/usr/local/bin/sqlite3 -interactive kudamono.sq3 insert into user(uname, pswd, email, name) values ('my@add.ress', "$1$hoge$Z/OJvXjat3gzS6ZMyA./q1", 'my@add.ress', "太郎");
この処理の動きを理解しやすくするためのRubyプログラムを示す
(useradm.rb
)。
#!/usr/bin/env ruby19 # coding: utf-8 require 'sqlite3' if ARGV[1] == nil STDERR.puts "第1引数にユーザ名(届くメイルアドレス)を" STDERR.puts "第2引数にデータベースファイルを指定して下さい。" exit(2) end uname = ARGV[0] # ユーザ名 string = [*'a'..'z', *'A'..'Z', *'*'..'9'] # 英数記号($が入らないように) salt = string.sample(4).join # ランダム文字列のSALT pswd = string.sample(9).join # ランダム文字列の初期パスワード crpt = pswd.crypt("$1$"+salt) # MD5化 db = SQLite3::Database.new(ARGV[1]) r = db.execute("select uid, email from user where uname=?", uname)[0] if r # 既存ユーザの場合 r=[uid, email] db.execute("update user set pswd=? where uid=?", crpt, r[0]) else # 新規作成 db.execute("insert into user(uname, pswd, email, name) values(?,?,?,?)", uname, crpt, uname, uname) end printf("Set pswd for [%s] to [%s]\n", uname, pswd)
./useradm.rb my@add.ress tmpdb/fruits.sq3
Set pswd for [my@address] to [huj-XR4*gp]
Webから行なえるようにしてもよい。
データベースアクセスWebに登録したユーザとパスワードを要求するようにする。 Webでのユーザ認識処理が難しいのは,ログイン処理をした情報を クリックした先の次のセッションに持ち越せないところにある。 それを簡易的に行なうためにcookieを利用する。
cookieは,Webサーバがブラウザに送った「変数=値」という文字列情報を, 次回以降のアクセス時にブラウザからWebサーバ側に送り返してもらうことで アクセスのセッション管理を擬似的に可能にすることのできる仕組みである。 サーバ側のCGIでは,最初に送るContent-type行と同時に,
Set-Cookie: 変数=値; expires=時刻
という並びを送信する。HTML本文を送信する前なので,最初に ユーザのパスワード確認を行ない,適合したらすぐにセッションキーを送信し, 以後はそのセッションキーをcookieに含むアクセスのみ許可するものとする。 セッションキーは,認証に通ったユーザのみが取得・利用し得る文字列で あればなんでもよく,ここでは接続者のIPアドレスとランダム文字列を利用する。 expiresは,cookieの有効期限で時刻の書式は
Tue, 16-10-2012 02:52:58 GMT
のような形式でUTCで指定する。
サンプルソースでは5時間後の時刻でこれを生成している。
例を示す
(ソース: fruits3.rb
)。