Skip to main content

Authentication System

Authentication System
  • The Django Authentication System
  • 쿠키와 세션
  • 로그인
  • 로그아웃
  • 로그인 사용자에 대한 접근 제한
  • 회원가입
  • 회원탈퇴
  • 회원정보 수정
  • 비밀번호 변경

The Django Authentication System

  • Django 인증 시스템은 django.contrib.auth에 Django contrib module로 제공
  • 필수 구성은 settings.py에 이미 포함되어 있으며 INSTALLED_APPS 설정에 나열된 아래 두 항목으로 구성됨
    1. django.contrib.auth
      • 인증 프레임워크의 핵심과 기본 모델을 포함
    2. django.contrib.contenttypes
      • 사용자가 생성한 모델과 권한을 연결할 수 있음
# settings.py
INSTALLED_APPS [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

  • Django 인증 시스템은 인증(Authentication)권한(Authorization) 부여를 함께 제공(처리)하며, 이러한 기능이 어느 정도 결합되어 일반적으로 인증 시스템이라고 함
  • 공식문서
  • 공식문서

Authentication & Authorization

  • Authentication (인증)
    • 신원 확인
    • 사용자가 자신이 누구인지 확인하는 것
  • Authorization (권한, 허가)
    • 권한 부여
    • 인증된 사용자가 수행할 수 있는 작업을 결정

두번째 앱 (accounts) 생성하기

$ python manage.py startapp accounts
  • app 이름이 반드시 accounts 일 필요는 없음
  • 단, auth와 관련해 django 내부적으로 accounts라는 이름으로 사용되고 있기 때문에 되도록 accounts로 지정하는 것을 권장

# settings.py

INSTALLED_APPS = [
'articles',
'accounts',
...
]
# crud/urls.py

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

from django.urls import path
from . import views

app_name = 'accounts'
urlpatterns = [

]

쿠키와 세션

HTTP

  • Hyper Text Transfer Protocol
    • HTML 문서와 같은 리소스(자원, 데이터)들을 가져올 수 있도록 해주는 프로토콜(규칙, 규약)
    • 웹에서 이루어지는 모든 데이터 교환의 기초
    • 클라이언트-서버 프로토콜이기도 함
  • 참고

HTTP 특징

  • 비연결지향 (connectionless)
    • 서버는 요청에 대한 응답을 보낸 후 연결을 끊음
  • 무상태 (stateless)
    • 연결을 끊는 순간 클라이언트와 서버 간의 통신이 끝나며 상태 정보가 유지되지 않음
    • 클라이언트와 서버가 주고 받는 메시지들은 서로 완전히 독립적임
  • 클라이언트와 서버의 지속적인 관계를 유지하기 위해 쿠키와 세션이 존재

쿠키(Cookie)

  • 서버가 사용자의 웹 브라우저에 전송하는 작은 데이터 조각
  • 사용자가 웹사이트를 방문할 경우 해당 웹사이트의 서버를 통해 사용자의 컴퓨터에 설치되는 작은 기록 정보 파일
    • 브라우저(클라이언트)는 쿠키를 로컬에 KEY-VALUE의 데이터 형식으로 저장
    • 이렇게 쿠키를 저장해 놓았다가, 동일한 서버에 재요청 시 저장된 쿠키를 함께 전송

참고

소프트웨어가 아니기 때문에 프로그램처럼 실행 될 수 없으며 악성코드를 설치 할 수 없지만, 사용자의 행동을 추적하거나 쿠키를 훔쳐서 해당 사용자의 계정 접근권한을 획득 할 수도 있음


  • HTTP 쿠키는 상태가 있는 세션을 만들어 줌
  • 쿠키는 두 요청이 동일한 브라우저에서 들어왔는지 아닌지를 판단할 때 주로 사용
    • 이를 이용해 사용자의 로그인 상태를 유지할 수 있음
    • 상태가 없는(stateless) HTTP 프로토콜에서 상태 정보를 기억 시켜주기 때문
  • 웹 페이지에 접속하면 요청한 웹 페이지를 받으며 쿠키를 저장하고, 클라이언트가 같은 서버에 재요청 시 요청과 함께 쿠키도 함께 전송

요청과 응답

image-20210915092316431


쿠키의 사용 목적

  1. 세션 관리 (Session management)
    • 로그인, 아이디 자동 완성, 공지 하루 안보기, 팝업 체크, 장바구니 등의 정보 관리
  2. 개인화 (Personalization)
    • 사용자 선호, 테마 등의 설정
  3. 트래킹 (Tracking)
    • 사용자 행동을 기록 및 분석

쿠키를 이용한 장바구니 예시

  • 장바구니에 상품 담기

image-20210915094010284


  • 개발자 도구 - Network 탭 - cartView.pang 확인
  • 서버는 응답과 함께 Set-Cookie 응답 헤더를 브라우저에게 전송 이 헤더는 클라이언트에게 쿠키를 저장하라고 전달

image-20210915094249558


  • Cookie 데이터 자세히 확인

image-20210915094338550


  • 메인 페이지 이동 - 장바구니 유지 상태 확인 이제 서버로 전송되는 모든 요청과 함께, 브라우저는 Cookie HTTP 헤더를 사용해 서버로 이전에 저장했던 모든 쿠키들을 함께 전송
  • 개발자 도구 - Application 탭 - Cookies 마우스 우측 버튼 - Clear 후 새로고침

image-20210915094451175

  • 빈 장바구니로 변경 확인

세션(Session)

  • 사이트와 특정 브라우저 사이의 "상태(state)"를 유지시키는 것
  • 클라이언트가 서버에 접속하면 서버가 특정 session id를 발급하고, 클라이언트는 발급 받은 session id를 쿠키에 저장
    • 클라이언트가 다시 서버에 접속하면 요청과 함께 쿠키(session id가 저장된)를 서버에 전달
    • 쿠키는 요청 때마다 서버에 함께 전송되므로 서버에서 session id를 확인해 알맞은 로직을 처리
  • ID는 세션을 구별하기 위해 필요하며, 쿠키에는 ID만 저장함

세션을 이용한 Gitlab 예시

  • Gitlab 로그인 - 개발자 도구 - Application 탭 : gitlab 서버로부터 받아 저장된 session 쿠키 확인
  • session 삭제 후 새로고침 - 로그아웃 상태 변경 확인

쿠키 lifetime (수명)

쿠키의 수명은 두 가지 방법으로 정의할 수 있음

  1. Session cookies
    • 현재 세션이 종료되면 삭제됨
    • 브라우저가 “현재 세션(current session)”이 종료되는 시기를 정의
참고

일부 브라우저는 다시 시작할 때 세션 복원(session restoring)을 사용해 세션 쿠키가 오래 지속 될 수 있도록 함


  1. Persistent cookies (or Permanent cookies)
    • Expires 속성에 지정된 날짜 혹은 Max-Age 속성에 지정된 기간이 지나면 삭제

Session in Django

  • Django의 세션은 미들웨어를 통해 구현됨
  • Django는 database-backed sessions 저장 방식을 기본 값으로 사용
참고

설정을 통해 cached, file-based, cookie-based 방식으로 변경 가능


  • Django는 특정 session id를 포함하는 쿠키를 사용해서 각각의 브라우저와 사이트가 연결된 세션을 알아냄
    • 세션 정보는 Django DB의 django_session 테이블에 저장됨
  • 모든 것을 세션으로 사용하려고 하면 사용자가 많을 때 서버에 부하가 걸릴 수 있음
  • 공식문서

Authentication System in MIDDLEWARE

  • SessionMiddleware : 요청 전반에 걸쳐 세션을 관리
  • Authentication Middleware : 세션을 사용하여 사용자를 요청과 연결
# settings.py

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
...
]

참고: MIDDLEWARE (미들웨어)
  • http 요청과 응답 처리 중간에서 작동하는 시스템(hooks)
  • django는 http 요청이 들어오면 미들웨어를 거쳐 해당 URL에 등록되어 있는 view로 연결해주고, http 응답 역시 미들웨어를 거쳐서 내보냄
  • 주로 데이터 관리, 애플리케이션 서비스, 메시징, 인증 및 API 관리를 담당
  • 공식문서

로그인

  • 로그인은 Session을 Create하는 로직과 같음
  • Django는 우리가 session의 메커니즘에 생각하지 않게끔 도움을 줌
  • 이를 위해 인증에 관한 built-in forms를 제공

Authentication Form

  • 사용자 로그인을 위한 form
  • request를 첫번째 인자로 취함
  • 공식문서

login 함수

login(request, user, backend=None)

  • 현재 세션에 연결하려는 인증 된 사용자가 있는 경우 login() 함수가 필요
  • 사용자를 로그인하며 view 함수에서 사용 됨
  • HttpRequest 객체와 User 객체가 필요
  • django의 session framework를 사용하여 세션에 user의 ID를 저장(== 로그인)
  • 공식문서

# accounts/urls.py

from django.urls import path
from . import views

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

from django.shortcuts import render, redirect
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import login as auth_login
from django.views.decorators.http import require_http_methods


@require_http_methods(['GET', 'POST'])
def login(request):
if request.method == 'POST':
form = AuthenticationForm(request, request.POST)
if form.is_valid():
auth_login(request, form.get_user())
return redirect('articles:index')
else:
form = AuthenticationForm()
context = {
'form': form
}
return render(request, 'accounts/login.html', context)
<!-- accounts/login.html -->

{% extends 'base.html' %}

{% block content %}
<h1>Login</h1>
<form action="{% url 'accounts:login' %}" method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit">
</form>
{% endblock content %}

  • admin 계정 만들고 accounts에 로그인
  • 브라우저와 django DB에서 django로부터 발급 받은 sessionid 확인

image-20210915103658067

image-20210915103756069


get_user()

  • AuthenticationForm의 인스턴스 메서드
  • user_cache는 인스턴스 생성 시에 None으로 할당되며, 유효성 검사를 통과했을 경우 로그인 한 사용자 객체로 할당 됨
  • 인스턴스의 유효성을 먼저 확인하고, 인스턴스가 유효할 때만 user를 제공하려는 구조
class Authentication Form(forms. Form):
"""
Base class for authenticating users. Extend this to get a form that accepts
username/password logins.
"""

def get_user(self):
return self.user_cache

Authentication data in templates

  • 로그인 링크 작성
  • 현재 로그인 되어 있는 유저 정보 출력
  <div class="container">
<h3>Hello, {{ user }}</h3>
<a href="{% url 'accounts:login' %}">Login</a>
{% block content %}
{% endblock content %}
</div>
  • admin 로그인 전

image-20210915104320187

  • admin 로그인 후

image-20210915104300181


context processors

  • 템플릿이 렌더링 될 때 자동으로 호출 가능한 컨텍스트 데이터 목록
  • 작성된 프로세서는 RequestContext에서 사용 가능한 변수로 포함됨
# 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',
],
},
},
]

Users

  • 템플릿 RequestContext를 렌더링할 때, 현재 로그인한 사용자를 나타내는 auth.User 인스턴스 (또는 클라이언트가 로그인하지 않은 경우 AnonymousUser 인스턴스)는 템플릿 변수 {{ user }}에 저장됨
  • 'django.contrib.auth.context_processors.auth'

로그아웃

  • 로그아웃은 Session을 Delete 하는 로직과 같음


logout 함수

logout(request)

  • HttpRequest 객체를 인자로 받고 반환 값이 없음
  • 사용자가 로그인하지 않은 경우 오류를 발생시키지 않음
  • 현재 요청에 대한 session data를 DB에서 완전히 삭제하고, 클라이언트의 쿠키에서도 sessionid 가 삭제됨
  • 이는 다른 사람이 동일한 웹 브라우저를 사용하여 로그인하고, 이전 사용자의 세션 데이터에 액세스하는 것을 방지하기 위함
  • 공식문서

# accounts/urls.py

from django.urls import path
from . import views

app_name = 'accounts'
urlpatterns = [
path('login/', views.login, name='login'),
path('logout/', vies.logout, name='logout'),
]
# accounts/urls.py

from django.contrib.auth import logout as auth_logout
from django.views.decorators.http import require_http_methods, require_POST

@require_POST
def logout(request):
auth_logout(request)
return redirect('articles:index')
<!-- base.html -->

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

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

Limiting access to logged-in users

  • 로그인 사용자에 대한 엑세스 제한 2가지 방법
    1. The raw way
      • is_authenticated attribute
    2. The login_required decorator
  • 공식문서

is_authenticated

  • User model의 속성(attributes) 중 하나
  • 모든 User 인스턴스에 대해 항상 True 인 읽기 전용 속성 (AnonymousUser에 대해서는 항상 False)
  • 사용자가 인증 되었는지 여부를 알 수 있는 방법
  • 일반적으로 request.user에서 이 속성을 사용하여, 미들웨어의 django.contrib.auth.middleware.AuthenticationMiddleware를 통과 했는지 확인
  • 단, 권한(permission)과는 관련이 없으며, 사용자가 활성화 상태(active)이거나 유효한 세션(valid session)을 가지고 있는지도 확인하지 않음
  • 공식문서

  • 적용 : 로그인과 비로그인 상태에서 출력되는 링크를 다르게 설정
# views.py

@require_http_methods(['GET', 'POST'])
def login(request):
if request.user.is_authenticated:
return redirect('articles:index')

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


@require_POST
def logout(request):
if request.user.is_authenticated:
auth_logout(request)
return redirect('articles:index')
<!-- base.html -->

<div class="container">
{% if request.user.is_authenticated %}
<h3>Hello, {{ user }}</h3>
<form action="{% url 'accounts:logout' %}" method="POST">
{% csrf_token %}
<input type="submit" value="Logout">
</form>
{% else %}
<a href="{% url 'accounts:login' %}">Login</a>
{% endif %}
{% block content %}
{% endblock content %}
</div>
<!-- index.html -->
{% if request.user.is_authenticated %}
<a href="{% url 'articles:create' %}">[CREATE]</a>
{% else %}
<a href="{% url 'accounts:login' %}">[새 글을 작성하려면 로그인하세요.]</a>
{% endif %}

login_required decorator

  • 사용자가 로그인되어 있지 않으면, settings.LOGIN_URL에 설정된 문자열 기반 절대 경로로 redirect 함
    • LOGIN_URL의 기본 값은 '/accounts/login/'
    • 두번째 app 이름을 accounts 로 했던 이유 중 하나
  • 사용자가 로그인되어 있으면 정상적으로 view 함수를 실행
  • 인증 성공 시 사용자가 redirect 되어야하는 경로는 "next"라는 쿼리 문자열 매개 변수에 저장됨
    • 예시) /accounts/login/?next=/articles/create/
  • 공식문서
  • 공식문서

# articles/views.py

from django.contrib.auth.decorators import login_required

@login_required
@require_http_methods(['GET', 'POST'])
def create(request):
...


@login_required
@require_POST
def delete(request, pk):
...


@login_required
@require_http_methods(['GET', 'POST'])
def update(request, pk):
...
  1. view 함수에 login_required 데코레이터 작성
  2. 비로그인 상태에서 /accounts/create/ 경로로 요청 보내기
  3. URL에 next 문자열 매개변수 확인
http://127.0.0.1:8000/accounts/login/?next=/articles/create/

next query string parameter

  • 로그인이 정상적으로 진행되면 기존에 요청했던 주소로 redirect 하기 위해 마치 주소를 keep 해주는 것
  • 단, 별도로 처리 해주지 않으면 우리가 view에 설정한 redirect 경로로 이동하게 됨
# accounts/views.py

@require_http_methods(['GET', 'POST'])
def login(request):
if request.user.is_authenticated:
return redirect('articles:index')

if request.method == 'POST':
form = AuthenticationForm(request, request.POST)
if form.is_valid():
auth_login(request, form.get_user())
return redirect(request.GET.get('next') or 'articles:index') # next query
else:
form = AuthenticationForm()
context = {
'form': form
}
return render(request, 'accounts/login.html', context)

  • 현재 URL로 (next parameter가 있는) 요청을 보내기 위해 action 값 비우기
<!-- accounts/login.html -->

{% extends 'base.html' %}

{% block content %}
<h1>Login</h1>
<form action="" method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit">
</form>
{% endblock content %}

두 데코레이터로 인해 발생하는 구조적 문제와 해결

  • 비로그인 상태에서 게시글 삭제 시도 → redirect로 이동한 로그인 페이지에서 로그인 시도 → 405(Method Not Allowed) status code 확인
  • @require_POST 작성된 함수에 @login_required를 함께 사용하는 경우 에러 발생
  • 로그인 이후 "next" 매개변수를 따라 해당 함수로 다시 redirect 되는데, 이 때 @require_POST 때문에 405 에러가 발생하게 됨
  • 두 가지 문제 발생
    1. redirect 과정에서 POST 데이터의 손실
    2. redirect 요청은 POST 방식이 불가능하기 때문에 GET 방식으로 요청됨

  • 로그인 성공 후 next에 담긴 경로로 리다이렉트 될 때 에러 발생

image-20210915203033372


  • login_required는 GET method request를 처리할 수 있는 view 함수에서만 사용해야 함.
"""
@login_required
@require_POST
def delete(request, pk):
article = get_object_or_404(Article, pk=pk)
article.delete()
return redirect('articles:index')
"""

@require_POST
def delete(request, pk):
if request.user.is_authenticated:
article = get_object_or_404(Article, pk=pk)
article.delete()
return redirect('articles:index')

회원가입

UserCreationForm

  • 주어진 username과 password로 권한이 없는 새 user를 생성하는 ModelForm
  • 3개의 필드를 가짐
    1. username (from the user model)
    2. password1
    3. password2
  • 공식문서
  • Github
# UserCreationForm 확인해보기

class UserCreationForm(forms.ModelForm):
"""
A form that creates a user, with no privileges, from the given username and
password.
"""
error_messages = {
'password_mismatch': _('The two password fields didn’t match.'),
}
password1 = forms.CharField(
label=_("Password"),
strip=False,
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
help_text=password_validation.password_validators_help_text_html(),
)
password2 = forms.CharField(
label=_("Password confirmation"),
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
strip=False,
help_text=_("Enter the same password as before, for verification."),
)
...

# accounts/urls.py

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

from django.contrib.auth.forms import AuthenticationForm, 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)
<!-- accounts/signup.html -->

{% extends 'base.html' %}

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

  • 회원가입 후 admin 페이지에서 새로운 계정이 생겼는지 확인

image-20210915205428275


@require_http_methods(['GET', 'POST'])
def signup(request):
if request.method == 'POST':
form = UserCreationForm(request.POST)
if form.is_valid():
user = form.save()
auth_login(request, user)
return redirect('articles:index')
else:
form = UserCreationForm()
context = {
'form': form,
}
return render(request, 'accounts/signup.html', context)
# UserCreationForm의 save 메서드
def save(self, commit=True):
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user

  • 회원가입 링크 작성
<!-- base.html -->

<div class="container">
{% if request.user.is_authenticated %}
<h3>Hello, {{ user }}</h3>
<form action="{% url 'accounts:logout' %}" method="POST">
{% csrf_token %}
<input type="submit" value="Logout">
</form>
{% else %}
<a href="{% url 'accounts:login' %}">Login</a>
<a href="{% url 'accounts:signup' %}">Signup</a>
{% endif %}
{% block content %}
{% endblock content %}
</div>

회원탈퇴

  • 회원탈퇴는 DB에서 사용자를 삭제하는 것과 같음
# accounts/urls.py

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

@require_POST
def delete(request):
if request.user.is_authenticated:
request.user.delete()
return redirect('articles:index')
<!-- base.html -->

<div class="container">
{% 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>
{% else %}
<a href="{% url 'accounts:login' %}">Login</a>
<a href="{% url 'accounts:signup' %}">Signup</a>
{% endif %}
{% block content %}
{% endblock content %}
</div>
  • 회원탈퇴 진행 후 sqlite 확장 프로그램이나 admin 페이지에서 유저가 삭제 되었는지 확인

  • 탈퇴 하면서 해당 유저의 세션 데이터도 함께 지울 경우 (단, 반드시 탈퇴 후 로그아웃 순으로 처리해야 함)
# accounts/views.py

@require_POST
def delete(request):
if request.user.is_authenticated:
request.user.delete()
auth_logout(request)
return redirect('articles:index')

회원정보 수정

UserChangeForm

  • 사용자의 정보 및 권한을 변경하기 위해 admin 인터페이스에서 사용되는 ModelForm
  • 공식문서
class UserChangeForm(forms.ModelForm):
password = ReadOnlyPasswordHashField(
label=_("Password"),
help_text=_(
'Raw passwords are not stored, so there is no way to see this '
'user’s password, but you can change the password using '
'<a href="{}">this form</a>.'
),
)

class Meta:
model = User
fields = '__all__'
field_classes = {'username': UsernameField}
...

# accounts/urls.py

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

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


@require_http_methods(['GET', 'POST'])
def update(request):
if request.method == 'POST':
pass
else:
form = UserChangeForm(instance=request.user)
context = {
'form': form,
}
return render(request, 'accounts/update.html', context)
<!-- accounts/update.html -->

{% extends 'base.html' %}

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

  • 회원정보 수정 페이지 확인

image-20210915212035774


  • 회원정보 수정 페이지 링크 작성
<!-- base.html -->

<div class="container">
{% if request.user.is_authenticated %}
<h3>Hello, {{ user }}</h3>
<a href="{% url 'accounts:update' %}">회원정보 수정</a>
<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>
{% else %}
<a href="{% url 'accounts:login' %}">Login</a>
<a href="{% url 'accounts:signup' %}">Signup</a>
{% endif %}
{% block content %}
{% endblock content %}
</div>

UserChangeForm 사용 시 문제점

  • 일반 사용자가 접근해서는 안될 정보들(fields)까지 모두 수정이 가능해짐
  • 따라서 UserChangeForm을 상속받아 CustomUserChangeForm이라는 서브클래스를 작성해 접근 가능한 필드를 조정해야 함

CustomUserChangeForm 작성

  1. get_user_model()
  2. User 모델의 fields
# accounts/forms.py

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


class CustomUserChangeForm(UserChangeForm):

class Meta:
model = get_user_model()
fields = ()

get_user_model()

  • 현재 프로젝트에서 활성화된 사용자 모델(active user model)을 반환
  • django는 User 클래스를 직접 참조하는 대신 django.contrib.auth.get_user_model() 을 사용하여 참조해야 한다고 강조
  • User model 참조에 대한 자세한 내용은 추후 모델 관계 수업에서 다룸
  • 공식문서
  • Github

User 클래스 상속 구조 살펴보기

  1. UserChangeForm 클래스 구조 확인
    • Meta 클래스를 보면 User라는 model을 참조하는 ModelForm이라는 것을 확인할 수 있음
    • UserChangeForm
  2. User 클래스 구조 확인
    • 실제로 User 클래스는 Meta 클래스를 제외한 코드가 없고 AbstractUser 클래스를 상속받고 있음
    • User
  3. AbstractUser 클래스 구조 확인
    • 클래스 변수명들을 확인해보면 회원수정 페이지에서 봤던 필드들과 일치한다는 것을 확인할 수 있음
    • AbstractUser
  4. 마지막으로 공식문서의 User 모델 Fields 확인

  • 수정 시 필요한 필드만 선택해서 작성
# accounts/forms.py

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


class CustomUserChangeForm(UserChangeForm):

class Meta:
model = get_user_model()
fields = ('email', 'first_name', 'last_name',)

- `CustomUserChangeForm`으로 변경
# accounts/views.py

from django.contrib.auth.decorators import login_required
from .forms import CustomUserChangeForm


@login_required
@require_http_methods(['GET', 'POST'])
def update(request):
if request.method == 'POST':
form = CustomUserChangeForm(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)

image-20210915213137413


비밀번호 변경

PasswordChangeForm

  • 사용자가 비밀번호를 변경할 수 있도록 하는 Form
  • 이전 비밀번호를 입력하여 비밀번호를 변경할 수 있도록 함
  • 이전 비밀번호를 입력하지 않고 비밀번호를 설정할 수 있는 SetPasswordForm을 상속받는 서브 클래스
  • 공식문서
  • Github

# accounts/urls.py

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

from django.contrib.auth.forms import (
AuthenticationForm,
UserCreationForm,
PasswordChangeForm
)


@login_required
@require_http_methods(['GET', 'POST'])
def change_password(request):
if request.method == 'POST':
form = PasswordChangeForm(request.user, 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)
<!-- accounts/change_password.html -->

{% extends 'base.html' %}

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

image-20210915213831882


SetPasswordForm

  • PasswordChangeForm의 첫번째 인자가 user인 이유
class SetPasswordForm(forms.Form):
"""
A form that lets a user change set their password without entering the old
password
"""
error_messages = {
'password_mismatch': _('The two password fields didn’t match.'),
}
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
strip=False,
help_text=password_validation.password_validators_help_text_html(),
)
new_password2 = forms.CharField(
label=_("New password confirmation"),
strip=False,
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
)

def __init__(self, user, *args, **kwargs):
self.user = user
super().__init__(*args, **kwargs)
...

암호 변경 시 세션 무효화 방지

update_session_auth_hash(request, user)

  • 현재 요청(current request)과 새 session hash가 파생 될 업데이트 된 사용자 객체를 가져오고, session hash를 적절하게 업데이트
  • 비밀번호가 변경되면 기존 세션과의 회원 인증 정보가 일치하지 않게 되어 로그인 상태를 유지할 수 없기 때문
  • 암호가 변경되어도 로그아웃되지 않도록 새로운 password hash로 session을 업데이트 함
  • 공식문서
# accounts/views.py

from django.contrib.auth import update_session_auth_hash


@login_required
@require_http_methods(['GET', 'POST'])
def change_password(request):
if request.method == 'POST':
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid():
form.save()
update_session_auth_hash(request, form.user)
return redirect('articles:index')
else:
form = PasswordChangeForm(request.user)
context = {
'form': form,
}
return render(request, 'accounts/change_password.html', context)

마무리

  • The Django Authentication System
  • 쿠키와 세션
  • 로그인
  • 로그아웃
  • 로그인 사용자에 대한 접근 제한
  • 회원가입
  • 회원탈퇴
  • 회원정보 수정
  • 비밀번호 변경