このサイトについて

みんなのPython Webアプリ編 - フォームを使った認証

みんなのPython Webアプリ編 - フォームを使った認証

フォームを使った認証

BASIC認証やダイジェスト認証はとても手軽に利用できる認証方式です。多くのWebサーバが対応していますし、Webブラウザが自動的にヘッダに認証情報を送信してくれるので、認証状態を継続できます。

半面、認証状態が勝手に継続してしまうため、いわゆるログアウトが面倒なのが難点です。また、ブラウザを終了すると認証情報が消えてしまいます。そのため、Webブラウザを起動するたびに再度ログインする必要があるのです。

ログインとログアウトを明示的に行ったり、ブラウザを終了しても一定期間ログイン状態を保持できれば便利な場合があります。SNSのような一般的なWebアプリケーションではそのような機能を実現しているものがほとんどです。

このように、ログアウトやログインの期間を変えるなどといった認証の要求に応えるためにフォーム認証が利用されることがあります。BASIC認証のようにブラウザが表示するダイアログに認証情報を入力するのではなく、Web上のフォームを使ってユーザ名やパスワードといった認証情報を入力するのです。

フォーム認証の遷移

フォーム認証では、認証情報の入力にフォームを使います。フォームに入力されたユーザ名やパスワードを受け取るのはWebアプリケーションのプログラムです。つまり、フォームを使ったWebアプリケーションと同様に処理が進んでいくのです。入力されたユーザ名やパスワードに入力ミスがあった場合などには、誤りであることをユーザに知らせなければなりません。ログインを行うときの遷移について考える必要があります。

フォーム認証時の遷移については、一般的な解答があるわけではありません。そこでここでは、できるだけ「気の利いた」遷移について考えてみることにしましょう。

まず、ユーザが認証情報を間違えて入力した場合のことを考える必要があります。ユーザ名やパスワードを間違えて入力した場合は、間違いであることを表示しつつフォームの再表示をすると便利なはずです。ただしこの場合、ユーザ名のみ残してパスワードは消去します。また、ユーザ名とパスワードどちらか一方があっていても、ただ単に「間違っている」と表示します。どちらか一方が合っていることをユーザに分かるように表示してしまうと、パスワード破りに利用される可能性があるのです。

フォーム認証の場合は、ログインフォームを表示する専用のURL上のパスを設置して利用します。無認証状態でユーザがWebアプリケーションにアクセスしたときには、このパスにリダイレクトすることでフォームを表示します。ログイン後は、最初に表示しようとしたURLに再度リダイレクトするようにしましょう。ユーザはもともと最初にアクセスしたページを見たかったわけですから、自動的にそのURLに移動すれば利便性が増すはずです。

ログイン後、リダイレクトをするには、最初にいたページのURLの情報を保存しておく必要があります。GETリクエストのクエリとして渡すか、フォームにhiddenフィールドとして埋め込んでおくとよいでしょう。

フォーム認証での認証状態の継続

Webアプリはリクエストとレスポンスを繰り返すことで処理を進めていきます。このため、「ログイン中である」といった状態を維持するには、リクエストを使って付加的な情報を送り続ける必要がある、ということはすでに説明したとおりです。BASIC認証やダイジェスト認証では、Webブラウザがリクエストのヘッダに自動的に認証情報を付加します。フォーム認証の場合は、認証を継続するために必要な情報を受け渡す方法も自前で用意する必要があります。

フォームを使った認証では、Cookie(クッキー)を使って認証状態を維持する手法がよく採用されます。Cookieとは、Webブラウザ上にさまざまな値を保存するために利用される仕組みです。簡易なデータベースのようなもので、Webサーバのドメインやパスなどと関連付けて、Webブラウザ上にキーと値を保存できます。

Cookieに値を保存するためには、WebサーバやWebアプリケーションの送り出すレスポンスに必要な情報を書き出します。Cookieを保存、更新、削除するためにWebサーバが送り出すヘッダがあらかじめ決められています。Webブラウザはレスポンスのヘッダを見て、必要があればCookieのデータベースを更新します。

Webブラウザは、必要があればCookieの情報をWebサーバやWebアプリケーションに伝えます。これにはリクエストヘッダが使用されます。

図03 Cookieの仕組み

図03 Cookieの仕組み

つまり、Cookieを使うと、Webアプリケーションからの指令で、任意のデータをWebブラウザ上に保存できるわけです。保存したデータはヘッダを通じてWebアプリケーションで利用できます。大量のデータを保存することはできませんが、ちょっとした文字列であればCookieを使って保存できます。この方法を使うと、BASIC認証やダイジェスト認証で使われていたのと似た方法を使って、ログイン状態を継続できます。

Cookieを使って認証状態を継続する手法の利点は、認証状態をコントロールできる、ということです。Cookieはレスポンスのヘッダを使うことで簡単にコントロールできます。ログイン中はCookieの特定の文字列に「合言葉」となる文字列を保存しておき、ログアウトしたいときには合言葉を消せばよいわけです。また、Webブラウザを終了してもCookieを保存されたままにできます。Webブラウザを終了してもログイン状態を維持できるわけです。

PythonとCookie

Pythonの標準モジュールにはCookieというモジュールがあります。このモジュールを使うと、クライアントとWebサーバ間でやりとりされるヘッダを手軽に扱えます。Cookieモジュールにはいくつかのクラスが定義されています。SimpleCookieというクラスを使って、クライアントとサーバの間でやりとりされている「生のCookie」を覗いてみましょう。

まずはSimpleCookieを使って、Webサーバからクライアントに送られるCookieのヘッダを見てみましょう。SimpleCookieは、Pythonの辞書オブジェクトのように扱えます。インスタンスを作った後、辞書のようにキーを使って値を登録します。キーを入れ子にして、path(Cookieが有効になるパス)、expires(Cookieの有効期限)などを指定することもできます。

インタラクティブシェルを使ってSimpleCookieを使ってみましょう。以下の例では、まずfooというキーに文字列を設定しています。また、expiresを指定してCookieの有効期限を遠い未来に指定し、長い期間残るCookieを設定しています。いったんCookie用のヘッダを表示した後、さらにbarというキーを設定して、再度ヘッダを表示しています。

コマンドライン Cookieモジュールの利用例1:

:::python
>>> from Cookie import SimpleCookie
>>> c=SimpleCookie()
>>> c['foo']='cookie value'
>>> c['foo']['expires']='Thu, 1-Jan-2030 00:00:00 GMT' >>> print c.output()
Set-Cookie: foo="cookie value"; expires=Thu, 1-Jan-2030 00:00:00 GMT
>>> c['bar']='another cookie value'
>>> print c.output()
Set-Cookie: bar="another cookie value"
Set-Cookie: foo="cookie value"; expires=Thu, 1-Jan-2030 00:00:00 GMT

ブラウザがSet-Cookieヘッダを受け取ると、有効期限やパスなどを考慮してCookieの値を保存します。ブラウザが保存したCookieの値は、必要に応じてリクエストのCookieヘッダに記載されます。Webアプリケーションのプログラムでは、このヘッダを解釈して、Cookieの値を得ることができます。このようにして、Cookieの値をやりとりするのです。

SimpleCookieクラスを使うと、リクエストのヘッダを解釈してPythonのオブジェクトに変換できます。変換したオブジェクトは辞書のように扱えます。キーを指定してCookieの値を読み込むことができるのです。

インタラクティブシェルで試してみましょう。クライアントから送られてくるCookieヘッダを使ってSimpleCookieインスタンスを初期化します。すると、Cookieの値が辞書風のオブジェクトになって返ってきます。キーの値として格納されているのはMorselオブジェクトです。値を取るためにはvalueというアトリビュートを参照します。他にexpiresやpathといったアトリビュートを使うこともできます。

コマンドライン Cookieモジュールの利用例2:

:::python
>>> from Cookie import SimpleCookie
>>> c2=SimpleCookie('Cookie: foo=string1; bar=string2;')
>>> c2.keys()
['foo', 'bar']
>>> print c2['foo'].value
string1
2014-09-03 15:00