[REAL Python – Flask] – “블로그 웹 애플리케이션 개발(1) – 로그인/회원가입 처리하기”
[REAL Python – Flask] – “블로그 웹 애플리케이션 개발(1) – 로그인/회원가입 처리하기”
https://youtu.be/W4GItcW7W-U 을 보고 추가/정리한 글입니다.
로그인, 회원가입 처리의 기본적인 아이디어
일반적으로 로그인을 하는 과정, 회원가입을 하는 과정을 생각해 봅시다. 보통 아이디, 비밀번호, 신상정보 등을 입력하겠죠. 그것을 토대로 회원가입일 경우에는 새로운 유저를 추가하는 과정을, 로그인일 경우에는 아이디와 비밀번호 정보가 맞는지 확인하고, 맞다면 로그인시켜주는 작업을 서버에서 구현할 겁니다. 그렇기에, 서버는 로그인을 하던 회원가입을 하던 폼으로 사용자의 데이터를 받아오는 과정을 필연적으로 거쳐야 합니다. 그리고, 우리는 폼으로부터 데이터를 POST 방식으로 받아올 겁니다.
회원가입, 로그인 폼의 form method = “POST”
이전에 만들었던 폼 코드의 일부분입니다.
결론적으로 위의 모습을 가지는 폼이 완성된 것이고, “SIGN UP” 을 누르면,
POST 요청이 이루어진 것을 볼 수 있습니다. 그러므로, 우리는 sign-up에서 POST 요청을 처리하는 코드를 작성할 겁니다.
from flask import Blueprint, render_template, redirect
auth = Blueprint("auth", __name__)
@auth.route("/login", methods=['GET', 'POST']) # 로그인에서 POST 요청을 처리해야 함.
def login():
return render_template("login.html")
@auth.route("/logout")
def logout():
return redirect("views.blog_home") # 로그아웃하면 views의 blog_home으로 리다이렉트됨
@auth.route("/sign-up", methods=['GET', 'POST']) # 회원가입에서 POST 요청을 처리해야 함.
def signup():
return render_template("signup.html")
그러면, 위처럼 methods=[‘GET’, ‘POST’] 의 코드를 작성함으로서 처리할 수 있을 것입니다.
이후, 폼에 임의의 값을 작성한 후 제출 버튼을 눌러 봅시다. 응답 상태 코드로 200을 주는 걸 확인할 수 있죠?
Flask의 request로 폼에서 데이터 받아오기
폼 코드의 일부입니다. input 태그의 name 속성이 보이죠? name속성은 폼이 제출된 후 서버에서 데이터를 참조하기 위해 사용됩니다.
플라스크의 request를 임포트해 줍니다.
그리고, 위의 코드를 입력해서 확인해 보겠습니다. get() 메서드의 인자로 name
속성에 사용되었던 email, username 등으로 데이터를 받아오는 것을 볼 수 있죠?
Flask-SQLAlchemy 를 이용한 데이터베이스 처리
위의 게시글에서, 우리는 파이썬의 sqlite3모듈을 사용해서 DB에 값을 넣는 등 쿼리를 직접 날려서 데이터베이스와 소통했었죠? 파이썬에서 그러한 방식으로 데이터베이스를 다룰 수 있다는 것은 좋은 일이지만, 아무래도 많이 복잡합니다.
이쯤에서, ORM이라는 개념을 소개하겠습니다.
의 첫 부분에 ORM에 대한 설명을 해 두었습니다. – 간단히 말하자면 파이썬의 클래스와 데이터베이스를 매핑한 것을 ORM이라고 합니다.
위와 같은 예시가 있을 겁니다. 위는 장고ORM 코드의 일부분인데, 위의 클래스를 ORM이라는 기술이 아래의 관계형 데이터베이스로 처리해 준 것입니다. 플라스크에서는, 맨 처음에 설치했던 flask_sqlalchemy 가 우리의 클래스를 데이터베이스의 테이블로 만드는 작업을 대신해 줄 겁니다. 중요한 건, 우리가 쿼리를 직접 날리는 게 아닌 “파이썬 클래스” 로 데이터베이스를 다룰 거라는 사실입니다. 그 부분을 숙지하고, 아래로 넘어갑시다.
첫째로, 폼으로부터 이메일, 비밀번호 등의 정보들을 잘 받아왔습니다. 둘째로, 이제는 그 받아온 데이터를 데이터베이스에 올려야 회원가입이라는 절차가 마무리되겠죠. 그렇기 위해서는, 일단 유저를 관리하는 유저 테이블이 존재해야 합니다.
유저 테이블을 존재하게끔 하기 위해서는 데이터베이스가 존재해야 합니다.
SQLAlchemy
는 파이썬의 클래스와, 데이터베이스를 매핑해 줄 수 있도록 하는 파이썬 모듈입니다. 그리고 그 모듈을 조금 더 쉽게 사용하기 위해서 사용하는 것이 Flask-SQLAlchemy
입니다.
아래의 코드를 작성해줌으로서 데이터베이스 설정을 해 줍니다.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from os import path
from flask_login import LoginManager
from pprint import pprint
# DB 설정하기
db = SQLAlchemy() # 이 줄 추가!
DB_NAME = "blog_db" # 이 줄 추가!
# app을 만들어주는 함수를 지정해 주자.
def create_app():
app = Flask(__name__) # Flask app 만들기
app.config['SECRET_KEY'] = "IFP"
# DB 설정하기
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_NAME}' # 이 줄 추가!
db.init_app(app) # 이 줄 추가!
from .views import views
# blueprint 등록, '/blog' 를 기본으로 한다.
app.register_blueprint(views, url_prefix="/blog")
from .auth import auth
# blueprint 등록, '/auth' 를 기본으로 한다.
app.register_blueprint(auth, url_prefix="/auth")
return app
이후, __init__.py에 새로운 코드를 하나 추가하겠습니다. 데이터베이스가 존재하지 않는다면 생성하는 코드를 작성해야겠죠? create_app 함수 아래에 바로 아래의 내용을 추가합니다.
# DB 추가
def create_database(app):
if not path.exists("blog/" + DB_NAME): # DB 경로가 존재하지 않는다면,
db.create_all(app=app) # DB를 하나 만들어낸다.
이후, 위의 과정에서 만들어준 함수를 호출해 줍니다. create_all() 은 아래와 같이 Flask-SQLAlchemy에 구현되어 있네요.
데이터베이스는 만들었지만 테이블은 만들어지지 않은 상태입니다.
이제, 데이터베이스에 직접 SQL을 날리지 않고 파이썬 클래스로 테이블을 만드는 작업을 해 볼 겁니다.
models.py
이곳에서는 우리의 데이터베이스 모델에 관한 것을 정의할 것입니다.
from . import db
from flask_login import UserMixin
from sqlalchemy.sql import func
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True) # id : 유일 키, Integer
email = db.Column(db.String(150), unique=True) # email : 같은 이메일을 가지고 있는 유저가 없도록 함, String
username = db.Column(db.String(150), unique=True) # username : 같은 이름을 가지고 있는 유저가 없도록 함, String
password = db.Column(db.String(150)) # password : 비밀번호, String
created_at = db.Column(db.DateTime(timezone=True), default=func.now()) # 생성일자, 기본적으로 현재가 저장되도록 함
위의 코드를 작성함으로서 모델을 하나 정의해 줍시다. SQLAlchemy라는 것이 무엇인지 몰라도, 코드를 보면어떠한 내용의 테이블이 생성될지 대충 예상되지 않나요?
계속 진행해서, 우리는 데이터베이스가 만들어지기 전에 작성했던 유저 모델을 등록해야 합니다. 이 작업을 하지 않으면, 데이터베이스에 테이블이 생성되지 않아요!
Flask-LoginManager() 이용해서 로그인 준비하기
flask_login 라이브러리는 플라스크에서 로그인 기능을 쉽게 구현할 수 있도록 도와주는 라이브러리입니다.
위에서 작성한 models.py 의 일부분입니다. UserMixin에서는 무엇을 제공해주는 것이고 무엇을 위해서 상속받은 걸까요?
클래스를 살펴봅시다. 플라스크 로그인에서 수행하는 메소드에 대한 기본 구현을 제공한다고 적혀 있네요.
위의 코드를 작성함으로서 만약, 로그인이 필요한 곳에 로그인하지 않은 유저가 접근할 경우에 로그인 페이지로 리다이렉트되도록 설정해 줍니다.
실제 LoginManager 클래스의 설명에도 위처럼 주석이 달려 있네요.
login_manager = LoginManager() # LoginManager() 객체를 만들어 준다.
login_manager.login_view = "auth.login" # 만약 로그인이 필요한 곳에 로그인하지 않은 유저가 접근할 경우, 로그인 페이지로 리다리엑트 되도록 해 준다.
login_manager.init_app(app)
# 받은 id로부터, DB에 있는 유저 테이블의 정보에 접근하도록 해 줌.
# login manager는, 유저 테이블의 정보에 접근해, 저장된 세션을 바탕으로 로그인되어 있다면 로그인 페이지로 안 가도 되게끔 해 줌.
@login_manager.user_loader
def load_user_by_id(id):
return User.query.get(int(id))
총, 위의 코드들의 작성을 완료해줍니다.
__init__.py
좋습니다. 이제 로그인을 위한 준비를 했으니, 회원가입을 처리해 볼 겁니다. 유저가 회원가입 폼에서 회원가입을 하는 과정을 잠깐 정리해 볼까요?
- 유저는 /auth/signup 에 접근해서 폼을 작성합니다.
- 이메일이 중복되는지를 검사합니다.
- 유저네임이 중복되는지를 검사합니다.
- 만약 보안을 신경쓸 것이라면 – 비밀번호가 충분히 긴지를 확인합니다.
- …….등등, 데이터가 유효한지를 검사합니다. 예컨대 “사용하면 안 되는 유저네임” 이 있다면, 회원가입되면 안 되겠죠.
- 데이터가 유효하다면, 데이터베이스에 새로운 유저를 한 명 저장합니다.
위의 몇 가지 작업을 거쳐야 합니다. 폼에서 받아온 데이터가 과연 유효한 것인지를 검사하는 것이죠. 우리는 폼 모듈을 사용해서 데이터 검증을 조금 더 쉽게 할 수 있도록 작업해볼 겁니다.
먼저, 라이브러리를 설치해 줍니다. pip install flask-wtf
, pip install email-validator
을 터미널에 입력해 주세요.
그리고, forms.py 파일을 하나 작성한 다음 아래와 같은 코드를 작성해줍니다.
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, PasswordField, EmailField
from wtforms.validators import DataRequired, Email, Length, EqualTo
class SignupForm(FlaskForm):
# email : 필수 입력 항목이며, 이메일의 형식을 유지해야 함.
email = EmailField('email', validators=[DataRequired(), Email()])
# username : 필수 입력 항목이며, 최소 5글자부터 최대 30글자까지 허용됨.
username = StringField('username', validators=[DataRequired(), Length(4, 30)])
# password1 : 필수 입력 항목이며, 최소 8글자부터 최대 30글자까지 허용됨, password2와 값이 같아야 함.
password1 = PasswordField('password', validators=[DataRequired(), Length(8, 30), EqualTo("password2", message="Password must match...")])
password2 = PasswordField('password again', validators=[DataRequired()])
그리고, auth.py에 가서 코드를 아래와 같이 수정합니다.
@auth.route("/sign-up", methods=['GET', 'POST']) # 회원가입에서 POST 요청을 처리해야 함.
def signup():
form = SignupForm()
if request.method == "POST" and form.validate_on_submit():
# 폼으로부터 검증된 데이터 받아오기
signup_user = User(
email=form.email.data,
username=form.username.data,
password=form.password1.data,
)
# 폼에서 받아온 데이터가 데이터베이스에 이미 존재하는지 확인
email_exists = User.query.filter_by(email=form.email.data).first()
username_exists = User.query.filter_by(username=form.username.data).first()
# 이메일 중복 검사
if email_exists:
flash('Email is already in use...', category='error')
# 유저네임 중복 검사
elif username_exists:
flash('Username is already in use...', category='error')
# 위의 모든 과정을 통과한다면, 폼에서 받아온 데이터를 새로운 유저로서 저장
else:
db.session.add(signup_user)
db.session.commit()
flash("User created!!!")
return redirect(url_for("views.home")) # 저장이 완료된 후 home으로 리다이렉트
# GET요청을 보낸다면 회원가입 템플릿을 보여줌
return render_template("signup.html", form=form)
검증이 완료된 폼에서 얻어온 데이터를 새로운 유저로 저장하도록 했습니다.
폼 코드 아래에, {{ form.csrf_token }}
을 입력해 주어야 폼이 작동합니다!
비밀번호 해싱하기
이제 폼에서 받아온 데이터를 데이터베이스에 저장하는 것까진 가능할 겁니다. 만약, 어떤 유저가 회원가입을 했다면, 이 유저의 이메일은 무엇이고, 이 유저의 이름, 비밀번호는 … 이런 식으로 데이터베이스에 모든 정보가 저장될 겁니다. 그렇다면, 데이터베이스에 접근할 수 있는 모든 사람들은 어떤 유저의 비밀번호가 무엇인지를 알 수 있는 것입니다. 아무래도 그건 좋은 방법이 아니죠. 그렇기에 우리는 비밀번호를 알 수 없는 어떠한 요상한 값으로 암호화하여 저장할 겁니다. 그리고 그것을 hashing이라고 합니다.
일단 비밀번호를 저장하기 전 해싱하기 위해서, from werkzeug.security import generate_password_hash, check_password_hash
를 입력해 줍니다.
그리고, 폼에서 받아온 데이터를 generate_password_hash() 메서드를 이용해서 해싱해 줍니다.
실제로 한번 회원가입 폼을 작성해 봅시다.
데이터가 잘 처리되었다면, home 페이지로 리다이렉트 될 것입니다.
그렇게 하고 나서 데이터베이스를 확인해 봅시다. 입력했던 비밀번호 값이 이상한 값으로 암호화되어 있는 것을 확인할 수 있죠?
축하합니다. 회원가입 폼을 만들고, 폼에서 데이터를 검증하고, 받아온 데이터를 데이터베이스에 저장함으로서 회원가입 절차가 마무리되었습니다!
로그인 처리하기
로그인 폼 또한 서버에 POST 요청을 보내도록 작성되어 있습니다. forms.py 에 다음의 로그인 폼 코드를 작성해 줍니다.
class LoginForm(FlaskForm):
email = EmailField('email', validators=[DataRequired(), Email()])
password = PasswordField('password', validators=[DataRequired()])
@auth.route("/login", methods=['GET', 'POST']) # 로그인에서 POST 요청을 처리해야 함.
def login():
form = LoginForm()
if request.method == "POST" and form.validate_on_submit():
# 폼으로부터 검증된 데이터 받아오기
password = form.password.data
# 폼에서 받아온 이메일로 유저 찾기
user = User.query.filter_by(email=form.email.data).first()
# 로그인 폼에서 입력된 이메일이 존재한다면,
if user:
if check_password_hash(user.password, password):
flash("Logged in!", category='success')
login_user(user, remember=True)
return redirect(url_for('views.home'))
else:
flash("Password is incorrect!", category='error')
# 로그인 폼에서 입력된 이메일이 존재하지 않는다면,
else:
flash("Email does not exist...", category='error')
return render_template("login.html", form=form)
로그인은 위의 흐름으로 처리할 수 있습니다.
로그아웃 처리하기
flask-login 의 기능을 이용해 로그아웃을 정말 쉽게 처리할 수 있습니다.
로그아웃에 대한 첫 번째 생각은, 로그인이 되어 있어야 로그아웃을 처리할 수 있다는 것입니다. 로그인도 안 되어 있는데 로그아웃을 처리할 수는 없으니까요.
flask-login에서는 “로그인이 되어 있어야지 뷰를 보여줄 수 있다” 에 대한 기능을 @login_required 데코레이터를 통해 지원합니다.
실제로 구현되어 있는 login_required() 의 일부입니다.
이후, logout_user() 코드를 작성함으로서 로그아웃을 완료해 줍니다. #주의사항! return redirect(url_for("views.home"))
으로 작성해야 합니다!
오류 메시지 나타내기
좋습니다. 로그인, 회원가입, 로그아웃 등의 처리를 했는데, 만약 에러가 날 만한 상황을 맞닥뜨린다면 ( 이미 데이터베이스에 존재하는 이메일로 회원가입을 시도하거나, 로그인할 때에 비밀번호를 틀리게 입력하거나..) 에러 메시지를 띄우도록 처리해 볼 겁니다.
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
base.html 의 <nav> 태그 바로 아래에 위의 코드를 입력해 봅시다.
무언가 모르겠지만, 어떤 메시지들이 위에 떴네요. 그런데, 이 메시지들 뭔가 익숙합니다.
우리는 위에서 카테고리를 설정해 주었습니다. 카테고리로 메시지를 분류할 수 있을 겁니다.
디자인에 맞추어서, {%block header %} {%endblock%} 아래에 메시지를 띄우도록 처리할 수 있습니다.
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
{% for category, message in messages %}
{# 카테고리 == error 이라면, 실패 메시지를 출력 #}
{% if category == "error" %}
<div class="alert alert-danger alert-dismissable fade show" role="alert" style="text-align: center">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{# 그렇지 않다면, 성공 메시지를 출력 #}
{% else %}
<div class="alert alert-success alert-dismissable fade show" role="alert" style="text-align: center">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
위의 코드를 입력하고, 메시지를 확인해 봅시다!
위와 같은 로그인 성공 메시지 등이 잘 나타나는 것을 볼 수 있네요.
추가로, 블루프린트를 수정하겠습니다.
# __init__.py 의 일부분:
from .views import views
# blueprint 등록, '/' 를 기본으로 한다.
app.register_blueprint(views, url_prefix="/")
# views.py 의 일부분:
@views.route("/")
@views.route("/home")
def home():
return render_template("index.html")
동적으로 변화하는 navbar 만들기
보통 로그인하면 “welcome, 닉네임” 와 같은 메시지가 네비게이션 바에 뜨죠. 그리고 이미 로그인을 했다면 로그인 링크가 또 보일 필요가 없습니다. 이러한 부분들을 처리해 보겠습니다.
바뀌는 부분들은 아래와 같겠죠?
일단 “닉네임(username)” 이 표시되기 위해서는 auth.py에서 로그인했을 때에 템플릿에 “현재 로그인한 유저의 정보” 를 넘겨주어야 합니다.
그리고, 네비게이션 바는 base.html에 포함되므로 base.html이 있는 모든 템플릿에 user가 변수로서 전달되어야 합니다.
from flask import Blueprint, render_template
from flask_login import current_user
views = Blueprint("views", __name__)
@views.route("/")
@views.route("/home")
def home():
return render_template("index.html", user=current_user)
@views.route("/about")
def about():
return render_template("about.html", user=current_user)
@views.route("/categories-list")
def categories_list():
return render_template("categories_list.html", user=current_user)
@views.route("/post-list")
def post_list():
return render_template("post_list.html", user=current_user)
@views.route('posts/<int:id>')
def post_detail():
return render_template("post_detail.html", user=current_user)
@views.route("/contact")
def contact():
return render_template("contact.html", user=current_user)
마찬가지로, auth.py에도 동일한 작업이 필요하겠죠?
import logging
from flask_login import login_user, logout_user, current_user, login_required
from . import db
from .forms import SignupForm, LoginForm
from .models import User
from flask import Blueprint, render_template, request, url_for, flash
from werkzeug.utils import redirect
from werkzeug.security import generate_password_hash, check_password_hash
auth = Blueprint("auth", __name__)
@auth.route("/login", methods=['GET', 'POST']) # 로그인에서 POST 요청을 처리해야 함.
def login():
form = LoginForm()
if request.method == "POST" and form.validate_on_submit():
# 폼으로부터 검증된 데이터 받아오기
password = form.password.data
# 폼에서 받아온 이메일로 유저 찾기
user = User.query.filter_by(email=form.email.data).first()
# 로그인 폼에서 입력된 이메일이 존재한다면,
if user:
if check_password_hash(user.password, password):
flash("Logged in!", category='success')
login_user(user, remember=True)
return redirect(url_for('views.home'))
else:
flash("Password is incorrect!", category='error')
# 로그인 폼에서 입력된 이메일이 존재하지 않는다면,
else:
flash("Email does not exist...", category='error')
return render_template("login.html", form=form, user=current_user)
@auth.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("views.home")) # 로그아웃하면 views의 blog_home으로 리다이렉트됨
@auth.route("/sign-up", methods=['GET', 'POST']) # 회원가입에서 POST 요청을 처리해야 함.
def signup():
form = SignupForm()
if request.method == "POST" and form.validate_on_submit():
# 폼으로부터 검증된 데이터 받아오기
signup_user = User(
email=form.email.data,
username=form.username.data,
password=generate_password_hash(form.password1.data),
)
# 폼에서 받아온 데이터가 데이터베이스에 이미 존재하는지 확인
email_exists = User.query.filter_by(email=form.email.data).first()
username_exists = User.query.filter_by(username=form.username.data).first()
# 이메일 중복 검사
if email_exists:
flash("이메일이 이미 존재합니다...", category='error')
# 유저네임 중복 검사
elif username_exists:
flash("유저네임이 중복됩니다...", category='error')
# 위의 모든 과정을 통과한다면, 폼에서 받아온 데이터를 새로운 유저로서 저장
else:
db.session.add(signup_user)
db.session.commit()
flash("User created!!!")
return redirect(url_for("views.home")) # 저장이 완료된 후 home으로 리다이렉트
# GET요청을 보낸다면 회원가입 템플릿을 보여줌
return render_template("signup.html", form=form, user=current_user)
위와 같이 수정한 다음, 넘겨준 변수로 템플릿 태그를 이용해서 네비게이션 바를 동적으로 바꿀 수 있습니다.
{% if user.is_authenticated %}
{# 유저가 로그인했다면 보일 것들 : "Welcome, username", "Logout" 이고, user 정보가 넘어가야 함. #}
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" style="color: red"
href="#">welcome, {{ user.username }}!</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" style="color: red"
href="{{ url_for('auth.logout') }}">Logout</a></li>
{% endif %}
{% if not user.is_authenticated %} {# 그리고, 로그인한 상태가 아니라면... #}
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4"
href="{{ url_for('auth.signup') }}">Sign Up</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4"
href="{{ url_for('auth.login') }}">Login</a></li>
{% endif %}
잘 동작하는 것을 확인하실 수 있을 것입니다. 로그아웃 전 후로 네비게이션 바의 내용이 동적으로 바뀌는 것을 처리했습니다!