logo

JEONGGON

    블로그
github
mode
목 차
down-arrow

Django 사용자 인증

2023.03.28.

post-thumbnail

Authentication

1. Django Authentication System

  • 사용자 인증과 관련된 기능을 모아 놓은 시스템
  • 인증과 권한 부여를 함께 제공 및 처리함

django authentication system
setting의 django authentication system


1-1. Authentication

  • 인증으로 사용자가 자신이 누구인지 확인하는 것
  • 신원 확인

1-2. Authorization

  • 인증된 사용자가 수행할 수 있는 작업을 결정
  • 권한을 부여

1-3. 사전 설정

  • app accounts 생성 및 등록
  • auth와 관련한 경로 및 키워드들은 django의 내부적으로 accounts라는 이름으로 사용하고 있기에 accounts로 지정하는 것을 권장함

# project/urls.py

urlpatterns = [
    ...,
    path('accounts/', include('accounts.urls')),
]
# accounts/urls.py

from django.urls import path
from . import views

app_name = 'accounts'
urlpatterns = []


2. Custom User model

2-1. Custom User model로 대체

  • django가 기본적으로 제공하는 User model은 내장된 auth 모듈의 User 클래스를 사용
  • 별도 설정없이 사용할 수 있어서 간편하지만, 직접 수정할 수 없음
  • Django - 유저모델 대체하기

2-2. 상속

  • AbstractUser상속받는 커스텀 User 클래스 작성
  • 기존 User 클래스도 AbstractUser를 상속받기 때문에 커스텀 User 클래스도 같은 모습을 가지게 됨

# accounts/models.py

from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    pass

2-3. User 모델 지정

  • django 프로젝트가 사용하는 기본 User 모델을 커스텀한 User 모델로 지정

# settings.py

AUTH_USER_MODEL = 'accounts.User'

2-4. admin site에 등록하기

  • 등록해야만 admin site에 출력됨

# accounts/admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User

admin.site.register(User, UserAdmin)

- 주의

  • 프로젝트 중간에 AUTH_USER_MODEL을 변경할 수 없음
  • 만약, 진행이 이미 되었다면 데이터 베이스 초기화 한 후 진행해야 함

- 반드시 User 모델을 대체해야하는가?

  • Django에서 공식적으로 커스텀 User 모델을 설정하는 것을 강력하게 권장하고 있음(highly recommended)
  • 기본 User 모델과 동일하게 작동하는 동시에 필요 시, 맞춤 설정 가능하기 때문
  • 단, 앞선 주의와 같이 User 모델 대체 작업은 프로젝트의 모든 migrations 혹은 첫 migrate 실행 전 수행되어야 함


3. Login

  • Session을 생성하는 과정

3-1. AuthenticationForm()

  • 로그인을 위한 built-in form

- login의 url 설정

# accounts/urls.py

app_name = 'accounts'
urlpatterns = [
    path('login/', views.login, name='login'),
]

- login의 views 함수의 GET method 설정

# accounts/views.py

from django.contrib.auth.forms import AuthenticationForm


def login(request):
    if request.method == 'POST':
        pass
    else:
        form = AuthenticationForm()
    context = {
        'form': form,
    }
    return render(request, 'accounts/login.html', context)
  • GET method로 login 요청을 받으면 form으로 인증 폼인 AuthenticationForm을 받아 로그인 페이지로 context에 담아 전달

- login의 html 페이지

<!-- accounts/login.html -->

<h1>로그인</h1>
<form action="{% url 'accounts:login' %}" method="POST">
  {% csrf_token %} {{ form.as_p }}
  <input type="submit" />
</form>

로그인 페이지
로그인 페이지


- login의 views 함수의 POST method 설정

# accounts/views.py

from django.shortcuts import render, redirect
from django.contrib.auth import login as auth_login
from django.contrib.auth.forms import AuthenticationForm


def login(request):
    if request.method == 'POST':
        form = AuthenticationForm(request, request.POST)
        # form = AuthenticationForm(request, data=request.POST)
        if form.is_valid():
            auth_login(request, form.get_user())
            return redirect('acticles:index')
    else:
        form = AuthenticationForm()
    context = {
        'form': form,
    }
    return render(request, 'accounts/login.html', context)

3-2. login() 함수

login(request, user)
  • 인증된 사용자를 로그인 하는 함수

3-3. get_user() 메서드

  • AuthenticationForm의 인스턴스 메서드
  • 유효성 검사를 통과했을 때, 로그인을 한 사용자 객체를 반환함

- login시, Session 데이터 확인

DB session

  • DB 테이블에서 django로부터 발급받은 Session 확인

browser session

  • 브라우저 개발자 도구에서 확인
  • 개발자 도구 -> Application -> Cookies


4. Logout

  • Session을 삭제하는 과정

4-1. logout(request)

  • 현재 요청에 대한 Session 데이터를 DB에서 삭제
  • 클라이언트 쿠키에서도 session id를 삭제

- logout의 url 설정

# accounts/urls.py

urlpatterns = [
    path('logout/', views.logout, name='logout'),
]

- logout의 views 함수의 설정

# accounts/views.py

from django.contrib.auth import logout as auth_logout


def logout(request):
    auth_logout(request)
    return redirect('articles:index')

- logout의 html 페이지

<!-- articles/index.html -->

<h1>Articles</h1>
<form action="{% url 'accounts:logout' %}" method="POST">
  {% csrf_token %}
  <input type="submit" value="Logout" />
</form>


5. Template with Authentication data

  • 템플릿에서 인증 관련 데이터를 출력하는 방법

5-1. 현재 로그인 되어있는 유저정보 출력

<!-- articles/index.html -->

<h3>Hello, {{ user }}</h3>

- context processors

  • 템플릿에 렌더링 될 때, 호출 가능한 context 데이터 목록
  • 작성된 context 데이터는 기본적으로 템플릿에서 사용 가능한 변수로 포함됨
  • Django에서 자주 사용되는 데이터 목록을 미리 템플릿에 로드해둔 것
# settings.py

TEMPLATES = [
    {
        ...
    'OPTIONS': {
    'context_processors': [
        'django.template.context_processors.debug',
        'django.template.context_processors.request',
        'django.contrib.auth.context_processors.auth',
        'django.contrib.messages.context_processors.messages',
    ],
},
},
]


6. 회원가입

  • User 객체를 생성하는 것

6-1. UserCreationForm()

  • 회원가입을 위한 built-in ModelForm

6-2. 회원가입 페이지 만들기

- 회원가입 url 설정

# accounts/urls.py

app_name = 'accounts'
urlpatterns = [
    ...,
    path('signup/', views.signup, name='signup'),
]

- 회원가입 GET 요청 시, view 함수 설정

# accounts/views.py

from django.contrib.auth.forms import UserCreationForm


def signup(request):
    if request.method == 'POST':
        pass
    else:
        form = UserCreationForm()
    context = {
        'form': form,
    }
    return render(request, 'accounts/signup.html', context)

- 회원가입 html 페이지

<!-- accounts/signup.html -->

<h1>회원가입</h1>
<form action="{% url 'accounts:signup' %}" method="POST">
  {% csrf_token %} {{ form.as_p }}
  <input type="submit" />
</form>

회원가입 페이지
회원가입 페이지


- 회원가입 POST 요청 시, view 함수 설정

# accounts/views.py

from django.contrib.auth.forms import UserCreationForm


def signup(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('articles:index')
    else:
        form = UserCreationForm()
    context = {
        'form': form,
    }
    return render(request, 'accounts/signup.html', context)

- 회원가입 후 에러

회원가입 에러
회원가입 에러


  • 회원가입에서 사용하는 UserCreationForm()이 커스텀 User 모델이 아닌 기본 유저 모델로 인해 작성된 클래스이기 때문

- 커스텀 User 모델 사용을 위해 Form 다시 작성

  • UserCreationForm(), UserChangeForm() : 두 개의 form 모두 기본 유저 모델을 사용
  • 회원가입 및 회원정보 수정을 위해 커스텀 User 모델과 같이 커스텀 Form을 작성해 주어야 함
# accounts/forms.py

from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm, UserChangeForm


class CustomUserCreationForm(UserCreationForm):
    class Meta(UserCreationForm.Meta):
        model = get_user_model()


class CustomUserChangeForm(UserChangeForm):
    class Meta(UserChangeForm.Meta):
        model = get_user_model()

- get_user_model()

  • 현재 프로젝트에서 활성화된 사용자 모델(Active User Model)을 반환하는 함수

- User 모델을 직접 참조하지 않는 이유

  • get_user_model()을 사용하면 커스텀 User 모델을 자동으로 반환(간편함)
  • Django에서 공식적으로 User 클래스 직접 참조보다 get_user_model()을 통해 참조해야 함을 강조

- 커스텀 Form을 사용하여 회원가입 view 함수 수정

# accounts/views.py

from .forms import CustomUserCreationForm


def signup(request):
    if request.method == 'POST':
        form = CustomUserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('articles:index')
    else:
        form = CustomUserCreationForm()
    context = {
        'form': form,
    }
    return render(request, 'accounts/signup.html', context)


7. 회원탈퇴

  • User 객체를 삭제하는 것

7-1. 회원탈퇴 로직 작성

- 회원탈퇴 url 설정

# accounts/urls.py

app_name = 'accounts'
urlpatterns = [
    ...,
    path('delete/', views.delete, name='delete'),
]

- 회원탈퇴 view 함수 작성

# accounts/views.py

def delete(request):
    request.user.delete()
    return redirect('articles:index')

- 회원탈퇴 html 작성

<!-- accounts/index.html -->

<form action="{% url 'accounts:delete' %}" method="POST">
  {% csrf_token %}
  <input type="submit" value="회원탈퇴" />
</form>


8. 회원정보 수정

  • User 객체를 수정하는 것

8-1. UserChangeForm()

  • 회원정보 수정을 위한 built-in ModelForm

8-2. 회원정보 수정 페이지 작성

- 회원정보 수정 url 설정

# accounts/url.py

app_name = 'accounts'
urlpatterns = [
    ...,
    path('update/', views.update, name='update'),
]

- 회원정보 수정 GET 요청 시, view 함수 설정

# accounts/views.py

from .forms import CustomUserChangeForm


def update(request):
    if request.method == 'POST':
        pass
    else:
        form = CustomUserChangeForm(instance=request.user)
    context = {
        'form': form,
    }
    return render(request, 'accounts/update.html', context)

- 회원정보 수정 html 페이지

<!-- accounts/update.html -->

<h1>회원정보 수정</h1>
<form action="{% url 'accounts:update' %}" method="POST">
  {% csrf_token %} {{ form.as_p }}
  <input type="submit" />
</form>

회원정보 수정 페이지
회원정보 수정 페이지


- UserChangeForm 사용 시, 문제

  • 일반 유저가 접근해서는 안 될 모든 정보들(fields)까지 수정이 가능해짐
  • 따라서 CustomUserChangeForm을 통해 접근 가능한 필드들만 받을 수 있게 조정해야 함

- CustomUserChangeForm의 fields 재정의

# accounts/forms.py

class CustomUserChangeForm(UserChangeForm):
    class Meta(UserChangeForm.Meta):
        model = get_user_model()
        fields = ('email', 'first_name', 'last_name')
  • User Model의 필드만 수정할 수 있도록 명시

- 회원정보 수정 POST 요청 시, view 함수 설정

# accounts/views.py

from .forms import CustomUserChangeForm


def update(request):
    if request.method == 'POST':
        form = CustomUserChangeForm(request.POST, instance=request.user)
        # form = CustomUserChangeForm(data=request.POST, instance=request.user)
        if form.is_valid():
            form.save()
            return redirect('articles:index')
    else:
        form = CustomUserChangeForm(instance=request.user)
    context = {
        'form': form,
    }
    return render(request, 'accounts/update.html', context)


9. 비밀번호 변경

  • Django에서는 비밀번호 변경 페이지를 회원정보 수정 form에서 별도의 주소로 제공하고 있음

기본 제공되는 비밀번호 변경 페이지
기본 제공되는 비밀번호 변경 페이지


9-1. PasswordChangeForm()

  • 비밀번호 변경을 위한 built-in Form

9-2. 비밀번호 변경 페이지 작성

- 비밀번호 변경 url 설정

# accounts/urls.py

app_name = 'accounts'
urlpatterns = [
    ...,
    path('password/', views.change_password, name='change_password'),
]

- 비밀번호 변경 GET 요청 시, view 함수 설정

# accounts/views.py

from django.contrib.auth.forms import PasswordChangeForm


def change_password(request):
    if request.method == 'POST':
        pass
    else:
        form = PasswordChangeForm(request.user)
    context = {
        'form': form,
    }
    return render(request, 'accounts/change_password.html', context)

- 비밀번호 변경 html 페이지

<!--accounts/change_password.html-->

<h1>비밀번호 변경</h1>
<form action="{% url 'accounts:change_password %}" method="POST">
  {% csrf_token %}
  {{ form.as_p }}
  <input type="submit">
</form>

- 비밀번호 변경 POST 요청 시, view 함수 설정

# accounts/views.py

def change_password(request):
    if request.method == 'POST':
        form = PasswordChangeForm(request.user, request.POST)
        # form = PasswordChangeForm(user=request.user, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('articles:index')
    else:
        form = PasswordChangeForm(request.user)
    context = {
        'form': form,
    }
    return render(request, 'accounts/change_password.html', context)

9-3. 비밀번호 변경 시 Session 무효화

  • 비밀번호가 변경될 경우, 기존의 Session회원 인증 정보가 일치하지 않아 로그인 상태가 유지가 안됨

- update_session_auth_hash(request, user)

  • 비밀번호 변경 시, Session이 무효화 되는 것을 방지
  • 비밀번호 변경되어도 로그아웃 되지 않도록 새로운 비밀번호의 Session 데이터로 기존의 Session을 업데이트

- update_session_auth_hash를 view 함수에 적용

# accounts/views.py

from django.contrib.auth import update_session_auth_hash


def change_password(request):
    if request.method == 'POST':
        form = PasswordChangeForm(request.user, request.POST)
        # form = PasswordChangeForm(user=request.user, data=request.POST)
        if form.is_valid():
            form.save()
            update_session_auth_hash(request, user)
            return redirect('articles:index')
    else:
        form = PasswordChangeForm(request.user)
    context = {
        'form': form,
    }
    return render(request, 'accounts/change_password.html', context)
  • index 페이지로 리다이렉트 되기 전, Session 업데이트


10. 로그인 사용자에 대한 접근 제한

10-1. 로그인 사용자에 대한 접근을 제한하는 2가지 방법

  • is_authenticated : 속성
  • @login_required : 데코레이터

10-2. is_authenticated

  • 사용자가 인증되었는지 여부를 알 수 있는 User model의 속성(attribute)
  • 모든 User 인스턴스에 대해 항상 True인 읽기 전용 속성이며, AnonymousUser에 대해서는 항상 False
  • 권한과 관련없고, Session도 확인하지 않고 로그인 유무만 판단

- 로그인과 비로그인 상태에서 출력되는 링크 다르게 설정

<!--articles/index.html-->

{% if request.user.is_authenticated %}
<h3>hello {{ user }}</h3>
<form action="{% url 'accounts:logout' %}" method="POST">
  {% csrf_token %}
  <input type="submit" value="logout">
</form>
<form action="{% url 'accounts:delete' %}" method="POST">
  {% csrf_token %}
  <input type="submit" value="회원탈퇴">
</form>
<a href="{% url 'accounts:update' %}">회원정보 수정</a>
{% else %}
<a href="{% url 'accounts:login' %}">로그인</a>
<a href="{% url 'accounts:signup' %}">회원가입</a>
{% endif %}
  • DTL(Django Template Language)의 태그 if 구문으로 로그인 유무를 분기 처리함
  • 로그인 된 사용자의 경우, 로그아웃, 회원탈퇴, 회원정보 수정 링크를 보여줌
  • 로그인 안 된 사용자의 경우, 로그인, 회원가입의 링크를 보여줌

- 로그인 된 사용자의 경우, 로그인/회원가입 로직을 수행할 수 없도록 처리

# accounts/views.py

def login(request):
    if request.user.is_authenticated:
        return redirect('articles:index')
    ...


def signup(request):
    if request.user.is_authenticated:
        return redirect('articles:index')
    ...

10-3. login_required

  • 인증된 사용자에 대해서만 view 함수를 실행시키는 데코레이터
  • 로그인 하지 않은 사용자의 경우, /accounts/login/ 주소로 리다이렉트 시킴

- 인증된 사용자만 게시글 작성/수정/삭제 할 수 있도록 설정

# acticles/views.py

from django.contrib.auth.decorators import login_required


@login_required
def create(request):
    pass


@login_required
def delete(request):
    pass


@login_required
def update(request):
    pass
  • 해당 view 함수 위에 데코레이터 @login_required 붙이기

- 인증된 사용자만 로그아웃/회원탈퇴/회원정보 수정/비밀번호 변경 할 수 있도록 설정

# accounts/views.py

from django.contrib.auth.decorators import login_required


@login_required
def logout(request):
    pass


@login_required
def delete(request):
    pass


@login_required
def update(request):
    pass


@login_required
def change_password(request):
    pass
  • 해당 view 함수 위에 데코레이터 @login_required 붙이기


11. 참고

11-1. User 모델 상속 관계

django User model

  • Django User 모델 상속은 다음과 같은 순서로 이루어짐

- AbstractUser class

  • 관리자 권한과 함께 완전한 기능을 가지고 있는 User model을 구현하는 추상 기본 클래스

- 추상 기본 클래스

  • 몇 가지 공통 정보를 여러 다른 모델에 넣을 때 사용하는 클래스
  • 데이터 베이스 테이블을 만드는데 사용되지 않으며, 대신 다른 모델의 기본 클래스로 사용되는 경우, 해당 필드가 하위 클래스의 필드에 추가됨

11-2. 데코레이터(Decorator)

  • 기존에 작성된 함수에 기능을 추가하고 싶을 경우, 해당 함수를 수정하지 않고 기능만 추가해주는 함수

11-3. 회원가입 후, 로그인까지 된 상태로 진행하기

# accounts/views.py

def signup(request):
    if request.method == 'POST':
        form = CustomUserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            auth_login(request, user)
            return redirect('articles:index')
    else:
        ...
  • form을 저장한 후, auth_login()을 통해 로그인 수행하고 index 페이지로 리다이렉트 시킴

11-4. 회원탈퇴 시, 유저의 Session 정보도 함께 지우기

# accounts/views.py

def delete(request):
    request.user.delete()
    auth_logout(request)
  • 탈퇴(delete()) -> 로그아웃(auth_logout())의 순서를 지켜야 함
  • 로그아웃을 먼저 하면 요청하는 유저의 객체정보가 없어져서 어떤 유저를 탈퇴시켜야하는지 모르기 때문
djangoframeworkauthenticationsignuplogin
profile

조정곤

주니어 프론트엔드 개발자

github
linkedin
instagram
email

< 이전글

Django 정적 파일

다음글 >

Django 요청 GET, POST

Django 포스트 (19)

down-arrow