로그인 / 회원가입 과정
지난 포스팅 에서 다뤘던 template 코드를 다시 한 번 살펴보겠습니다.
보시면 form의 method를 설정 할 수 있습니다. POST 방식으로 되어있는지 확인한 후
auth.py 파일에서 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")
login과 signup 부분을 보시면 method = ['GET', 'POST'] 로 수정해줌으로써 POST요청을 처리하도록 합니다.
그 다음 웹 브라우저에서 회원가입 /blog/sign-up 창 에서 제출 버튼을 누르면
위와 같이 응답 상태 코드가 200으로 뜨는데
해당 코드 200은 서버가 요청을 제대로 처리 했다는 뜻 입니다.
이제 서버가 제대로 응답하는 것을 확인 했으니
임의의 값을 작성하여 전송을 한 후에도 정상적으로 데이터를 받아올 수 있는지 시험해보겠습니다.
그 전에 폼 코드의 일부를 살펴보겠습니다.
폼이 제출된 후 서버에서 데이터를 참조하기 위해 input 태그의 name 속성을 사용할 겁니다.
from flask import request # 추가
@auth.route("/sign-up", methods=['GET', 'POST'])
def signup():
email = request.form.get('email')
print(email)
username = request.form.get('username')
print(username)
password1 = request.form.get('password1')
print(password1)
password2 = request.form.get('password2')
print(password2)
return render_template("signup.html")
auth.py 코드를 위와 같이 추가해줍니다.
get() 메서드의 인자로 name 속성을 이용하여 전송한 데이터를 가져올 수 있도록 해줄겁니다.
이제 임의의 값을 입력한 후에 제출버튼을 누르면
아래와 같이 정상적으로 데이터를 받아온 걸 확인 할 수 있습니다.
작업을 하기에 앞서 저번 포스팅에서 다뤘던 내용을 살펴보겠습니다.
저번 포스팅 내용을 보면 sqlite3 모듈을 통해 데이터베이스에 값을 넣는 등
쿼리를 직접 날려 데이터베이스와 소통하였습니다.
파이썬을 설치했다면 사용할 수 있는 모듈로 별도의 데이터베이스전용 프로그램이 없이 사용할 수 있었습니다.
하지만 이번에는 파이썬에서 사용할 수 있는 라이브러리 중 하나로 ORM의 일부인 SQLAlchemy을 이용해볼 겁니다.
ORM이란 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 것을 말합니다.
자세하게는, 객체지향 프로그래밍에서 쓰이는 객체 라는 개념을 구현한 클래스와
관계형 데이터베이스에서 쓰이는 데이터인 테이블을 자동으로 매핑하는 것을 말합니다.
덕분에 SQL 쿼리(query)라는 구조화된 질의를 작성하고 실행하는 등의 복잡한 과정 없이
파이썬 클래스로 데이터베이스를 다룸으로써
파이썬 문법만으로도 데이터베이스를 다룰 수 있게 할 수 있습니다.
ORM과 SQLAlchemy에 대해 자세하게 이해하고 싶다면 저번 포스팅을 참고해주세요.
SQLAlchemy DB 처리
현재 데이터베이스가 존재하지 않기 때문에 데이터베이스를 생성해주는 코드가 필요합니다.
__init__.py 파일에서 아래와 같이 코드를 추가해줍니다.
# DB 추가
def create_database(app):
if not path.exists("blog/" + DB_NAME): # DB 경로가 존재하지 않는다면,
db.create_all(app=app) # DB를 하나 만들어낸다.
그 다음 DB에 대한 설정을 추가해줍니다.
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="/")
from .auth import auth
# blueprint 등록, '/auth' 를 기본으로 한다.
app.register_blueprint(auth, url_prefix="/blog")
return app
이제 아래와 같이 데이터베이스가 생성되는 것을 확인할 수 있습니다.
그렇지만 아직 테이블은 만들어지지 않았기때문에
이제 테이블 만드는 작업을 해보겠습니다.
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()) # 생성일자, 기본적으로 현재가 저장되도록
데이터베이스 모델에 관한 것을 정의했고
이제 데이터 베이스가 만들어지기 전에 작성했던 유저 모델을 등록해야 합니다.
만약 그렇지 않으면 데이터베이스에 데이블이 생성되지 않습니다.
아래와 같이 데이터베이스를 생성하는 함수를 호출하기 전에 유저 모델을 등록해줍니다.
이제 테이블도 생성되는 것을 확인할 수 있습니다.
로그인 준비
Flask-Login
Flask 프레임워크를 위한 사용자 세션관리 기능을 제공하는 라이브러리로
사용자 정보를 세션에 저장할 수 있습니다.
이 라이브러리를 이용하여 로그인 기능을 구현하겠습니다.
우선 models.py 부분을 먼저 살펴보겠습니다.
from . import db
from flask_login import UserMixin
from sqlalchemy.sql import func
class User(db.Model, UserMixin):
User 클래스는 db.Model과 UserMixin를 다중상속하는 것을 볼 수 있습니다.
근데 여기서 UserMixin에서는 무엇을 제공해주는 것이고 무엇때문에 상속을 받은 것인지 살펴볼 필요가 있습니다.
class UserMixin:
"""
This provides default implementations for the methods that Flask-Login
expects user objects to have.
"""
# Python 3 implicitly set __hash__ to None if we override __eq__
# We set it back to its default implementation
__hash__ = object.__hash__
@property
def is_active(self):
return True
@property
def is_authenticated(self):
return self.is_active
@property
def is_anonymous(self):
return False
def get_id(self):
try:
return str(self.id)
except AttributeError:
raise NotImplementedError("No `id` attribute - override `get_id`") from None
UserMixin에 대한 클래스 부분 입니다. 설명 부분을 보시면 플라스크 로그인에서 수행하는 메소드에 대한 기본 구현을 제공한다고 적혀 있습니다.
이제 __init__.py 파일에 아래와 같이 추가해줍니다.
from flask_login import LoginManager
# DB 생성하는 함수 호출하기
from .models import User
create_database(app)
login_manager = LoginManager() # LoginManager() 객체를 만들어 준다.
login_manager.login_view = "auth.login" # 만약 로그인이 필요한 곳에 로그인하지 않은 유저가 접근할 경우, 로그인 페이지로 리다이렉트 되도록 해준다.
# 받은 id로부터, DB에 있는 유저 테이블의 정보에 접근하도록 해 줌.
login_manager.init_app(app)
# login manager는, 유저 테이블의 정보에 접근해, 저장된 세션을 바탕으로 로그인되어 있다면 로그인 페이지로 안 가도 되게끔 해 줌.
@login_manager.user_loader
def load_user_by_id(id):
return User.query.get(int(id))
이제 로그인이 필수적으로 필요한 곳에서 로그인을 하지 않은 사용자가 접근할 경우
로그인 페이지로 리다이렉트되도록 설정하였습니다.
회원가입 처리
사용자가 회원가입을 한다고 가정해봅시다.
이미 회원가입을 진행했던 사용자의 이메일, 이름과 중복되는지 체크해야 할 것이고,
간단한 비밀번호로 회원가입을 하지 않도록 보안에 신경 써야 할 것입니다.
사용자는 /blog/signup 에 접근해서 폼을 작성할 것이므로
폼에서 받아온 테이터가 조건에 맞는지 체크해보겠습니다.
우선 두 라이브러리를 터미널을 통해 설치해주겠습니다.
flask의 form을 관리할수 있는 기능을 제공해주는 라이브러리
> 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.blog_home")) # 저장이 완료된 후 home으로 리다이렉트
# GET요청을 보낸다면 회원가입 템플릿을 보여줌
return render_template("signup.html", form=form)
이미 회원가입을 진행했던 사용자의 데이터를 받아와서
회원가입을 진행하고 있는 사용자의 이메일, 이름이 중복되지 않는다면 새로운 사용자로 저장되도록,
만약 중복이 된다면 중복되는 부분에 대한 메시지를 띄우도록 했습니다.
여기서 보안성을 더 생각해보겠습니다.
데이터베이스에 누군가가 접근한다고 가정했을 때
그 누군가는 사용자의 모든 정보를 알 수 있기때문에 다른 사용자의 데이터로 로그인 할 수 있을 것 입니다.
여기서 만약 비밀번호를 hashing을 한다면 비밀번호가 해석 할 수 없는 형태로 변형이 됩니다.
즉, hashing은 원하는 데이터를 암호화하여 데이터베이스에 저장하는 기능을 수행합니다.
비밀번호 해싱
우선 auth.py 파일에 아래와 같이 입력해줍니다.
from werkzeug.security import generate_password_hash, check_password_hash
werkzeug.security 는 보안 기능을 제공합니다.
그 다음 폼에서 받아온 데이터 (비밀번호) 를 generate_password_hash() 메서드를 이용하여 해싱해줍니다.
@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),
)
generate_password_hash() 메서드는 특정 문자열에 salt를 첨가한 후, 해싱하는 것을 말합니다.
여기서 salt란 소금을 생각하시면 될 것 같습니다.
식재료에 소금을 골고루 뿌리듯이 식재료는 사용자가 입력한 데이터 그대로를 뜻하고, 소금은 임의의 데이터를 뜻합니다.
즉, 원본 데이터의 앞 혹은 뒤에 임의의 데이터를 키워놓은 상태를 말합니다.
{{ form.csrf_token }}
이제 폼 코드 아래 부분에 {{ form.csrf_token }} 을 입력해주면 폼이 정상 작동하게 됩니다.
이 부분은 회원가입 뿐만 아니라 다른 탬플릿 코드에도 적용해야 합니다.
이제 데이터가 잘 처리가 되는지 폼을 작성해줍니다.
제출 버튼을 누르고 나니 signup 페이지가 아닌 home 페이지로 리다이렉트 되는 것을 보아하니
데이터가 정상적으로 처리 된 것 같습니다.
이제 데이터베이스로 확인을 해보겠습니다
아이디, 이메일, 이름, 그리고 비밀번호 해싱처리가 되어있는 것까지 성공적으로 데이터를 저장한 걸 확인 할 수 있습니다.
로그인 처리
forms.py 파일에 아래와 같이 코드를 추가해줍니다.
class LoginForm(FlaskForm):
email = EmailField('email', validators=[DataRequired(), Email()])
password = PasswordField('password', validators=[DataRequired()])
또한 auth.py 파일에도 아래와 같이 코드를 추가해줍니다.
from flask_login import login_user
@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.blog_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 기능을 이용해서 로그아웃은 금방 처리 할 수 있습니다.
from flask_login import logout_user, login_required
@auth.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("views.blog_home"))
@login_required 부분에 대해 살펴보겠습니다.
def login_required(func):
"""
If you decorate a view with this, it will ensure that the current user is
logged in and authenticated before calling the actual view. (If they are
not, it calls the :attr:`LoginManager.unauthorized` callback.) For
example::
@app.route('/post')
@login_required
def post():
pass
If there are only certain times you need to require that your user is
logged in, you can do so with::
사용자에게 요구해야 하는 경우 로그인한 경우 다음을 사용할 수 있다고 나와있습니다.
즉, 사용자 로그인이 되어있는 상태여야 로그아웃을 처리할 수 있도록 도와주는 것 입니다.
이제 회원가입, 로그인, 로그아웃에 대해 처리가 모두 가능해졌습니다.
근데 여기서 아쉬운 점은 사용자가 회원가입을 한다고 가정했을 때
home으로 리다이렉트되는 행위가 사용자 입장에선
정상적으로 회원가입이 이루어졌다고 받아들이기 어려울 것 입니다.
정상적으로 회원가입, 로그인, 로그아웃이 이루어졌는지, 이루어지지 않았는지
오류 메시지와 동적으로 변화하는 navbar가 있다면 사용자가 수월하게 알 수 있을 것 입니다.
오류 메시지
저번 포스팅에서 base.html을 만들고
그것을 가지고 모든 템플릿 들을 관리하도록 했었습니다.
base.html의 <nav> 태크 아래에 아래와같은 코드를 입력해줍니다.
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
그럼 아래와 같이 오류 메시지들이 뜨는 것을 확인 할 수 있습니다.
위에 코드를 자세히 살펴보면 category를 설정해여 category로 메시지를 분류한 것을 알 수 있습니다.
정상적으로 메시지가 뜨는 것을 확인했으니 보기 좋게 디자인을 바꿔보겠습니다.
아래와 같이 코드를 수정해줍니다.
{% 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 %}
보기 좋게 메시지가 뜨는 것을 확인 할 수 있습니다.
동적변화하는 navbar
로그인 했을 경우 "Welcome. 사용자이름" 와 같은 메시지가 뜬다면 로그인이 되어있는지 쉽게 구별이 가능합니다.
그렇다면 "사용자 이름" 을 표시하기 위해서는 사용자가 로그인했을 때에 사용자에 대한 정보를 템플릿에 넘겨줘야합니다.
auth.py 파일에서 아래와 같이 코드를 수정해줍니다.
return render_template("login.html", form=form, user=current_user)
네비게이션 바는 base.html에 포함되므로 base.html이 있는 모든 템플릿에 user가 변수로서 전달되어야 합니다.
그러므로 views.py 와 auth.py 파일의 코드를 아래와 같이 수정해줍니다.
views.py
from flask import Blueprint, render_template
from flask_login import current_user
views = Blueprint("views", __name__)
@views.route("/")
@views.route("/home")
def blog_home():
return render_template("index.html",user=current_user)
@views.route("/about")
def about_me():
return render_template("about.html",user=current_user)
@views.route("/contact")
def contact():
return render_template("contact.html",user=current_user)
@views.route("/categories-list")
def categories_list():
return render_template("list.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'])
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.blog_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.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('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.blog_home")) # 저장이 완료된 후 home으로 리다이렉트
# GET요청을 보낸다면 회원가입 템플릿을 보여줌
return render_template("signup.html", form=form, user=current_user)
마지막으로 base.html에서 아래와 같이 코드를 수정해줍니다.
{% if user.is_authenticated %}
{# 유저가 로그인했다면 보일 것들 : "Welcome, username", "Logout" 이고, user 정보가 넘어가야 함. #}
<ul>
<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>
</ul>
{% endif %}
{% if not user.is_authenticated %} {# 그리고, 로그인한 상태가 아니라면... #}
<ul>
<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>
</ul>
{% endif %}
이제 로그아웃 전 후로 네이게이션 바의 내용이 동적으로 바뀌는 것을 확인 할 수 있습니다.
여기까지 회원가입/로그인/로그아웃 처리까지 다뤄봤습니다!
'flask' 카테고리의 다른 글
CORS란? 그리고 해결방법은? / CSR vs SSR (0) | 2022.11.02 |
---|---|
Flask - 테스트 코드 / 관리자 페이지 / 카테고리, 게시물 (0) | 2022.08.10 |
Flask - 라이브러리 설치부터 정적 파일 다루기까지 ( Blueprint / jinja template engine / render_template() ) (0) | 2022.07.11 |
Flask - python으로 쉽게 데이터베이스 다루기 (0) | 2022.07.07 |
Flask - HTTP Methods / URL Building / 데코레이터 / 변수 규칙 / 후행 슬래시에 관해 (0) | 2022.07.06 |