책 '점프 투 플라스크'를 공부하면서 정리한 내용입니다.
출처 : https://wikidocs.net/book/4542
forms.py에 로그인 폼 만들기
# --------------------------------- [edit] ---------------------------------- #
class UserLoginForm(FlaskForm):
username = StringField('사용자이름', validators=[DataRequired(), Length(min=3, max=25)])
password = PasswordField('비밀번호', validators=[DataRequired()])
# --------------------------------------------------------------------------- #
auth_view.py에 로그인을 수행할 라우트 함수 작성
# --------------------------------- [edit] ---------------------------------- #
from flask import Blueprint, url_for, render_template, flash, request, session
from werkzeug.security import generate_password_hash, check_password_hash
# --------------------------------------------------------------------------- #
from werkzeug.utils import redirect
from pybo import db
# --------------------------------- [edit] ---------------------------------- #
from pybo.forms import UserCreateForm, UserLoginForm
# --------------------------------------------------------------------------- #
(... 생략 ...)
# --------------------------------- [edit] ---------------------------------- #
@bp.route('/login/', methods=('GET', 'POST'))
def login():
form = UserLoginForm()
if request.method == 'POST' and form.validate_on_submit():
error = None
user = User.query.filter_by(username=form.username.data).first()
if not user:
error = "존재하지 않는 사용자입니다."
elif not check_password_hash(user.password, form.password.data):
error = "비밀번호가 올바르지 않습니다."
if error is None:
session.clear()
session['user_id'] = user.id
return redirect(url_for('main.index'))
flash(error)
return render_template('auth/login.html', form=form)
# --------------------------------------------------------------------------- #
POST 방식 요청에는 로그인을 수행하고, GET 방식 요청에는 로그인 템플릿을 렌더링한다.
POST 방식 요청으로 로그인 작업 수행 과정
1. 폼 입력으로 받은 username으로 DB에 해당 사용자 있는지 검사
2. 만약 없으면 '존재하지 않는 사용자입니다.' 오류 발생
3. 존재한다면 폼 입력으로 받은 password와 check_password_hash 함수를 사용하여 DB의 비밀번호와 일치하는지 비교. DB에 저장된 비밀번호는 암호화되었으므로 입력된 비밀번호도 반드시 check_password_hash 함수로 똑같이 암호화하여 비교해야 한다.
4. 사용자도 존재하고 비밀번호도 올바르면 플라스크 세션(session)에 키와 키값을 저장. 키에는 'user_id'라는 문자열 저장, 키값은 DB에서 조회된 사용자의 id값 저장
- 세션(session)
- request와 마찬가지로 플라스크가 자동으로 생성하여 제공하는 변수
- 플라스크 서버를 구동하는 동안에 영구히 참조할 수 있는 값
- 다양한 URL 요청에 session값 사용 가능 ex) 현재 웹 브라우저를 요청한 주체가 로그인한 사용자인지 판별 가능.
- request vs session
request는 요청, 응답 과정만 사용할 수 있는 반면, session은 플라스크 서버를 구동하는 동안 영구히 사용할 수 있는 값이므로 사용자 id를 저장하거나 활용하는데 적합. 단, 시간제한이 있어서 일정 시간 접속하지 않으면 자동 삭제
- 웹 브라우저와 서버의 실행 방식과 쿠키, 세션의 이해
웹 프로그램은 [웹 브라우저 요청 → 서버 응답] 순서로 실행되며, 서버 응답이 완료되면 웹 브라우저와 서버 사이의 연결은 끊어짐.
이 때 요청한 것 중 같은 브라우저인지 아닌지를 구분하게 해주는 것이 쿠키(Cookie).
- 쿠키(Cookie)란? 쿠키는 웹 브라우저를 구별하는 값
- 웹 브라우저는 서버에서 받은 쿠키를 저장
- 이후 서버에 다시 요청시 이 쿠키 전송
- 서버는 웹 브라우저가 보낸 쿠키를 보고 이전에 보냈던 쿠키와 비교
이 때 세션은 바로 쿠키 1개당 생성되는 서버의 메모리 공간이라고 할 수 있다.
auth/login.html 파일을 만들어서 로그인 폼에서 생성한 필드 2개(username, password)를 input 엘리먼트로 생성
{% extends "base.html" %}
{% block content %}
<div class="container my-3">
<form method="post" class="post-form">
{{ form.csrf_token }}
{% include "form_errors.html" %}
<div class="form-group">
<label for="username">사용자 이름</label>
<input type="text" class="form-control" name="username" id="username"
value="{{ form.username.data or '' }}">
</div>
<div class="form-group">
<label for="password">비밀번호</label>
<input type="password" class="form-control" name="password" id="password"
value="{{ form.password.data or '' }}">
</div>
<button type="submit" class="btn btn-primary">로그인</button>
</form>
</div>
{% endblock %}
템플릿에서 <로그인> 버튼을 누르면 form 엘리먼트가 현재 웹 브라우저의 주소 창에 표시된 URL인 /auth/login/을 통해 POST 방식으로 요청된다.
<li class="nav-item ">
<!-- ------------------------------ [edit] -------------------------------- -->
<a class="nav-link" href="{{ url_for('auth.login') }}">로그인</a>
<!-- ---------------------------------------------------------------------- -->
</li>
로그인을 수행한 후에도 여전히 내비게이션 바에 여전히 '로그인'링크가 남아 있는데 '로그아웃'링크로 바뀌어야 한다. session에 저장된 값을 조사해서 사용자의 로그인 여부를 파악한다.
auth_views.py파일에 load_logged_in_user 함수 구현
# --------------------------------- [edit] ---------------------------------- #
from flask import Blueprint, url_for, render_template, flash, request, session, g
# --------------------------------------------------------------------------- #
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import redirect
(... 생략 ...)
# --------------------------------- [edit] ---------------------------------- #
@bp.before_app_request
def load_logged_in_user():
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = User.query.get(user_id)
# --------------------------------------------------------------------------- #
@bp.before_app_request 어노테이션을 사용. 이 어노테이션이 적용된 함수는 라우트 함수보다 먼저 실행된다. -> load_logged_in_user 함수는 모든 라우트 함수보다 먼저 실행
g : 플라스크가 제공하는 컨텍스트 변수로 request 변수와 마찬가지로 [요청 → 응답] 과정에서 유효하다.
이후 사용자 로그인 검사시 session을 조사할 필요가 없이 g.user에 값이 있는지만 알아내면 된다. g.user에는 User 객체가 있으므로 username, email 등 추가 정보 가능.
<div class="collapse navbar-collapse flex-grow-0" id="navbarNav">
<!-- ------------------------------ [edit] -------------------------------- -->
{% if g.user %}
<ul class="navbar-nav">
<li class="nav-item ">
<a class="nav-link" href="#">{{ g.user.username }} (로그아웃)</a>
</li>
</ul>
{% else %}
<!-- ---------------------------------------------------------------------- -->
<ul class="navbar-nav">
<li class="nav-item ">
<a class="nav-link" href="{{ url_for('auth.signup') }}">계정생성</a>
</li>
<li class="nav-item ">
<a class="nav-link" href="{{ url_for('auth.login') }}">로그인</a>
</li>
</ul>
<!-- ------------------------------ [edit] -------------------------------- -->
{% endif %}
<!-- ---------------------------------------------------------------------- -->
</div>
g.user는 load_logged_in_user 함수로 생성한 정보값으로 로그인되어 있다면 g.user가 만들어진 상태니까 username 값과 '로그아웃'링크를 보여줄 것이다.
auth_views.py에 /logout/ 라우트 URL에 매핑되는 logout 함수를 작성
@bp.route('/logout/')
def logout():
session.clear()
return redirect(url_for('main.index'))
session.clear()을 추가하여 세션에 저장된 user_id등 모든 값 삭제. -> g.user == None
'로그아웃' 링크 활성화
<li class="nav-item ">
<!-- ------------------------------ [edit] -------------------------------- -->
<a class="nav-link" href="{{ url_for('auth.logout') }}">{{ g.user.username }} (로그아웃)</a>
<!-- ---------------------------------------------------------------------- -->
</li>