Django Ninja
Django Ninja는 Flask만큼 간단하게 Django API Controller를 구성할 수 있는 보조 라이브러리입니다.
보안에 관련된 모든 사양을 학습하지 않아도 NinjaAPI를 정의할 때 혹은 api를 정의할 때 간단하게 Auth를 정의할 수 있는 방법이 있어 소개합니다.
from ninja import NinjaAPI
from ninja.security import django_auth # Django의 Auth 기능 그대로 사용
api = NinjaAPI(csrf=True)
@api.get("/whoami", auth=django_auth) # auth=django_auth 만으로 해결
def whoami(request):
return f"Authenticated user {request.auth}"
위 코드에서 클라이언트는 Django의 기본 사양인 세션 인증을 사용해 로그인한 경우에만 whoami 메소드를 호출할 수 있습니다. 그렇지 않으면 자연스럽게 401 (인증되지 않은 요청) 상태 코드를 반환합니다.
Bearer
HTTP Bearer 헤더 방식의 보안을 구현할 수도 있습니다.
from ninja import NinjaAPI
from ninja.security import HttpBearer
from django_project.settings import SECRET_KEY
api = NinjaAPI(csrf=True)
class AuthBearer(HttpBearer):
def authenticate(self, request, token):
if token == SECRET_KEY:
return token
@api.get("/bearer", auth=AuthBearer())
def bearer(request):
return {"token": request.auth}
위 코드에서는 Django를 초기 설정할 때 settings에 정의되는 값인 SECRET_KEY 값을 인증 토큰으로 사용했습니다. 이 과정을 클래스와 클래스 메소드로 정의하고, auth 파라미터로 클래스 인스턴스를 넘겨줍니다. 다음 이미지처럼 Django Ninja의 OpenAPI 페이지에서도 확인할 수 있듯이, 간단하게 Bearer 인증방식을 구현할 수 있습니다.
Global Authentication
API마다 모두 auth 파라미터를 통과해야 하는 상황이라면, 상위 NinjaAPI 객체를 정의할 때 이를 미리 명시함으로써 코드 중복을 줄일 수 있습니다.
from ninja import NinjaAPI, Form
from ninja.security import HttpBearer
class GlobalAuth(HttpBearer):
def authenticate(self, request, token):
if token == "supersecret":
return token
api = NinjaAPI(auth=GlobalAuth())
# @api.get(...)
# def get_action(request):
# @api.post(...)
# def post_action(request):
그리고 이렇게 전역적으로 Auth가 적용되어 있을 때 일부의 api만 이것을 무효화하고 싶다면 해당 api에 auth 값을 None으로 입력하면 됩니다.
from ninja import NinjaAPI, Form
from ninja.security import HttpBearer
class GlobalAuth(HttpBearer):
def authenticate(self, request, token):
if token == "supersecret":
return token
api = NinjaAPI(auth=GlobalAuth())
@api.get("/hi")
def get_action(request):
pass
@api.post("/hello", auth=None) # 이렇게 오버라이딩해주면 됩니다.
def post_action(request):
pass
토큰을 입력하는 보안 외에 어떤 것을 또 적용할 수 있을까요? 사실 auth는 모든 Callable한 객체를 인수로 허용합니다. 그리고 Boolean으로 변환할 수 있는 값을 True/False로 변환하여 인증을 통과시킵니다. 그리고 통과된 값은 request.auth에 할당됩니다. request의 메타데이터나 POST의 QueryDict 내부 인자 등을 가지고 보안 인증에 사용할 수 있습니다.
def ip_whitelist(request):
if request.META["REMOTE_ADDR"] == "111.215.67.41":
return "111.215.67.41"
@api.get("/ipwhiltelist", auth=ip_whitelist)
def ipwhiltelist(request):
return f"Authenticated client, IP = {request.auth}"
# Authenticated client, IP = 111.215.67.41 출력됨.
API KEY 구현하기
공공 API나 오픈 API를 사용하면, 서버에서 발급해주는 API Key를 가지고 있는 사용자에게만 응답을 반환합니다. 이것 역시 Ninja를 통해 구현할 수 있습니다. 이러한 키는 URI의 쿼리 문자열로도 보낼 수 있고,
GET /something?api_key=XOXO1234
요청의 헤더로 보낼 수도 있고,
GET /something HTTP/1.1
X-API-KEY: XOXO1234
요청의 쿠키를 읽어낼 수도 있습니다. Django Ninja에서는 각 케이스에 맞는 내장 클래스가 있습니다.
from ninja.security import APIKeyQuery
from someapp.models import Client
class ApiKey(APIKeyQuery):
param_name = "api_key"
def authenticate(self, request, key):
try:
return Client.objects.get(key=key)
except Client.DoesNotExist:
pass
@api.get("/apikey", auth=ApiKey())
def apikey(request):
assert isinstance(request.auth, Client)
return f"Hello {request.auth}"
위 예시에서 APIKeyQuery를 상속받은 ApiKey를 선언하면 param_name에 정의한 문자열의 쿼리에 해당하는 문자열이 authenticate함수의 key로 넘어옵니다. 역시 마찬가지로 결과 값은 request.auth에 할당되어 출력됩니다.
from ninja.security import APIKeyHeader
class ApiKey(APIKeyHeader):
param_name = "X-API-Key"
def authenticate(self, request, key):
if key == "supersecret":
return key
@api.get("/headerkey", auth=ApiKey())
def apikey(request):
return f"Token = {request.auth}"
APIKeyHeader 클래스를 APIKeyQuery 대신 사용했고 그 외에는 모두 같은 방식입니다. 헤더에 param_name의 문자열에 해당하는 값이 key로 넘어와 인증에 사용됩니다.
from ninja.security import APIKeyCookie
class CookieKey(APIKeyCookie):
def authenticate(self, request, key):
if key == "supersecret":
return key
@api.get("/cookiekey", auth=CookieKey())
def apikey(request):
return f"Token = {request.auth}"
쿠키 역시 해당하는 클래스를 가져오면 됩니다.
HTTP Bearer
from ninja.security import HttpBearer
class AuthBearer(HttpBearer):
def authenticate(self, request, token):
if token == "supersecret":
return token
@api.get("/bearer", auth=AuthBearer())
def bearer(request):
return {"token": request.auth}
HTTP Basic Authentication
from ninja.security import HttpBasicAuth
class BasicAuth(HttpBasicAuth):
def authenticate(self, request, username, password):
if username == "admin" and password == "secret":
return username
@api.get("/basic", auth=BasicAuth())
def basic(request):
return {"httpuser": request.auth}
Multi Auth
auth 인수의 값으로 리스트를 입력하면 여러 인증 방식이 적용됩니다.
from ninja.security import APIKeyQuery, APIKeyHeader
class AuthCheck:
def authenticate(self, request, key):
if key == "supersecret":
return key
class QueryKey(AuthCheck, APIKeyQuery):
pass
class HeaderKey(AuthCheck, APIKeyHeader):
pass
@api.get("/multiple", auth=[QueryKey(), HeaderKey()])
def multiple(request):
return f"Token = {request.auth}"
위 코드와 같은 상황에서 Django Ninja는 먼저 GET 요청의 API_KEY 쿼리를 먼저 확인하고 이것이 유효하지 않은 경우 Header에서 API_KEY를 확인합니다. 둘 다 유효하지 않으면 요청은 오류를 반환합니다.
Django Router
Django Ninja에서는 각 객체에 따라 api를 분리해 간결한 코드 작성이 가능합니다. 라우터 역시 auth 적용이 가능합니다.
# events/api.py
from ninja import Router
from events.models import Event
router = Router()
@router.get('/')
def list_events(request):
return [
{"id": e.id, "title": e.title}
for e in Event.objects.all()
]
@router.get('/{event_id}')
def event_details(request, event_id: int):
event = Event.objects.get(id=event_id)
return {"title": event.title, "details": event.details}
# urls.py
from ninja import NinjaAPI
from events.api import router as events_router
from news.api import router as news_router
from blogs.api import router as blogs_router
api = NinjaAPI()
api.add_router("/events/", events_router)
api.add_router("/news/", news_router)
api.add_router("/blogs/", blogs_router)
여기서 auth를 적용할 수 있습니다.
from ninja import NinjaAPI
from ninja.security import HttpBasicAuth
from events.api import router as events_router
api = NinjaAPI()
class BasicAuth(HttpBasicAuth):
def authenticate(self, request, username, password):
if username == "admin" and password == "secret":
return username
api.add_router("/events/", events_router, auth=BasicAuth())
# add_router 뿐만 아니라 Router() 클래스 생성자를 사용할 때에 auth를 적용할 수도 있습니다.
# router = Router(auth=BasicAuth())
Django Ninja에는 exception_handler를 사용할 수 있습니다. auth에 할당한 클래스는 결과값으로 에러를 반환할 경우 exception_handler에게 넘어갑니다.
from ninja import NinjaAPI
from ninja.security import HttpBearer
api = NinjaAPI()
class InvalidToken(Exception):
pass
@api.exception_handler(InvalidToken) # 에러 발생 시 작동
def on_invalid_token(request, exc):
return api.create_response(request, {"detail": "Invalid token supplied"}, status=401)
class AuthBearer(HttpBearer):
def authenticate(self, request, token):
if token == "supersecret":
return token
raise InvalidToken # 토큰 불일치 시 에러 발생
@api.get("/bearer", auth=AuthBearer()) # 에러가 발생할 수 있는 클래스 auth에 할당
def bearer(request):
return {"token": request.auth}
'Python' 카테고리의 다른 글
[프로젝트후기] 모바일 어플마켓 크롤러 개발 후기 근데 슬랙봇을 곁들인 (0) | 2022.03.09 |
---|---|
[파이썬웹개발] 장고에 딱 붙고 착 붙는 데이터베이스는 무엇일까? (0) | 2022.03.01 |
[장고웹개발] 웹스크래핑, 장고와 함께 하기 (0) | 2022.02.17 |
[장고웹개발] 장고와 친해져 봅시다. (부제: 요즘도 장고 쓰냐) (1) | 2022.02.15 |
내 티스토리는 무슨 키워드로 많이 유입될까??? 워드클라우드 다시 맛보기 (0) | 2022.02.11 |
댓글