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)

태그:

카테고리:

업데이트: