読者です 読者をやめる 読者になる 読者になる

django.contrib.authのImproperlyConfigured

Django

Djangoの標準の認証機構(django.contrib.auth)をざっくりとまとめて、今困っていることを書きます。


まず、自分で書いたビュー関数の中で認証処理を行うとすると、django.contrib.auth.authenticate関数を使って条件に合致するユーザが登録されているかどうかを調べます。登録ユーザであった、すなわちユーザオブジェクトが返った場合は、django.contrib.auth.login関数を使って、認証済みであることをマークします。

from django.contrib.auth import authenticate, login

def my_login(request):
    # paramsは、dict(username='scott', password='tiger')
    # のような認証に使うパラメータ
    user = authenticate(**params)
    if user:
         # 正規のユーザ
         login(request, user)

    # 何らかの処理が続く


settings.AUTHENTICATION_BACKENDSにauthenticateとget_userいうメソッドを実装した任意のクラスを登録しておくと、django.contrib.auth.authenticatedを呼び出したときに、順番に認証処理を行ってくれます。DjangoでLDAP認証とか、WSSE認証とか、ベーシック認証とかのバックエンドは探せばたくさん出てくると思いますが、どれもこのAUTHENTICATION_BACKENDSを使って任意のアプリケーションに組み込めるようにデザインされているはずです。
僕の場合は、SQLAlchemy経由でデータベースにアクセスして携帯電話の端末番号で認証、という事情があるので、次のようなクラスで認証処理を行っています。(実際のコードではありません。)

from myproject.auth.models import Session, User

class MobileHardwareIDBackend(object):
     def authenticated(self, carrier, device_id):
          return Session.query(User).filter_by(carrier=carrier, device_id=devive_id).first()

     def get_user(self, user_id)
          return Session.query(User).get(user_id)

このクラスがmyproject.auth.backends.MobileHardwareIDBackendだとすると、settings.pyには次のように書きます。

AUTHENTICATION_BACKENDS = (
    'myproject.auth.backends.MobileHardwareIDBackend',
)


認証を通った場合、次回以降のアクセスのためにその情報をセッションに保存しておかないといけないわけですが、それを行っているのが、django.contrib.auth.loginです。
具体的に何をやっているかというと、セッションに「認証に使ったバックエンドのモジュール名・クラス名」と「認証されたユーザのID」だけを保存しています。実際のコードを見ると分かると思います。最近のバージョンでもあまり変わっていないので、手元で使っているバージョンのコードを貼り付けます。

def login(request, user):
    """
    Persist a user id and a backend in the request. This way a user doesn't
    have to reauthenticate on every request.
    """
    if user is None:
        user = request.user
    # TODO: It would be nice to support different login methods, like signed cookies.
    user.last_login = datetime.datetime.now()
    user.save()
    request.session[SESSION_KEY] = user.id
    request.session[BACKEND_SESSION_KEY] = user.backend
    if hasattr(request, 'user'):
        request.user = user


request.session[SESSION_KEY]に「ユーザーID」、request.session[BACKEND_SESSION_KEY]に「バックエンドのモジュール名・クラス名」が保存されます。
次回以降アクセスがあった場合は、request.session[BACKEND_SESSION_KEY]からバックエンドのクラスを判別し、そのクラスのインスタンスのget_userメソッドを呼び出して、ユーザオブジェクトを復元しているわけです。


上記の例だと、ログイン済みユーザのセッションには、実際には次のようなデータが保存されているので、

{'_auth_user_id': 1,
 '_auth_user_backend': 'myproject.auth.backends.MobileHardwareIDBackend' }

必要な場合は、Djangoはmyproject.auth.backends.MobileHardwareIDBackendをインポートしようとします。しかし、リファクタリングによってモジュール名が変わってしまうと、ImproperlyConfiguredという例外が発生してアプリケーションの実行が中断してしまいます。これが考え物です。


「バックエンドがインポートできないのはプログラミング時の問題」という考え方からこのような設計になており、実際、認証処理実行前にインポートできないモジュールを検出してImproperlyConfiguredを投げてくれるのは良い設計だと思うのですが、認証処理完了後(すでに認証済みのユーザのセッション)でこれをやられると、あとから認証処理のリファクタリングができなくなってしまい非常に困ります。


どういう経緯でこういった設計になったかにも興味がありますが、個人的な意見としては、例えば次のように、使用できなくなったバックエンドの情報を破棄し、未ログインユーザオブジェクトを返すべきだと思います。

def get_user(request):
    from django.contrib.auth.models import AnonymousUser
    try:
        user_id = request.session[SESSION_KEY]
        backend_path = request.session[BACKEND_SESSION_KEY]
        try:
            backend = load_backend(backend_path)
        except ImproperlyConfigured:
            del request.session[SESSION_KEY]
            del request.session[BACKEND_SESSION_KEY]
            return AnonymousUser()
        user = backend.get_user(user_id) or AnonymousUser()
    except KeyError:
        user = AnonymousUser()
    return user