Herkese merhabalar. Web uygulamalarında kullanıcı doğrulama işlemlerini muhtemelen defalarca tekrar etmişizdir. İstekte bulunan kullancının o işleme erişip erişemeyeceği, erişim izni alabilmesi için yönlendirme vs vs.
Django kendi içinde gelen auth uygulaması sayesinde bunu session bazlı olarak yönetmekte.
Fakat birden fazla client’a destek vermemiz gereken durumlarda (mobil uygulama,desktop uygulaması veya Vue,React tarzı bir JS frameworkü) oluşturacağınız REST API üzerinde ortak bir taban kullanmak gerekecektir. Bildiğiniz üzere HTTP protokolü kendi içinde bir kimlik doğrulama mekanizmasına sahiptir.
Token bazlı doğrulama da en fazla kullanılan yöntemler arasındadır.
Bu yöntemlerde kullanıcı, kullanıcı adı ve parolasını kullanarak sistemden bir token ister. Gönderilen kimlik bilgileri doğru ise ya bir token oluşturulur ya da eşleşen token döndürülür. Daha sonraki isteklerde token HTTP Authorization header’ına eklenerek doğrulama işlemi tamamlanmış olur.
Django Rest Framework yerleşik bir token bazlı kullanıcı doğrulama yeteneğine sahiptir. Okumak isterseniz token bazlı doğrulama.
Biz bu yazıda JWT üzerinden konuşacağız.
JWT nedir?
JSON Web Token (JWT), tarafların birbirleri arasındaki veri alışverişini ve bunun doğrulamasını sağlayan JSON tabanlı RFC 7519‘de tanımlanmış açık bir standarttır. Örneğin bir sunucu, kullanıcının yönetici ayrıcalıklarına sahip olduğunu belirten bir anahtar (token) oluşturabilir ve bunu kullanıcıya gönderebilir. Kullanıcı daha sonra bu anahtar ile kendisine tanımlanmış olan yönetici yetkisini bir istemcide kullanabilir ve bütün taraflar tarafından yetkisi doğrulanabilir.
Kısaca bakacak olursak birbirine . ile bağlanan 3 parçadan oluşur.
header.payload.signature
Header.
Token tipini ve oluşturulurken kullanılacak olan algoritmayı belirler. Örneğin.
{ "alg": "HS256", "typ": "JWT" }
Payload.
Token ve üzerinde tutulan bazı bilgileri içerir. Token geçerlilik süresi, kullanıcınının primary key’i gibi.
{ "token_type": "access", "exp": 234234123, "jti": "7f5997b7150d46579dc2b49167097e7b", "user_id": 1 }
Signature.
İmza. Oluşturulabilmesi için base64 kodlu header, payload, header’da belirtilen algoritma türü ve secret_key’e ihtiyaç duyar. Token göndericisi ve veri tutarlılığı için kullanılır.
Not: Django ve JWT’leri oluştururken proje SECRET_KEY’ ini kullanır. Bu sebepten dolayı canlıya çıkarken SECRET_KEY ve diğer hassas bilgi taşıyacak settings değişkenlerini korumalısınız.
Tabii ki bu çok basit bir yaklaşım. Daha ayrıntılı bilgi için şu iki adrese gidebilirsiniz.
Django ve JWT.
Django ile JWT çok kolay bir şekilde kullanılabilmekte. Kuruluma geçmeden önce senaryoyu özetleyecek olursak.
- Kullanıcı kayıt olacak. (Yerleşik kullanıcı modeli baz alındı.)
- Daha sonra kullanıcı adı ve parolası ile token isteği yapacak.
- 2 tip token dönülecek. Access ve Refresh.
- Access tokeni kullanıcının doğrulanması gereken kısımlarda kullanılacak ve kısa süreli olacak. Belirli bir süre sonra(örneğin 10 dakika) geçerliliğini yitirecek.
- Refresh ise daha uzun ömürlü (örneğin 24 saat) olacak. Kullanıcı access tokenini yenilemek istediğinde kullanılacak.
Gerekli kütüphanenin kurulması.
pip install djangorestframework-simplejwt
Spesifik bir view üzerinden gidecek olursak söyle bir kullanım işimizi görecektir.
your_app/views.py
from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import permissions,status from rest_framework_simplejwt.authentication import JWTAuthentication class Home(APIView): authentication_classes = (JWTAuthentication,) permission_classes = (permissions.IsAuthenticated,) def get(self, request, *args, **kwargs): return Response( content_type="application/json", data={"hello": "world"}, status=status.HTTP_200_OK )
View için kullanılacak olan doğrulama tipi, ve bu view’a sadece doğrulanmış kullanıcıların erişebileceğini tanımladık.
your_app/urls.py
from django.urls import path from api.views import Home urlpatterns = [ path("", Home.as_view(), name="home"), ]
View ve url bağlantısından sonra deneme isteğini yollayalım.
➜ http localhost:8000/api/ HTTP/1.1 401 Unauthorized Allow: GET, HEAD, OPTIONS Content-Length: 58 Content-Type: application/json Date: Sat, 16 May 2020 21:31:54 GMT Server: WSGIServer/0.2 CPython/3.8.2 Vary: Accept WWW-Authenticate: Bearer realm="api" X-Frame-Options: SAMEORIGIN { "detail": "Authentication credentials were not provided." }
Doğrulama başarısız. Ve gerekli olan doğrulama metodu döndürüldü. Tokene ihtiyacımız var.
Not: Shell üzerinden HTTP istekleri gönderirken curl fazlasıyla karmaşıklaşabiliyor. HTTPie bu işlemleri çok daha kolay yönetebilirsiniz.
Token isteği yapabilmek için url tanımlamalarını yapalım.
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView urlpatterns = [ ... path("token/obtain/", TokenObtainPairView.as_view(), name="obtain"), path("token/refresh/", TokenRefreshView.as_view(), name="refresh"), ]
Token isteği yapalım!
➜ http post localhost:8000/api/token/obtain/ username=egundogdu password=supersecret HTTP/1.1 200 OK Allow: POST, OPTIONS Content-Length: 438 Content-Type: application/json Date: Sat, 16 May 2020 21:42:41 GMT Server: WSGIServer/0.2 CPython/3.8.2 Vary: Accept X-Frame-Options: SAMEORIGIN { "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTg5NjY1NjYxLCJqdGkiOiIyMWUyNTU3MjU5OWE0N2UzOTQyNzRlNDE3YWY5ZGUxZCIsInVzZXJfaWQiOjJ9.iG6JJZxXU5nBD28FQNiRNSvQdcwcCOx_ld143ZktynQ", "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU4OTc1MTc2MSwianRpIjoiNGRmOGM5MjE1MTk1NGYyYzllYmM5OTk5MmVhZGZhOGEiLCJ1c2VyX2lkIjoyfQ.hpjSr0pyfAaz9fGM99Bzg8EKzNCFqcmzJLU88Cep5CY" }
Tokenleri elde ettik. Http header’ımıza tokeni ekleyip tekrar ulaşmaya çalışalım.
➜ http localhost:8000/api/ "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTg5NjY1NjYxLCJqdGkiOiIyMWUyNTU3MjU5OWE0N2UzOTQyNzRlNDE3YWY5ZGUxZCIsInVzZXJfaWQiOjJ9.iG6JJZxXU5nBD28FQNiRNSvQdcwcCOx_ld143ZktynQ" HTTP/1.1 200 OK Allow: GET, HEAD, OPTIONS Content-Length: 17 Content-Type: application/json Date: Sat, 16 May 2020 21:47:37 GMT Server: WSGIServer/0.2 CPython/3.8.2 Vary: Accept X-Frame-Options: SAMEORIGIN { "hello": "world" }
Doğrulama başarılı! Fakat tokenlerimizin süreleri olduğundan bahsetmiştik. Bu ayarlamalar için.
your_project/settings.py
from datetime import timedelta ... your settings SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(seconds=45), "REFRESH_TOKEN_LIFETIME": timedelta(days=1), }
Basitçe JWT tokenlerimizin geçerlilik sürelerini ayarlayalım. Token oluşturulduktan sonra 45 saniye kullanılabilsin ve bu süre sonunda yenilenmek zorunda olsun. Yenileme tokeni de 1 gün sonra geçerliliğini yitirsin. Herhangi bir tanımlama yapmaz iseniz bu süreler sırası ile 5 dakika ve 1 gündür. Bu ayarlamaların yanında bir çok farklı ayar daha yapabilirsiniz. Tokenin hangi algoritma ile oluşturulacağı, hangi secret_key ile kodlanacağı, payload’da bulunacak benzersiz kullanıcı bilgisi veya hangi isimle orada bulunacağı. Gerekli dökümantasyon.
O zaman yeni bir token isteği yapalım. 45 saniye bekleyip isteğimizi yapalım. Tokenimizin geçersiz olduğu cevabını bekliyoruz.
➜http localhost:8000/api/ "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTg5NjY2NTg1LCJqdGkiOiJlZTYxN2QwOWM2MmY0MTc1YTI0Mzg2NGQyNTdlYmZkMSIsInVzZXJfaWQiOjJ9.SZ9G6y7jmX0PgbgmkYWLtb-iSFdyoSJPu1sDjjdE92k" HTTP/1.1 401 Unauthorized Allow: GET, HEAD, OPTIONS Content-Length: 183 Content-Type: application/json Date: Sat, 16 May 2020 22:03:17 GMT Server: WSGIServer/0.2 CPython/3.8.2 Vary: Accept WWW-Authenticate: Bearer realm="api" X-Frame-Options: SAMEORIGIN { "code": "token_not_valid", "detail": "Given token not valid for any token type", "messages": [ { "message": "Token is invalid or expired", "token_class": "AccessToken", "token_type": "access" } ] }
Token geçersiz o halde yenileyelim.
➜ http post localhost:8000/api/token/refresh/ refresh=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU4OTY3MDE0MCwianRpIjoiOTBkYzQ3OWEwYTk0NDFkOWIyZDgzOGU0NDhlYmNkMDciLCJ1c2VyX2lkIjoyfQ.E9_xZJu-ooM9bhi_A1wlT2sRDu8evKudBJgQfWSiqd4 HTTP/1.1 200 OK Allow: POST, OPTIONS Content-Length: 218 Content-Type: application/json Date: Sat, 16 May 2020 22:07:32 GMT Server: WSGIServer/0.2 CPython/3.8.2 Vary: Accept X-Frame-Options: SAMEORIGIN { "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTg5NjY2ODk3LCJqdGkiOiJlYjQ4Njk2NjU0YTY0N2I4YmM2NTAyZGExYjIzM2M4YyIsInVzZXJfaWQiOjJ9.fZW-kwGnnk9Ft6G_E4l7t6oJD3VSqbp3a-3uYCpMUb4" }
Tekrar tokeni elde ettikten sonra doğrulamayı sağlayabilirsiniz.
Ek olarak kullanıcının halihazırda elinde olan refresh tokenin doğruluğunu kontrol etmek isterseniz:
your_app/urls.py
from rest_framework_simplejwt.views import TokenVerifyView, urlpatterns = [ ... path("token/verify/", TokenRefreshView.as_view(), name="verify"), ]
Özet.
JWT ile kullanıcı doğrulaması görüldüğü üzere çok kolay bir işlem. Projenize bu özelliği ekledikten sonra tokenleri client tarafında saklayarak HTTP istemcinizi konfigüre edebilirsiniz. (Örneğin Vue.js ile geliştirilmiş bir uygulamada local storage)
Eğer shell HTTP istemcileri ile uğraşmak istemezseniz şöyle basit bir test yazabilirsiniz. Yeni görünümlü bloğun ilk yazısı olmuş olsun böylece. Görüşmek üzere!
your_app/tests.py
from django.test import TestCase, Client from django.contrib.auth import get_user_model from django.urls import reverse class JwtAuthTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="jwt_test", password="super_secret" ) self.client = Client() @property def get_user_access_token(self): return self.client.post( reverse("obtain"), {"username": "jwt_test", "password": "super_secret"} ).data["access"] def test_create_token(self): req = self.client.post( reverse("obtain"), {"username": "jwt_test", "password": "super_secret"}, ) self.assertIn("access", req.data) self.token = req.data.get("access") def test_unauthorized_case(self): req = self.client.get(reverse("home")) self.assertEqual(req.status_code, 401) def test_authorized_case(self): req = self.client.get( reverse("home"), HTTP_AUTHORIZATION=f"Bearer {self.get_user_access_token}", ) self.assertEqual(req.status_code, 200)
[…] tipini destekler. 3.parti uygulamalar ile daha da genişletilebilir. Token authentication, ve JWT authentication ile alakalı bloglarda […]