LoginSignup
153
145

More than 5 years have passed since last update.

Django REST frameworkについての雑記

Last updated at Posted at 2015-06-10
  • Django REST frameworkでスマホアプリ用のWebAPIを作ったときのメモ書きなど。

トークンを使用した認証処理について

最初にユーザIDとパスワードで認証を行ったときにトークンを発行して、以降はそのトークンをリクエストに含める形式にしたかったので、以下のように実装した(DjangoのUserテーブルとかをそのまま使用する)。

ユーザ情報の新規登録処理

ユーザ情報をUserテーブルに登録する際、パスワードだけDjangoのライブラリを使用してハッシュをかけて登録するようにした。
https://docs.djangoproject.com/en/1.8/_modules/django/contrib/auth/hashers/#make_password

serializers.py
class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'username', 'password')
        write_only_fields = ('password')
        read_only_fields = ('id')

        def create(self, validated_data):
          """
          passwordをハッシュ化してから登録(djangoデフォルトのライブラリを使用)
          """
          password = validated_data.get('password')
          validated_data['password'] = make_password(password)
          return User.objects.create(**validated_data)

        # ......

トークンの生成

Django REST frameworkのTokenAuthentication機能をそのまま使用した。
http://www.django-rest-framework.org/api-guide/authentication/

settings.py
INSTALLED_APPS = (
    # .......
    'rest_framework.authtoken',
)

Tokenテーブルを作成する処理を追加しておく

models.py
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    """
    ユーザ新規作成時、自動的にTOKENを発行する。
    """
    if created:
        Token.objects.create(user=instance)

以上の設定をしておくと、Userテーブルにユーザが登録された際に、トークンが発行されTokenテーブルに格納されるようになる。

トークンの発行

obtain_auth_tokenを任意のURIに割り当て、クライアントがトークンを取得するためのエンドポイントを作成する。

urls.py
from rest_framework.authtoken import views as auth_views
urlpatterns = patterns('',
    url(r'^api-token-auth/', auth_views.obtain_auth_token),
)

以下のようにusername、passwordを含むjsonファイルをPOSTするとusernameに対応するトークンが返却される。

$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"username":"test","password":"111111"}' https://127.0.0.1:8000/api/api-token-auth/

トークンが必要なURIに対して、クライアントからリクエストを投げる際は、以下のようにトークンをHttp HeaderのAuthorizationにつめてあげればOK。

$ curl -X GET http://127.0.0.1:8000/api/example/ -H 'Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b'

トークンを使用した認証処理

通常の認証処理であれば、settings.pyに以下の設定を追加しておけばリクエストがきた際にトークンの内容を確認して認証処理をやってくれる。

settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.TokenAuthentication',
    )
}

ただし、この設定を行うとデフォルトですべてのURIへのリクエストに対してトークンの認証を行うようになるので、一部のURIで認証方法を変えたい場合は、以下の例のように独自のAuthenticationクラスを定義してその中に認証処理を書き、viewクラスにそのクラスを指定してやればよい。
※サンプルコードはちょっと意味不明になってしまったが、POST(新規作成)の時は固定のトークンで認証して、それ以外(参照とか更新とか)の時は登録値ごとに発行されたトークンで認証するようなイメージ。

ちなみにクライアントで指定した、Http Headerのフィールド名には自動で"HTTP_"のプリフィックスがつくので注意。Djangoの仕様っぽい。

authentications.py
class FooAuthentication(authentication.BaseAuthentication):
    def authenticate(self, request):
        # POSTの時はデフォルトの固定トークンを評価する
        if request.method == 'POST':
            default_token = request.META.get('HTTP_DEFAULT_TOKEN')
            try:
                token = System.objects.get(key='HTTP_DEFAULT_TOKEN')
            except Token.DoesNotExist:
                raise exceptions.AuthenticationFailed('error')

            if default_token != token.value:
                raise exceptions.AuthenticationFailed('error')

            return None
        # POST以外できたときは、登録値ごとに発行した認証トークンを評価する
        else:
            auth_token = request.META.get('HTTP_AUTHORIZATION')
            if not auth_token:
                raise exceptions.AuthenticationFailed('Authentication token is none')
            try:
                user = Token.objects.get(key=auth_token.replace('Token ', ''))
            except Token.DoesNotExist:
                raise exceptions.AuthenticationFailed('error')

            return (user.user, None)
view.py
class FooViewSet(viewsets.ModelViewSet):
    queryset = Foo.objects.none()
    serializer_class = FooSerializer
    authentication_classes = (FooAuthentication, )

    # .......

逆に、一部のURIでのみ認証を行いたい場合は、viewクラスの authentication_classesにTokenAuthenticationを指定してあげればいいと思う。

テストコードについて

テストコードはREST frameworkのAPIClientを使用した。
http://www.django-rest-framework.org/api-guide/testing/
こんな感じ。

tests.py
class UserTests(APITestCase):
    def setUp(self):
        """
        setUp for testing
        """
        User.objects.create(username='user1', password='user1')
        User.objects.create(username='user2', password='user2')
        self.user1 = User.objects.get(username='user1')
        self.user2 = User.objects.get(username='user2')

    def test_user_list_normal1(self):
        """
        user-list: normal pattern
        """
        url = reverse('user-list')
        expected_data = {
            "count": 1,
            "next": None,
            "previous": None,
            "results": [{
                "id": 1,
                "username": "user1"
            }]
        }
        token = Token.objects.get(user=self.user1).key
        # AuthorizationにTokenを設定
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + token)
        response = self.client.get(url, None, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        # 期待値通りjsonが返ってくることを確認
        self.assertEqual(response.data, expected_data)

153
145
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
153
145