Skip to content

Latest commit

 

History

History
214 lines (147 loc) · 12.8 KB

File metadata and controls

214 lines (147 loc) · 12.8 KB

Blueprints and Views

Flask Framework 튜토리얼 프로젝트 블루프린트와 뷰 함수에 관련된 문서입니다. 자세한 사항은 여기를 참고하세요.

View 함수는 어플리케이션으로 들어오는 요청에 응답하기 위해 작성하는 코드입니다. Flask는 패턴을 이용해 요청 URL과 이 요청을 처리할 뷰를 매치합니다. 이 때, 뷰 함수는 (Flask에 의해) response로 변경할 데이터를 리턴합니다. 반대로, 뷰 함수의 이름과 인자를 바탕으로 이 뷰에 대응하는 URL을 생성할 수도 있습니다.

Create a Blueprint

Blueprint는 서로 관련있는 뷰끼리 묶어주는 기능입니다. 각각의 뷰를 어플리케이션에 직접 등록하는 대신 블루프린트에 등록하고, factory function을 통해 블루프린트를 어플리케이션에 등록합니다. Flaskr 어플리케이션에서는 두 개의 블루프린트를 사용할 것입니다. 하나는 유저 인증 관련 기능을 위한 것이고, 다른 하나는 블로그 포스트 관련 기능을 위한 것입니다. 각 블루프린트를 위한 코드는 모듈을 분리하여 따로 작성합니다. 사용자 인증 관련 기능부터 작성해 보겠습니다.

flaskr/auth.py

from flask import Blueprint

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

auth라는 이름의 블루프린트를 생성하는 코드입니다. 앱 오브젝트와 마찬가지로, 블루프린트도 자신이 어디에서 정의되었는지 알아야 합니다. 따라서 현재 위치를 나타내는 __name__ 이 두 번째 인자로 전달됩니다. 세 번째 인자인 url_prefixauth 블루프린트와 관련된 모든 URL의 앞부분에 추가됩니다. 다시 말해, 위 코드에 따르면 auth와 관련된 모든 URL이 /auth 로 시작하게 됩니다.

이제 app.register_blueprint()를 통해 이 블루프린트를 앱에 등록해보겠습니다. factory function에 아래 코드를 추가합니다.

flaskr/__init__.py

def create_app():
    app = ...
    # existing code omitted

    from . import auth
    app.register_blueprint(auth.bp)

    return app

우리는 auth 블루프린트에 회원 가입, 로그인, 로그아웃을 위한 뷰를 추가할 것입니다.

The First View: Register

유저가 /auth/register URL을 방문하면, register 뷰는 HTML로 회원가입 양식을 반환합니다. 유저가 폼을 제출하면, register 뷰는 유저가 입력한 내용을 검증합니다. 입력이 유효하지 않으면 에러 메시지와 함께 회원가입 양식을 다시 보여주고, 모든 입력이 유효한 경우 새로운 유저를 생성하고 로그인 페이지로 이동합니다.

먼저 뷰를 작성해 봅시다. (HTML 폼을 생성하는 템플릿은 다음 장에서 작성하겠습니다.)

flaskr/auth.py

from flask import Blueprint, flash, redirect, render_template, request, url_for
from werkzeug.security import check_password_hash, generate_password_hash

from flaskr.db import get_db

...
@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.'
        elif db.execute(
            'SELECT id FROM user WHERE username = ?', (username,)
        ).fetchone() is not None:
            error = 'User {} is already registered.'.format(username)

        if error is None:
            db.execute(
                'INSERT INTO user (username, password) VALUES (?, ?)',
                (username, generate_password_hash(password))
            )
            db.commit()
            return redirect(url_for('auth.login'))

        flash(error)

    return render_template('auth/register.html')

register 뷰가 하는 일을 살펴보겠습니다.

  1. @bp.route/register URL을 register 뷰 함수에 연결해줍니다. Flask는 /auth/register로 요청을 받으면, register 뷰를 호출하고 그 리턴값을 response로 사용합니다.

  2. 유저가 폼을 제출하면, request.method'POST'가 되고, 입력값 검증을 시작합니다.

  3. request.form은 제출된 양식을 key-value 형식으로 저장하는 특별한 dict 타입입니다. 유저는 usernamepassword를 form에 입력합니다.

  4. usernamepassword가 비어있지 않은지 검증합니다.

  5. 데이터베이스에 쿼리를 보내 username이 중복되지 않는지 체크합니다. db.execute는 유저의 입력을 전달받기 위해 ? placeholder를 포함하는 SQL 쿼리와, placeholder에 대입할 값의 tuple을 인자로 받습니다. 이 값을 쿼리 문자열에 직접 concat하지 않고, placeholder를 이용해 파라미터화된 쿼리를 사용하는 이유는 SQL injection 공격을 방지하기 위해서입니다.

fetchone()은 쿼리로 얻은 결과 중 한 줄을 리턴합니다. 만약 쿼리 결과값이 아무것도 없다면 None을 리턴합니다. fetchall()은 쿼리 결과값 전부를 리스트로 반환합니다.

  1. 입력값이 모두 유효하면, 새로운 유저 정보를 DB에 저장합니다. 보안상 절대로 패스워드를 직접 DB에 저장해서는 안됩니다. 대신 generate_password_hash()를 이용해 패스워드를 안전하게 해시 처리한 후 저장합니다. 이 때 데이터가 수정되기 때문에, db.commit() 을 호출해 변경된 내용을 커밋해주어야 합니다.

  2. 로그인 페이지로 redirect합니다. url_for()login 뷰를 위한 URL을 (login 뷰의 이름을 기준으로) 생성합니다. 만약 URL을 직접 하드코딩하게 되면, URL을 변경할 때 관련 코드를 일일히 수정해주어야 하는 번거로움이 있고, 이 과정에서 실수가 생길 수도 있습니다. 이런 위험을 막기 위해 url_for를 사용하는 것을 권장합니다. redirect()는 생성된 URL로 redirect를 걸어주는 역할을 합니다.

  3. 검증이 실패하면, 유저에게 에러 메세지를 띄웁니다. flash()는 템플릿을 다시 render할 때 사용할 수 있도록 메세지를 저장해줍니다.

  4. 사용자가 직접 auth/register로 접근하거나 입력값 검증 에러가 발생할 경우, 회원가입 양식 화면이 표시됩니다. render_template()이 HTML 템플릿을 뿌려줍니다.

Login

이 뷰는 위의 "register" 뷰의 패턴과 동일합니다.

flaskr/auth.py

# 위와 동일

@bp.route('/login', methods=('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')

"register" 뷰와는 몇 가지 차이점이 있습니다.

  1. user를 검색하는 쿼리를 먼저하고 나중에 사용하기 위해 변수에 저장됩니다.
  2. check_password_hash()는 제출 된 비밀번호를 저장할 때 쓴 generate_password_hash와 동일한 방식으로, 해시하여 안전하게 비교합니다. 일치하면, 암호가 유효하다고 판단합니다.
  3. session은 "requests" 전반에 걸쳐 저장된, 사전 형태의 데이터입니다. 비밀번호 검증 이후, 사용자의 ID를 새 센션에 저장합니다. 데이터는 브라우저에 전송되는 쿠키에 저장되고 브라우저는 후속 요청과 함께 데이터를 다시 보냅니다. Flask는 데이터를 변조할 수 없도록 안전하게 데이터를 보장("Sing = 서명" 의역)합니다.

이제 session 안에 저장된 user의 id가 저장되어 있고, 이것은 후속 요청에서 사용될 것입니다. 각 요청 시작점에서, 로그인된 유저라면, 그들 정보를 불러올 수 있고 다른 뷰에서 그것을 사용할 수 있습니다. 세션 안에 유저를 확인하기 위해 다음 코드를 작성해주세요.

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()

"bp.before_app_request()"는 요청 된 URL에 관계 없이 모든 뷰 함수가 실행되기 이전에 호출되는 함수를 등록합니다. "load_logged_in_user"는 세션에 저장된 user_id를 통해서 유저를 찾고, 그 정보를 데이터베이스에서 읽어옵니다. 그 후, 요청이 끝날 때까지 지속되는 "g.user"에 그 유저 정보를 저장합니다. 만약 user_id가 세션에 없거나, user_id에 해당하는 유저가 없다면, "g.user"는 None이 될 것입니다.

Logout

로그아웃을 하기 위해서는 세션에 저장된 user_id를 지ㅝ야 합니다. 그러면, load_logged_in_user는 후속 요청에 대해서, 유저를 불러오지 못할 것입니다. 로그아웃 함수를 작성해보도록 하겠습니다. 다음 코드를 입력해주세요.

flaskr/auth.py

# 이전과 동일

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

위 코드는 사용자가 로그아웃 시, 세션에 저장된 모든 데이터를 제거하고, "/index" 경로로 리다이렉트시킵니다.

Require Authentication in Other Views

블로그 포스트들은, 인증된 유저에 한해서, 만들고, 수정되고, 삭제되어야만 합니다. "decorator"를 사용하여 각 뷰에서 이것이 적용되는지 확인할 수 있습니다.

flaskr/auth.py

# 이전과 동일

def login_required(view):
    
    # 뷰를 래핑함. g.user가 없다면, 로그인 뷰로, 아니라면 해당 뷰로 이동
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            return redirect(url_for('auth.login'))

        return view(kwargs)
    
    return wrapped_view

이 데코레이터는 적용된 원래 뷰를 래핑하는 새로운 뷰 함수를 반환합니다. 위 함수는 사용자가 로드외었는지 확인하고, 그렇지 않으면 로그인 페이지로 리다이렉션하게 됩니다. 사용자가 로드 되면, 원래 뷰 함수가 호출되고, 정상적으로 동작합니다. "blog" 뷰를 작성할 때 이 데코레이터를 사용합니다.

Endpoints and URLs

url_for() 함수는 이름과 인수를 기반으로 뷰에 URL을 생성합니다. 뷰와 관련된 이름을 엔드 포인트라고도하며 기본적으로 뷰 함수의 이름과 동일합니다. 예를 들어, 이전 장에서 Application Factory에 추가 된 hello() 뷰의 이름은 'hello'이며, 이는 url_for( 'hello')와 연결될 수 있습니다. 나중에 다루겠지만 url_for( 'hello', who = 'World')를 형식을 사용하여 뷰 함수와 이름을 연결하는 방법도 있습니다.

블루 프린트를 사용할 때 블루 프린트의 이름은 함수 이름 앞에 추가됩니다. 위에서 작성한 로그인 함수의 엔드 포인트는 'auth', 블루 프린트에 추가했기 때문에 'auth.login'이 됩니다.

Templates에서 계속됩니다.