본문 바로가기
Python

[파이썬으로 웹개발] 장고 닌자의 인증 기능을 알아보자

by 돈민찌 2022. 4. 13.
반응형

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 인증방식을 구현할 수 있습니다. 

자물쇠 버튼을 클릭하면 이렇게 AuthBearer의 값을 입력하라는 창이 나타납니다.

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}

 

반응형

댓글