Notice
Recent Posts
Recent Comments
05-21 07:17
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

Byeol Lo

Flask - 5. Tutorial Blueprint and View 본문

BackEnd/Flask

Flask - 5. Tutorial Blueprint and View

알 수 없는 사용자 2024. 2. 14. 14:52

 view 함수들은 전부 어플리케이션에 대한. 요청에 응답하기 위한 코드인데, Flask는 패턴을 사용해 들어오는 요청 URL을 처리해야하는 뷰와 일치시킨다. 

 

Create a Blueprint

 블루프린트는 뷰 또는 관련된 코드 그룹을 구성하는 방법인데, 뷰와 그런 코드들을 애플리케이션에 직접 등록하는 대신 블루프린트에 등록한다. 그런 다음 팩토리 기능에서 사용할 수 있게 되면 청사진이 애플리케이션에 등록되게 된다. 여기에는 두 개의 청사진이 있는데, 하나는 인증 기능용이고 다른 하나는 블로그 게시물 기능용이다. 각 청사진의 코드는 별도의 모듈에 들어가게 된다.

# flaskr/auth.py

from flask import (
    Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash

from flaskr.db import get_db

bp = Blueprint('auth', __name__, url_prefix='/auth')

 예시를 보자.

@bp.route('/login', methods=['GET', 'POST'])
def login():
    # 로그인 로직
    pass

@bp.route('/register', methods=['GET', 'POST'])
def register():
    # 회원가입 로직
    pass

위는 bp를 사용하여 login과 register에 대한 view를 해주는 함수이다. 여기서 annotation에 bp.route를 통해 bp가 가지고 있는 url_prefix가 prefix로 사용되어 각 endpoint에 접두어로 들어가게 된다. 이렇게 된다면 서로 다른 Blueprint 간의 URL 충돌을 방지하고, URL 구조를 조직화할 수 있으며, 굳이 url을 다 입력하지 않아도 간편하게 이용 가능하게 된다.

 여기서 이런 bp를 그냥 사용할 수는 없고 자신의 application에 청사진을 등록해야 한다. 다음 코드를 보자.

# flaskr/__init__.py

def create_app():
    app = ...
    # existing code omitted
    
    from . impport auth
    app.register_blueprint(auth.bp)
    
    return app

인증 청사진에는 신규 사용자를 등록하고 로그인 및 로그아웃하는 뷰가 있어야 할 것이다. 이제 그걸 만들어주면 된다.

 

The First View: Register View

 /auth/register URL을 방문할 때, register view는 사용자에게 등록하기 위한 어떤 HTML 양식을 리턴할 것이고, 그걸 받은 사용자는 HTML 양식에 맞춰 채워주고 우리에게 request를 요청할 것이다. 이 형식을 제출했을때, input 값이 검증된 값인지, 에러가 없는지 등을 검사하고, register가 성공적으로 되었다면 login page로 redirect를 해주면 될 것이다.

# flaskr/auth.py

@bp.route('/register', methods=('GET', 'POST'))
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        error = None
        
        if not username:
            error = 'Username is required'
        elif not password:
            error = 'Password is required'
        
        if error is None:
            try:
                db.execute(
                    "INSERT INTO user (username, password) VALUES (?, ?)",
                    (username, generate_password_hash(password)),
                )
                db.commit()
            except db.IntegrityError:
                error = f"User {username} is already registered."
            else:
                return redirect(url_for("auth.login"))
        
        flash(error)
    return render_template('auth/register.html')

 뷰를 작성할 때는 전에 배웠던 것들이 다양하게 사용되는데, 아직 익숙하지 않다면 보면서 따라하다가 이 코드들의 하나하나 의미를 파악하고, 그 다음에 두 번, 세 번 반복하는 것이 좋다. POST와 GET 방식을 알아보자.

방식 설명
GET  GET 메서드는 데이터를 URL의 쿼리 문자열(query string)에 포함하여 전송하기에 중간에 가로채기만 한다면 보안적으로 문제가 될 수 있다.

 GET 메서드는 URL에 데이터를 포함하기 때문에 전송할 수 있는 데이터에 제한이 있다. 대부분의 브라우저는 URL의 최대 길이에 제한을 두기 때문.

 GET 요청은 캐싱될 수 있는데, 이 때문에 동일한 GET 요청을 여러 번 보내더라도 같은 결과를 받게 될 수 있다. 이는 웹 페이지의 로딩 속도를 향상시키는 데 유용하다.

 위에서 말한 것과 동일하지만, GET 요청은 데이터가 노출 되기 때문에 보안에 취약할 수 있다. 예를 들어, 사용자의 개인정보와 같은 것을 GET 방식으로 받게 되면 이는 매우 위험하다. 
POST  POST 메서드는 데이터를 HTTP 요청의 본문(body)에 포함하여 전송한다. 따라서 전송되는 데이터는 URL에 노출이 되지 않는다.

 POST 메서드는 HTTP 요청의 본문에 데이터를 포함하기 때문에 데이터 전송량에 대한 제한이 없다. 따라서 대량의 텍스트 데이터를 보내기에 적절하다.

 일반적으로 캐싱이 되지 않기 때문에 동일한 POST 요청을 여러 번 보내면 매번 새로운 요청이 서버에 전송이 되게 된다.

 POST 요청은 데이터가 HTTP 요청의 본문에 포함되기 때문에 GET 보다 보안적으로 안전하다.

 따라서 POST로 받았다면 그것은 client가 서버에게 자신의 register의 양식을 채운 http 데이터를 보냈다는 것이다. 코드에서는 받은 request에서 username, password를 받아와 db에게 사용자 등록하는 sql 쿼리를 수행하게 하는 것이다. 이때 쿼리에 ?가 들어간게 생소할 수 있는데, 사용자 입력에 대한 입력 대체자인데, 그 뒤에 나오는 튜플의 데이터가 들어가게 된다. 이렇게 동작하는 이유는 사용자 입력을 그냥 바로 SQL 쿼리에 넣게 되면 SQL Injection 공격이 발생할 수 있기 때문에 이를 방지하도록 하기 위해 입력이 쿼리로 직접 삽입되는 것을 막고, 데이터베이스에 대한 안전한 쿼리를 실행하도록 할 수 있다.

 또한 보안상의 문제로 generate_password_hash를 사용하여 데이터베이스에 날 것의 password를 그대로 저장하는 것보다는 hash 처리를 하여 저장되게 된다. 그런 후에 데이터베이스의 데이터들이 수정된 것에 대한 변경 사항 저장을 위해 db.commit()을 해주면 끝이다. 여기서 저장할 때 사용자 이름이 이미 존재하는 경우 sqlite3.IntegrityError가 발생하게 되고 이는 오류 형태로 출력되게 된다.

 만약 예외절이 실행되지 않았다면 login 페이지로 redirect가 될 것이고, 그렇지 않다면 클라이언트에게 flash()를 통해 error 메시지 창을 띄우게 된다. 그리고 실패를 했기 때문에 다시 /auth/register을 띄우게 된다."

 

The View: Login View

 이제 로그인을 할 수 있는 창을 만들자.

@bp.route('login', method=('GET', 'POST'))
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        error = None
        
        user = db.execute(
            'SELECT * FROM user WHERE username = ?',
            (username,) # 튜플은 ,로 해줘야 함
        ).fetchone()
        
        if user is None:
            error = 'Incorrect username.'
        elif not check_password_hash(user['password'], password):
            error = 'Incorrect password.'
            
        if error is None:
            session.clear()
            session['user_id'] = user['id']
            return redirect(url_for('index'))
        
        flash(error)
    
    return render_template('auth/login.html')

 fetchone()을 통해 결과 set에서 맨 첫번째 하나의 행을 가져오는 것이고, 없다면 None을 반환하게 된다. 이 외에도 fetchall(), fetchmany(size) 등이 있다. 그 후에 user가 none이 아니면 제대로 등록이 되어 있는 것이며, password 까지도 맞다면 session(서버 임시저장소)을 clear하고, session dict에 user_id 를 저장시킨다. 그런 후에 원래 있던 곳으로 redirect를 시켜주면 될 것이다. 만약에 그렇지 않다면 error를 출력하고 다시 login 창으로 보낸다.

 이제 사용자 정보가 session에 저장되었기 때문에 이 user 정보를 간단하게 이용할 수 있도록 할 것이다. 매번 bp를 통해 session에서 user_id를 가져와서 검증을 하는 것은 번거롭고 코드 중복이 일어난다. 따라서 전역 객체인 g에 저장하여 알맞게 사용할 수 있도록 한다. 또한 이렇게 한다면 bp가 실행되기 전에 실행되는 함수를 넣어서 요청하는 사용자의 정보도 쉽게 알 수 있고, 다른 함수나 라우트에서도 쉽게 사용이 가능하다.

# flaskr/auth.py

@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 = get_db().execute(
            'SELECT * FROM user WHERE id = ?', (user_id,)
        ).fetchone()

 

The View: Logout View

 로그인을 했다면 로그아웃도 가능해야한다. session에서 사용자 ID를 제거만 하면 끝이다.

# flaskr/auth.py

@bp.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('index'))

 

Required Authentication in Other Views

 블로그 게시물을 생성, 편집 및 삭제를 하려면 사용자가 로그인을 해야 할 것이다. 데코레이터(자바에선 어노테이션) 을 사용하여 적용되는 각 보기에 대해 이를 확인할 수 있다.

# flaskr/auth.py

def login_required(view):
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            return reidrect(url_for('auth.login'))
        
        return view(**kwargs)
    
    return wrapped_view

 다음과 같이 데코레이터를 사용하여 view 함수를 wrapped_view로 감싸주어서 wrapped_view에서는 g.user를 통해 로그인이 되어 있는지를 검사하도록 할 수 있다. 이렇게 된다면 굳이 login을 필요로 하는 view function에서의 중복된 코드를 피하고 가독성을 높일 수 있다.

Endpoints and URLs
 url_for() 함수는 이름과 인수를 기반으로 뷰에 대한 URL을 생성한다. 뷰와 연관된 이름은 엔드포인트라고도 하며 기본적으로 뷰 함수의 이름과 동일하게 된다. 예를 들어서 hello 튜토리얼에서는 hello() 함수는 hello로 연결되고, url_for('hello')를 통해 URL을 자동으로 생성시켜주게 된다. Blueprint를 사용한 경우는 조금 다른데, url_for('auth.login')을 통해 접근가능하다. blueprint의 이름을 prefix를 구분하는 것이다.

 

이제 실행을 시켜 "/auth/logout", "/auth/login", "/auth/register" 에 다음이 나오면 성공이다.

 

'BackEnd > Flask' 카테고리의 다른 글

Flask - 7. Tutorial Blog Blueprint  (0) 2024.02.15
Flask - 6. Tutorial Templates  (0) 2024.02.14
Flask - 4. Define and Access the Database  (1) 2024.02.12
Flask - 3. Tutorial Application Setup  (0) 2024.02.12
Flask - 2. Tutorial Project Layout  (0) 2024.02.08
Comments