Django REST frameworkについての雑記

+ Django REST frameworkでスマホアプリ用のWebAPIを作ったときのメモ書きなど。

http://www.django-rest-framework.org/

## 

## トークンを使用した認証処理について
最初にユーザ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
$ 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
$ 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)

```