Django 인증·권한과 배포 · 선택 확장
기존 진입점을 보호하고 개인 메모 API로 로그인·소유자 권한·CSRF·배포 조건 검증하기
선택 확장: 로그인과 소유자 권한
Django 미니 프로젝트와 REST API 문서 이후 진행합니다. 인증은 누구인지 확인하고 인가는 그 사람이 이 데이터를 읽거나 바꿀 수 있는지 결정합니다. 이 문서는 기존 관리자 명단 경로를 보호하고 자기 메모만 다루는 API로 객체 권한을 연습합니다.
학습 프로젝트는 이미 기본 사용자 모델로 migration을 진행했으므로 그대로 사용합니다. 사용자 모델 교체가 필요한 새 프로젝트는 첫 migration 전에 결정합니다. 진행 중 프로젝트에서 모델을 바꾸려고 DB와 이력을 지우지 않습니다.
기존 진입점도 함께 보호하기
DRF API만 보호해도 이전 HTML CRUD나 JSON view가 열려 있으면 우회 경로가 남습니다. lions/views.py에 다음 import를 추가하고 member_api, member_list, member_create, member_update, member_delete 각각의 기존 HTTP 메서드 데코레이터 위에 @staff_member_required를 추가합니다.
from django.contrib.admin.views.decorators import staff_member_required
# 적용 형태 예시: 기존 함수 본문은 그대로 유지합니다.# @staff_member_required# @require_GET# def member_list(request):# ...이 경로들은 관리자 명단 관리용입니다. 비로그인 브라우저는 기본적으로 Admin 로그인으로 이동할 수 있습니다. API에서 항상 JSON 401을 기대하는 계약과 혼동하지 않습니다. 기존 view 테스트에는 staff 사용자의 force_login을 준비하거나, 익명 거절과 staff 성공 테스트로 분리해야 합니다.
소유자가 있는 새 모델
기존 Member에 필수 owner 필드를 억지로 추가해 이전 데이터를 임의 사용자에게 배정하지 않습니다. 권한 학습용 새 PersonalNote 모델을 lions/models.py에 추가합니다.
from django.conf import settings
class PersonalNote(models.Model): owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) text = models.CharField(max_length=200) created_at = models.DateTimeField(auto_now_add=True)python manage.py makemigrations lionspython manage.py migratelions/serializers.py에 추가합니다.
from .models import PersonalNote
class PersonalNoteSerializer(serializers.ModelSerializer): class Meta: model = PersonalNote fields = ["id", "text", "created_at"] read_only_fields = ["id", "created_at"]owner를 입력 필드에서 제외하고 서버가 로그인 신원으로 설정합니다. 클라이언트가 owner ID를 바꾸어 다른 사람 이름으로 저장하게 만들지 않습니다.
목록과 상세에 같은 소유자 범위 적용
lions/api.py에 추가합니다.
from rest_framework.permissions import IsAuthenticatedfrom .models import PersonalNotefrom .serializers import PersonalNoteSerializer
class PersonalNoteViewSet(ModelViewSet): serializer_class = PersonalNoteSerializer authentication_classes = [SessionAuthentication] permission_classes = [IsAuthenticated] pagination_class = MemberPagination
def get_queryset(self): return PersonalNote.objects.filter(owner=self.request.user).order_by("id")
def perform_create(self, serializer): serializer.save(owner=self.request.user)lions/api_urls.py에서 PersonalNoteViewSet을 import하고 urlpatterns = router.urls보다 앞에 등록합니다.
from .api import PersonalNoteViewSetrouter.register("notes", PersonalNoteViewSet, basename="personal-note")DRF의 기본 getobject는 getqueryset 범위 안에서 상세 객체를 찾으므로 다른 사용자의 ID는 404로 처리됩니다. 이 예제에서는 목록 필터가 상세·수정·삭제에도 적용됩니다. 별도 관리자 접근, 공유 권한, 커스텀 action을 추가한다면 해당 경로에도 소유자·역할 검사를 명시합니다. 모든 action에 기본 객체 권한이 자동으로 호출된다고 가정하지 않습니다.
두 사용자로 우회 시도하기
lions/test_api.py에 추가합니다.
from .models import PersonalNote
class PersonalNotePermissionTests(APITestCase): def test_other_user_cannot_read_or_change_note(self): users = get_user_model() first = users.objects.create_user(username="first") second = users.objects.create_user(username="second") note = PersonalNote.objects.create(owner=first, text="가상 학습 메모") self.client.force_authenticate(second) url = f"/api/v2/notes/{note.pk}/" self.assertEqual(self.client.get(url).status_code, 404) self.assertEqual(self.client.patch(url, {"text": "변경"}, format="json").status_code, 404) self.assertEqual(self.client.delete(url).status_code, 404) note.refresh_from_db() self.assertEqual(note.text, "가상 학습 메모") self.assertEqual(self.client.get("/api/v2/notes/").json()["count"], 0)
def test_owner_is_taken_from_authentication(self): user = get_user_model().objects.create_user(username="owner") self.client.force_authenticate(user) response = self.client.post("/api/v2/notes/", {"text": "복습", "owner": 99999}, format="json") self.assertEqual(response.status_code, 201) self.assertEqual(PersonalNote.objects.get().owner_id, user.pk)실제 로그인에는 Admin에서 별도의 가상 사용자 계정을 만들거나 createuser에 안전한 테스트 비밀번호를 설정합니다. 위 forceauthenticate 테스트 계정은 로그인 화면 검증용이 아닙니다.
SessionAuthentication에서는 미인증 거절이 403일 수 있고 인증된 쓰기 요청도 CSRF 검증에 실패하면 403입니다. 코드 숫자 하나만 보고 권한 오류 원인을 단정하지 말고 응답과 인증 방식을 함께 확인합니다.
인증 방식과 운영 설정
같은 사이트의 브라우저 앱에는 세션·쿠키 방식이 단순할 수 있습니다. 다른 클라이언트에 토큰이 필요하다면 저장·만료·회전·폐기 정책까지 정합니다. JWT 서명 확인만으로 소유권과 로그아웃 정책이 해결되지는 않습니다. 비밀번호는 Django의 createuser·setpassword와 검증 API를 사용합니다.
CSRF는 브라우저가 자동으로 보내는 인증 정보와 관련된 상태 변경 요청을 보호합니다. CORS는 다른 origin의 브라우저 접근 정책입니다. CORS 전체 허용으로 권한 문제를 해결하려 하지 않습니다.
배포 전에 다음을 실제 환경에서 확인합니다.
| 항목 | 확인할 증거 |
|---|---|
| DEBUG·SECRETKEY·ALLOWEDHOSTS | 운영 설정과 비밀 관리, 잘못된 호스트 거절 |
| HTTPS·쿠키 | Secure·HttpOnly·SameSite 정책, 프록시 신뢰 범위 |
| 인증·권한 | 익명·일반·staff·타인 ID 테스트 |
| CSRF·CORS | 보호된 쓰기 실패·성공, 허용 origin |
| DB·migration | 백업·적용 순서·복구 계획 |
| 정적 파일·서버 | collectstatic과 운영 WSGI/ASGI 서버 |
| 로그 | 토큰·쿠키·비밀번호·개인정보 노출 없음 |
python manage.py check --deploy의 결과를 읽고 해결 근거를 남깁니다. 명령 통과만으로 전체 보안 검증이 끝난 것은 아닙니다. 로그인·로그아웃·만료·비밀번호 변경 후 동작도 확인합니다.
세션 실습과 완료 기준
권장 120분: 진입점 목록 20분, 소유자 API 35분, 두 사용자 테스트 35분, 운영 검토 30분입니다. 완료 기준은 타인 ID를 알아도 조회·변경할 수 없고 생성 소유자가 서버에서 정해지며 기존 우회 경로도 보호되는 것입니다.
제출물은 권한 행렬·테스트 결과·운영 설정 점검·남은 배포 작업입니다. 실습용 계정을 실제 회원 계정처럼 공개하지 않습니다.