- Django REST frameworkでスマホアプリ用のWebAPIを作ったときのメモ書きなど。
トークンを使用した認証処理について
最初にユーザIDとパスワードで認証を行ったときにトークンを発行して、以降はそのトークンをリクエストに含める形式にしたかったので、以下のように実装した(DjangoのUserテーブルとかをそのまま使用する)。
ユーザ情報の新規登録処理
ユーザ情報をUserテーブルに登録する際、パスワードだけDjangoのライブラリを使用してハッシュをかけて登録するようにした。
https://docs.djangoproject.com/en/1.8/_modules/django/contrib/auth/hashers/#make_password
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/
INSTALLED_APPS = (
# .......
'rest_framework.authtoken',
)
Tokenテーブルを作成する処理を追加しておく
@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に割り当て、クライアントがトークンを取得するためのエンドポイントを作成する。
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に以下の設定を追加しておけばリクエストがきた際にトークンの内容を確認して認証処理をやってくれる。
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
)
}
ただし、この設定を行うとデフォルトですべてのURIへのリクエストに対してトークンの認証を行うようになるので、一部のURIで認証方法を変えたい場合は、以下の例のように独自のAuthenticationクラスを定義してその中に認証処理を書き、viewクラスにそのクラスを指定してやればよい。
※サンプルコードはちょっと意味不明になってしまったが、POST(新規作成)の時は固定のトークンで認証して、それ以外(参照とか更新とか)の時は登録値ごとに発行されたトークンで認証するようなイメージ。
ちなみにクライアントで指定した、Http Headerのフィールド名には自動で"HTTP_"のプリフィックスがつくので注意。Djangoの仕様っぽい。
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)
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/
こんな感じ。
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)