サイトのテストアップ時などちょっとした認証をかけたいときにbasic認証をよく使っているのですが、
去年の暮れに「basic認証のことを知ってるようで全然知らないな」と思う出来事があったので
basic認証のことを少し調べました。
basic認証の処理の簡単にかくと以下の通りです
basic認証で行われているwebサーバ側の処理をざっくりとまとめると
という2つになるのですが、これをwebサーバにやらせるためにwebサーバの設定ファイルに
AuthType Basic
AuthName "Secret Zone"
AuthUserFile /etc/httpd/.htpasswd
Require user secret
というような設定を書いているわけです。
実はこの処理は自分で実装することが可能で、python3 + django1.8で書いてみるとこんな感じになります。
とりあえず、作業用ディレクトリを作ります。
$ mkdir basicauth_test
$ cd basicauth_test
pyvenvで仮想環境を作ってdjango1.8をインストールします。
$ pyvenv virtualenv
$ . virtualenv/bin/activate
(virtualenv)$ pip install django==1.8
テスト用のプロジェクトとアプリケーションを作成します。
(virtualenv)$ django-admin startproject testproject
(virtualenv)$ cd testproject
(virtualenv)$ ./manage.py startapp basicauth
これで準備ができたので、後はコードを書いていきます。
まずは、https://サーバのIP:8000/にアクセスが来たときにhello worldというテキストを返すだけの処理を書きます。
testproject/urls.py
# -*- coding: utf-8 -*-
from django.conf.urls import url
# /にアクセスが来たらbasicauth/views.pyのindexを呼ぶ
urlpatterns = [
url(r'^$', 'basicauth.views.index', name='index', ),
]
basicauth/views.py
# -*- coding: utf-8 -*-
from django.http import HttpResponse
def index(request):
# hello worldというテキストをレスポンスとして返す
return HttpResponse('hello world')
コードを入力したら
(virtualenv)$ ./manage.py runserver 0.0.0.0:8000
で開発サーバを起動し、ブラウザでhttps://サーバのIP:8000/にアクセスするとhello worldが返ってくると思います。
これにbasic認証の処理を追加し、ID:HELLO, PASSWORD: WORLD!!が入力されたときだけhello worldを返すようにします。
basicauth/views.py
# -*- coding: utf-8 -*
import base64
from django.http import HttpResponse
def index(request):
# もしリクエストヘッダにAuthrizationがなければ401を返す。
if 'HTTP_AUTHORIZATION' not in request.META:
return _http401()
"""
Basic認証の場合のAuthrizationのフォーマットは
Basic ユーザID:パスワードをbase64エンコードした値
なので、これを解析してIDとPASSWORDを抽出、
ID: HELLO, PASSWORD: WORLD!!のときだけhello worldを返す
"""
(authscheme, base64_idpass) = request.META['HTTP_AUTHORIZATION'].split(' ', 1)
if authscheme.lower() != 'basic':
return _http401()
idpass = base64.decodestring(base64_idpass.strip().encode('ascii')).decode('ascii')
(id_, password) = idpass.split(':', 1)
if id_ == 'HELLO' and password == 'WORLD!!':
return HttpResponse('hello world')
else:
return _http401()
def _http401():
# WWW-Authenticate付きのレスポンスを返す処理
response = HttpResponse("Unauthorized", status=401)
# WWW-Authenticateヘッダを追加し認証スキームにbasic, realmに"basic auth test"を設定
response['WWW-Authenticate'] = 'Basic realm="basic auth test"'
return response
また、
(virtualenv)$ ./manage.py runserver 0.0.0.0:8000
で開発サーバを起動し、ブラウザでアクセスするとID/PASSWORDが求められるようになったのではないかと思います。
2回目以降はブラウザが勝手にAuthrizationを送ってくれているからです。
ブラウザのデバッグツールなどでリクエストヘッダの内容を見るとそれを確認できます。
WWW-Authenticateで指定したrealmは認証領域を表すもので、同一ドメインで同じrealmの場合は
同じ認証情報であると判断するために使用していると考えられます。
(はっきりとそうだと書いた資料を見つけられなかったので推測です)
先ほどのプログラムを少し変更して、
/zone1/にアクセスが来た場合ID: zone1, PASSWORD: pass1で認証
/zone2/にアクセスが来た場合ID: zone2, PASSWORD: pass2で認証
するように変更してみます。
testproject/urls.py
# -*- coding: utf-8 -*
from django.conf.urls import url
urlpatterns = [
url(r'^zone1/$', 'basicauth.views.zone1', name='zone1', ),
url(r'^zone2/$', 'basicauth.views.zone2', name='zone2', ),
]
basicauth/views.py
# -*- coding: utf-8 -*
import base64
from django.http import HttpResponse
def zone1(request):
# zone1はrealm="hello world", id=zone1, password=pass1で認証
return _get_response(request, 'hello world', 'zone1', 'pass1')
def zone2(request):
# zone2はrealm="hello world", id=zone2, password=pass2で認証
return _get_response(request, 'hello world', 'zone2', 'pass2')
def _get_response(request, realm, realm_id, realm_password):
""" 引数で与えられたrealm, id, passwordを元に認証を行いそれに合わせたレスポンスを返す。"""
# もしリクエストヘッダにAuthrizationがなければ401を返す。
if 'HTTP_AUTHORIZATION' not in request.META:
return _http401(realm)
(authscheme, base64_idpass) = request.META['HTTP_AUTHORIZATION'].split(' ', 1)
if authscheme.lower() != 'basic':
return _http401(realm)
idpass = base64.decodestring(base64_idpass.strip().encode('ascii')).decode('ascii')
(id_, password) = idpass.split(':', 1)
if id_ == realm_id and password == realm_password:
return HttpResponse('hello world')
else:
return _http401(realm)
def _http401(realm):
# ステータスコード401のレスポンスを用意
response = HttpResponse("Unauthorized", status=401)
# WWW-Authenticateヘッダを追加し認証スキームにbasic, realmにauthtestを設定
response['WWW-Authenticate'] = 'Basic realm="%s"' % (realm, )
return response
この状態で/zone1/, /zone2/に交互にアクセスすると、アクセスするたびにID/PASSWORDの入力を求められるはずです。
なぜなら、/zone1/と/zone2/で同じrealmを設定しているため、/zone2/にアクセスしたときにブラウザが勝手に/zone1/で入力した
ID/PASSWORDを送り付けているからです。(/zone2/ -> /zone1/というアクセスでも同様の現象が起こります。)
そこでzone1とzone2のrealmに別の値を設定し、zone1とzone2は別々のID/PASSWORDを使っている
認証領域だとブラウザに理解させます。
basicauth/views.py
~~ 略 ~~
def zone1(request):
# zone1はrealm="zone1", id=zone1, password=pass1で認証
return _get_response(request, 'zone1', 'zone1', 'pass1')
def zone2(request):
# zone2はrealm="zone2", id=zone2, password=pass2で認証
return _get_response(request, 'zone2', 'zone2', 'pass2')
~~ 略 ~~
今度は/zone1/,/zone2/でそれぞれ一回だけID/PASSWORDを入力すれば、あとは交互にアクセスできるようになると思います。
realmはダイアログにメッセージを表示するためだけにあったわけではないということですね。
YI