[H-UP] flask_login 을 활용한 로그인, 로그아웃 구현
H-UP
- model
- view -> 다음 포스트
- controller
Model
MVC 구조에서의 Model의 역할을 되새겨보면 애플리케이션의 정보를 담고 있는 파일로, 주로 데이터의 속성을 정의하거나 데이터 처리 등 데이터베이스를 관리하는 파일이다.
login/model/mysql.py
데이터베이스 연결을 오랜만에 하다보니 db 이름을 적어야할 곳에 connection name 을 적는 실수를 저질렀다. 역시 익숙하지 않은 개발은 매일 상기해주어야 손에 붙는다..
데이터베이스를 연결하기 전, 아이디와 패스워드만을 가진 데이터베이스를 간단하게 설계했다. 실제 구현하고자 하는 서비스에서는 그룹도 생성해야하고, 보다 다양한 정보를 받게 될 것이니 훨씬 복잡해질 예정이다. 복잡한 데이터베이스를 구성해본 적이 없어서 ERD 설계부터 애 먹는 중이다..
CREATE TABLE user_info (
user_id varchar(10) not null,
user_pw varchar(16) not null
);
login/model/mysql.py 에서는 데이터베이스 연결 작업과, 연결이 끊길 시 재연결 해줄 수 있는 코드를 작성하였다.
# flask_login에서 제공하는 사용자 클래스 객체
import pymysql
HOST = 'localhost'
connect = pymysql.connect(
host = HOST,
port = 3306,
user = 'root',
db = 'user_login',
passwd = '',
charset = 'utf8'
)
# db 연결이 끊겼을 때 재연결
def conn_mysql():
if not connect.open:
connect.ping(reconnect = True)
return connect
View
사실 MVC 구조로 코드를 작성하는 것이 생각보다 강의 뒷단에 나와서 2주 후 쯤 들으려 했던 강의를 미리 청강했다. 그러나 아직 한계는 Model과 Controller 까지인 걸로 ,,,, 현재 app 에 작성해둔 여러 라우팅 경로들을 view에 나누어 작성하는 것 같은데 정확히 어떻게 view를 사용하는 건 지 잘 모르겠어서 이번 주에는 구현을 포기했다.
강의 청강만으로는 부족할 것 같고, 유튜브 강의나 다른 MVC 구조를 다루는 강의를 찾아 코드를 열심히 살펴보고 구조를 이해해봐야겠다고 느꼈다.
Controller
작성해둔 model 파일에서 함수를 import 했다. UserMixin이 무엇인 지 찾아보았으나 거의 다 ‘필요하다’ 정도만 알려주고 넘어간다. 일단 get_id, is_authenticated, is_active와 같은 함수들을 작성하지 않고도 사용할 수 있도록 해주는 것 같다.
공식 문서에는 다음과 같이 적혀있고,
To make implementing a user class easier, you can inherit from UserMixin, which provides default implementations for all of these properties and methods. (It’s not required, though.)
이를 해석하면 (파파고 사용)
사용자 클래스를 더 쉽게 구현하기 위해 이러한 모든 속성과 메서드에 대한 기본 구현을 제공하는 UserMixin에서 상속할 수 있습니다.
가 되는 것으로 미루어보아 대충 맞게 해석한 것 같다
생성자, get_id
def __init__(self, user_id, user_pw):
self.id = user_id
self.pw = user_pw
def get_id(self):
return str(self.id)
User 클래스의 생성 부분은 강의 코드를 거의 그대로 쳤다. 블로그를 작성하다보니 이미 UserMixin을 사용한 이상 get_id 함수는 필요 없는 것이 아닌가 ? 라는 의문이 든다. 후에 삭제해보고 문제가 발생하는 지 테스트 해봐야겠다.
@staticmethod
staticmethod 는 클래스와 무관하게 독립적으로 사용되는 영역이다. 따라서 매개변수로 self 가 필요없다. 이와 반대로, 클래스 변수를 사용할 때 선언하는 @classmethod 가 존재한다.
아래 코드는 데이터베이스에서 user_id에 해당하는 유저를 불러와 User 객체로 만들어주는 역할을 한다.
@staticmethod
def get(user_id):
mysql_db = conn_mysql()
db_cursor = mysql_db.cursor()
sql = "select * from user_info where user_id = '" + str(user_id) + "'"
db_cursor.execute(sql)
user = db_cursor.fetchone()
if not user:
return None
user = User(user_id = user[0], user_pw = user[1])
return user
전체 코드 (login/control/user.py)
from flask_login import UserMixin
from model.mysql import conn_mysql
class User(UserMixin):
# User 객체에 저장할 사용자 정보
def __init__(self, user_id, user_pw):
self.id = user_id
self.pw = user_pw
def get_id(self):
return str(self.id)
@staticmethod
def get(user_id):
mysql_db = conn_mysql()
db_cursor = mysql_db.cursor()
sql = "select * from user_info where user_id = '" + str(user_id) + "'"
db_cursor.execute(sql)
user = db_cursor.fetchone()
if not user:
return None
user = User(user_id = user[0], user_pw = user[1])
return user
App & HTML
HTML (login/templates/login.html)
MVC 구조와 flask_login 을 사용한 로그인 기능의 테스트용이기에 굉장히 간단하게 작성했다. log 는 로그인 여부를 백엔드에서 판단하여 넘겨주는 변수이다. 이를 기준으로 로그인이 되어 있지 않으면, id, pw 의 입력창과 함께 login 버튼이 표시되고, 그렇지 않을 때에는 logout 기능을 하는 a태그가 나타나도록 했다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login</title>
</head>
<body>
{ % if not log: % }
<form action="/login" method="post">
<h2>LOGIN</h2>
<div class="int-area" id = 'id'><input type="text" placeholder="id" name="id" /></div>
<div class="int-area"><input type="password" placeholder="password" name="pw" /></div>
<div class="btn-area"><input type="submit" id="btn" value="login" /></div>
</form>
{ % else % }
<a href="/logout">Logout</a>
{ % endif % }
</section>
</body>
</html>
App (login/app.py)
view를 분리하지 않았기에 기능된 기능이 굉장히 간단함에도 길다. 분리해서 설명하는 것보다 주석처리해두는 것이 보기 편할 것 같아 필요한 부분에 모두 주석을 달아두었다.
아이디 / 비밀번호가 틀렸을 때의 동작은 제대로 동작하는 지만 확인해보기 위해 alert 를 사용하여 확인하도록 했다. 후에 서비스 개발이 시작되면 타 요소로 바뀔 지도 모르겠다.
CORS
Cross Origin Resource Sharing 으로, 서버가 다른 출처의 리소스를 요청할 수 있도록 하는 웹 서버 정책이다. 후에 카카오톡 로그인을 구현할 예정이기에 미리 설정해두었다.
OS
보안 강화를 위해 많이 사용되는데, 보안 또한 현재 프로젝트에서 중요시하는 부분이므로 미리 설정했다.
from flask import Flask, request, render_template, make_response, redirect, url_for
from flask_login import LoginManager, current_user, login_required, login_user, logout_user
from flask_cors import CORS
import os
from control.user import User
# from view import login_view
# https 만을 지원하는 기능읗 http에서 테스트할 때 필요한 설정
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
app = Flask(__name__, static_url_path='/static') # flask 객체 생성
CORS(app) # 외부 API 받을 수 있도록
app.secret_key = os.urandom(24) # 보안을 위해 서버가 생성될 때마다 시크릿키 새로 발급
login_manager = LoginManager() # 로그인 객체 생성
login_manager.init_app(app) # app에 login_manager 연결
login_manager.session_protection = 'strong' # session 보안성 강화
# User class 를 통해 사용자 정보를 가져옴
@login_manager.user_loader
def load_user(user_id):
return User.get(user_id)
# login_required 데코레이터로 로그인 후 접근 가능한 페이지 보호
@login_manager.unauthorized_handler
def unauthorized():
return '<script>alert("not found user");history.go(-1);</script>'
@app.route('/')
def main():
return render_template('login.html', log = current_user.is_authenticated)
@app.route('/login', methods = ['get','post'])
def login():
id = request.form.get('id')
pw = request.form.get('pw')
user = User.get(id)
if not user:
return '<script>alert("not found user");history.go(-1);</script>'
elif pw != user.pw:
return '<script>alert("passwork error");history.go(-1);</script>'
else:
login_user(user)
return redirect(url_for('main', log = current_user.is_authenticated))
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('main', log = current_user.is_authenticated))
if __name__ == '__main__':
app.run(host = '0.0.0.0', port = '8080', debug = True)