Python Flask 인증 파트 #01

안녕하십니까.! 잘 지내고 계시길 바랍니다. 지난 게시물에서 애플리케이션 프런트엔드 워크플로를 설정했으며 이번 게시물에서는 인증 페이지와 인증 유효성 검사를 설정했습니다.

이메일 유효성 검사의 경우 Flask-WTF는 email-validator에 의존하므로 추가 작업을 위해 이를 설치해야 하므로 pip로 설치할 수 있습니다.

pip install email-validator


로그인과 등록을 위한 두 가지 양식이 필요하므로 login_form.py에 두 개의 파일register_form.pyapplictaion/forms을 만들어야 합니다.

login_form.py

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField
from wtforms.validators import InputRequired, DataRequired


class LoginForm(FlaskForm):
    username = StringField(
        'username',
        validators=[InputRequired(), DataRequired()],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Username'
        }
    )
    password = PasswordField(
        'password',
        validators=[InputRequired(), DataRequired()],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Password'
        }
    )



register_form.py

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, EmailField
from wtforms.validators import InputRequired, DataRequired, Email, EqualTo, Length, Regexp
from application.validators.username_exists import UsernameExists
from application.validators.email_exists import EmailExists


class RegisterForm(FlaskForm):
    name = StringField(
        'name',
        validators=[InputRequired(), DataRequired()],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Name'
        }
    )
    username = StringField(
        'username',
        validators=[InputRequired(), DataRequired(), Regexp('^[a-zA-Z_0-9]\w+$', message="Only alphabets, numbers and _ are allowed."), UsernameExists()],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Username'
        }
    )
    email = EmailField(
        'email',
        validators=[InputRequired(), DataRequired(), Email(), EmailExists()],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Email'
        }
    )
    password = PasswordField(
        'password',
        validators=[InputRequired(), DataRequired(), Length(min=8), EqualTo('password_confirmation', message='Password should be match with confirm field.')],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Password'
        }
    )
    password_confirmation = PasswordField(
        'password_confirmation',
        validators=[InputRequired(), DataRequired()],
        render_kw={
            'class': 'input input-bordered w-full focus:outline-2 focus:outline-blue-700',
            'placeholder': 'Confirm Password'
        }
    )



사용자 등록을 위해 두 개의 WTForms 사용자 지정 유효성 검사기가 필요합니다. 하나는 username용이고 다른 하나는 email용으로 두 필드가 이미 DB에 있는지 확인합니다. validators에 폴더application를 만들고 validators 폴더에 username_exists.pyemail_exists.py 두 개의 파일을 만듭니다.

email_exists.py

from application.models.user import User
from wtforms.validators import ValidationError


class EmailExists:

    def __init__(self, model=User, exclude=None, message=None):
        self.model = model
        self.exclude = exclude
        if not message:
            message = "Email is already in use."
        self.message = message

    def __call__(self, form, field):
        user = self.model.query.filter_by(email=field.data)
        if not self.exclude:
            user.filter_by(id=self.exclude)
        if user.first():
            raise ValidationError(self.message)



username_exists.py

from application.models.user import User
from wtforms.validators import ValidationError


class UsernameExists:

    def __init__(self, model=User, exclude=None, message=None):
        self.model = model
        self.exclude = exclude
        if not message:
            message = "Username is already taken"
        self.message = message

    def __call__(self, form, field):
        user = self.model.query.filter_by(username=field.data)
        if not self.exclude:
            user.filter_by(id=self.exclude)
        if user.first():
            raise ValidationError(self.message)


auth.py에서 인증 컨트롤러에 대한 파일application/controllers을 생성합니다.

auth.py

from flask import Blueprint, render_template, request
from application.forms.login_form import LoginForm
from application.forms.register_form import RegisterForm
from application.helpers.general_helper import form_errors, is_ajax

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


@controller.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if request.method == 'POST' and is_ajax(request):
        if form.validate_on_submit():
            pass
        else:
            return {
                'error': True,
                'form': True,
                'messages': form_errors(form)
            }
    return render_template('pages/auth/login.jinja2', form=form)


@controller.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm()
    if request.method == 'POST' and is_ajax(request):
        if form.validate_on_submit():
            pass
        else:
            return {
                'error': True,
                'form': True,
                'messages': form_errors(form)
            }
    return render_template("pages/auth/register.jinja2", form=form)


이제 각 필드의 오류 배열에서 필드의 첫 번째 오류만 반환하는 도우미 함수가 필요합니다.
helpers 폴더에 폴더applictaion를 만들고 general_helper.py 폴더에 파일helpers을 추가하고 이 파일에 form_erroris_ajax 함수를 추가합니다. is_ajax 함수는 이 요청이 ajax인지 여부를 확인합니다.

general_helper.py


def form_errors(form):
    errors = {}
    for error in form.errors:
        errors[error] = form.errors.get(error)[0]
    return errors


def is_ajax(request):
    return request.headers.get('X-Requested-With') == 'XMLHttpRequest'


Auth Blueprintapplication/settings.py 등록

def register_blueprints(app):
    from application.controllers import (
        home,
        auth
    )

    app.register_blueprint(home.controller)
    app.register_blueprint(auth.controller)

authviews/pages에 폴더를 만들고 두 개의 파일login.jinja2을 만들고 register.jinja2 또한 auth.jinja2에 레이아웃 파일views/layouts을 만듭니다.

auth.jinja2

<!doctype html>
<html lang="en" class="h-full scroll-smooth bg-gray-100 antialiased">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
    <title>{% block title %}{% endblock %}</title>
</head>
<body>

    {% block content %}{% endblock %}

    <script src="{{ url_for('static', filename='js/app.js') }}"></script>
</body>
</html>


로그인.jinja2

{% extends 'layouts/auth.jinja2' %}

{% block title %} Login {% endblock %}

{% block content %}
    <div class="min-h-screen flex justify-center items-center">
        <div class="card md:w-2/6 w-4/5 bg-base-100 shadow-xl">
            <div class="card-body">
                <form action="{{ url_for('auth.login') }}" method="post" class="ajax-form">
                    {{ form.csrf_token() }}
                    <h1 class="card-title">Login!</h1>
                    <p class="mb-6">Welcome back! Log in to your account.</p>
                    <div class="form-control mb-3 w-full">
                        <label for="username" class="label">Username</label>
                        {{ form.username }}
                        <p class="mt-2 text-sm text-red-600 username-feedback error-feedback hidden"></p>
                    </div>
                    <div class="form-control mb-3 w-full">
                        <label for="password" class="label">Password</label>
                        {{ form.password }}
                        <p class="mt-2 text-sm text-red-600 password-feedback error-feedback hidden"></p>
                    </div>
                    <div class="card-actions justify-between items-center mt-6">
                        <a href="{{ url_for('auth.register') }}">Not have an account?</a>
                        <button type="submit" class="btn btn-primary">Login</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
{% endblock %}


레지스터.jinja2

{% extends 'layouts/auth.jinja2' %}

{% block title %} Register {% endblock %}

{% block content %}
    <div class="min-h-screen flex justify-center items-center">
        <div class="card md:w-2/6 w-4/5 bg-base-100 shadow-xl">
            <div class="card-body">
                <form action="{{ url_for('auth.register') }}" method="post" class="ajax-form">
                    {{ form.csrf_token() }}
                    <h1 class="card-title">Register!</h1>
                    <p class="mb-6">Welcome back! Log in to your account.</p>
                    <div class="form-control mb-3 w-full">
                        <label for="name" class="label">Name</label>
                        {{ form.name }}
                        <p class="mt-2 text-sm text-red-600 name-feedback error-feedback hidden"></p>
                    </div>
                    <div class="form-control mb-3 w-full">
                        <label for="username" class="label">Username</label>
                        {{ form.username }}
                        <p class="mt-2 text-sm text-red-600 username-feedback error-feedback hidden"></p>
                    </div>
                    <div class="form-control mb-3 w-full">
                        <label for="email" class="label">Email</label>
                        {{ form.email }}
                        <p class="mt-2 text-sm text-red-600 email-feedback error-feedback hidden"></p>
                    </div>
                    <div class="form-control mb-3 w-full">
                        <label for="password" class="label">Password</label>
                        {{ form.password }}
                        <p class="mt-2 text-sm text-red-600 password-feedback error-feedback hidden"></p>
                    </div>
                    <div class="form-control mb-3 w-full">
                        <label for="password_confirmation" class="label">Confirm Password</label>
                        {{ form.password_confirmation }}
                        <p class="mt-2 text-sm text-red-600 password_confirmation-feedback error-feedback hidden"></p>
                    </div>
                    <div class="card-actions justify-between items-center mt-6">
                        <a href="{{ url_for('auth.login') }}">Already have an account?</a>
                        <button type="submit" class="btn btn-primary">Register</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
{% endblock %}


콘텐츠 배열에서 tailwind.config.js 추가applictaion/forms 디렉토리를 엽니다. WTF 양식을 통해 입력 클래스를 추가하므로 최종 빌드에 이러한 클래스를 포함하도록 tailwindcss에 알려야 하므로 tailwind.config.js는 다음과 같이 표시됩니다.

tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
      './application/views/**/*.jinja2',
      './application/assets/js/**/*.js',
      './application/forms/**/*.py',
  ],
  theme: {
    extend: {},
  },
  plugins: [
      require('@tailwindcss/typography'),
      require('daisyui')
  ],
}


사용자 이름 필드를 추가한 후 사용자 모델에 사용자 이름 필드를 추가하면 모델이 다음과 같이 표시됩니다.

user.py

from application import db


class User(db.Model):

    __tablename__ = 'users'

    id = db.Column(
        db.Integer,
        primary_key=True
    )

    name = db.Column(
        db.String(255),
        nullable=False
    )

    username = db.Column(
        db.String(255),
        nullable=False,
        unique=True,
    )

    email = db.Column(
        db.String(255),
        unique=True,
        nullable=False
    )

    password = db.Column(
        db.String(255),
        nullable=False
    )

    role = db.Column(
        db.String(50),
        nullable=False,
        server_default="user"
    )

    created_at = db.Column(
        db.DateTime,
        server_default=db.func.now(),
        nullable=False
    )

    updated_at = db.Column(
        db.DateTime,
        server_default=db.func.now(),
        nullable=False
    )


필드를 추가한 후 새로운 필드 마이그레이션 생성을 위한 마이그레이션을 실행해주세요.

flask db migrate -m "Add username column in users table."


마이그레이션 후 마이그레이션을 실행하여 DB에 적용하십시오.

flask db upgrade


이제 AJAX의 인증 양식 열기app.js에서 src/js/app.js를 구현할 시간입니다. ajax로 파일을 업데이트하면 파일이 다음과 같이 표시됩니다.

window.$ = window.jQuery = require('jquery');


const spinner = `<div role="status">
    <svg aria-hidden="true" class="w-6 h-6 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
        <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
    </svg>
    <span class="sr-only">Loading...</span>
</div>`;

// ajax form submission
$(".ajax-form").on('submit', function (e){
    e.preventDefault();
    const url = $(this).attr("action");
    const method = $(this).attr("method");
    const payload = $(this).serializeArray();
    const is_refresh = $(this).data("refresh");
    const is_redirect = $(this).data("redirect");
    let submit_btn = $(this).find("button[type=submit]");
    let form_data = new FormData(this);
    let submit_html = submit_btn.html();
    $(this).find("input, select, button, textarea").attr("disabled", true);
    $.ajax({
       url: url,
       method: method,
       data: form_data,
       processData: false,
       contentType: false,
       cache: false,
       beforeSend: () => {
           $(this).find("input, select, textarea").removeClass("input-error focus:outline-red-600").addClass('focus:outline-blue-700');
           $(this).find(".error-feedback").addClass('hidden').text('');
           submit_btn.html(spinner);
       },
       success: (data) => {
           $(this).find("input, select, button, textarea").attr("disabled", false);
           submit_btn.html(submit_html);
           if(data.error && data.form){
               let messages = data.messages;
                Object.keys(messages).forEach(function (key) {
                  $("#" + key).addClass("input-error focus:outline-red-600").removeClass('focus:outline-blue-700');
                  $("." + key + "-feedback").removeClass('hidden').text(messages[key]);
                });
           }else{

           }
       }
    });
})


자산 재구축 또는 시청 시작

yarn watch


애플리케이션 실행

python run.py




GitHub Repo에서 업데이트된 코드를 얻을 수 있습니다.

나와 함께 해주셔서 감사합니다.

이 게시물을 진행하는 동안 문제가 있으면 다음 게시물에서 뵙겠습니다. 언제든지 댓글을 남겨주세요.

좋은 웹페이지 즐겨찾기