<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Python Flask  |  takeHo（たけほ）のへなちょこ台帳</title>
	<atom:link href="https://blog.takeho.com/tag/python-flask/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.takeho.com</link>
	<description>いわゆる自由帳ってところです。</description>
	<lastBuildDate>Thu, 17 Sep 2026 01:36:23 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.6</generator>

<image>
	<url>https://blog.takeho.com/wp-content/uploads/2024/08/icon-150x150.png</url>
	<title>Python Flask  |  takeHo（たけほ）のへなちょこ台帳</title>
	<link>https://blog.takeho.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Python・FlaskでMySQLログイン認証を作る方法―MVC構成、登録確認メール、パスワード再設定まで詳しく解説</title>
		<link>https://blog.takeho.com/x0y9hpuj4w632wsf0pvk6n5ok0xyfioq/</link>
					<comments>https://blog.takeho.com/x0y9hpuj4w632wsf0pvk6n5ok0xyfioq/#respond</comments>
		
		<dc:creator><![CDATA[たけほ]]></dc:creator>
		<pubDate>Thu, 17 Sep 2026 01:28:07 +0000</pubDate>
				<category><![CDATA[Python]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Python Flask]]></category>
		<category><![CDATA[ログイン認証]]></category>
		<guid isPermaLink="false">https://blog.takeho.com/?p=2005</guid>

					<description><![CDATA[PythonでWebアプリを作り始めると、早い段階で必要になるのがログイン認証です。単にユーザー名とパスワードを照合するだけなら短いコードでも作れますが、実際のサービスではアカウント作成、入力値の確認、メールアドレスの有 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>PythonでWebアプリを作り始めると、早い段階で必要になるのがログイン認証です。単にユーザー名とパスワードを照合するだけなら短いコードでも作れますが、実際のサービスではアカウント作成、入力値の確認、メールアドレスの有効性確認、ログアウト、パスワードを忘れた場合の再設定まで必要です。</p>



<p>この記事では、PythonのWebフレームワーク「Flask」とMySQLを使い、認証機能をMVC形式で整理して作る手順を解説します。認証情報は「ユーザー名・メールアドレス・パスワード」の3項目です。パスワードは平文で保存せず、ハッシュ化して安全に管理します。</p>




  <div id="toc" class="toc tnt-number toc-center tnt-number border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-2"><label class="toc-title" for="toc-checkbox-2">目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">この記事で作る機能</a></li><li><a href="#toc2" tabindex="0">使用する構成</a></li><li><a href="#toc3" tabindex="0">MVC形式の考え方</a></li><li><a href="#toc4" tabindex="0">完成時のファイル構成</a></li><li><a href="#toc5" tabindex="0">1. Pythonの仮想環境を作る</a></li><li><a href="#toc6" tabindex="0">2. 必要なライブラリをインストールする</a></li><li><a href="#toc7" tabindex="0">3. MySQLにデータベースと専用ユーザーを作る</a></li><li><a href="#toc8" tabindex="0">4. 環境変数を設定する</a></li><li><a href="#toc9" tabindex="0">5. 設定ファイルを作る</a></li><li><a href="#toc10" tabindex="0">6. 拡張機能をまとめる</a></li><li><a href="#toc11" tabindex="0">7. Userモデルを作る</a></li><li><a href="#toc12" tabindex="0">8. フォームとバリデーションを作る</a></li><li><a href="#toc13" tabindex="0">9. 有効期限付きトークンを作る</a></li><li><a href="#toc14" tabindex="0">10. メール送信処理を作る</a></li><li><a href="#toc15" tabindex="0">11. Controllerを作る</a></li><li><a href="#toc16" tabindex="0">12. Flaskアプリを初期化する</a></li><li><a href="#toc17" tabindex="0">13. HTMLテンプレートを作る</a></li><li><a href="#toc18" tabindex="0">14. MySQLへテーブルを作成する</a></li><li><a href="#toc19" tabindex="0">15. 動作を確認する</a></li><li><a href="#toc20" tabindex="0">本番公開前に追加したい対策</a></li><li><a href="#toc21" tabindex="0">よくあるエラー</a><ol><li><a href="#toc22" tabindex="0">Access denied for user</a></li><li><a href="#toc23" tabindex="0">No module named PyMySQL</a></li><li><a href="#toc24" tabindex="0">確認メールのURLがlocalhostになる</a></li><li><a href="#toc25" tabindex="0">CSRF token is missing</a></li></ol></li><li><a href="#toc26" tabindex="0">まとめ</a></li></ol>
    </div>
  </div>

<h2 class="wp-block-heading"><span id="toc1">この記事で作る機能</span></h2>



<ul class="wp-block-list">
<li>ユーザー名、メールアドレス、パスワードによるアカウント作成</li>



<li>フォーム入力値のバリデーション</li>



<li>確認メールによるメールアドレスの有効性確認</li>



<li>ログイン、ログアウト</li>



<li>ログインが必要なページの保護</li>



<li>パスワード紛失時の再設定メール送信</li>



<li>期限付きトークンによるパスワード再設定</li>



<li>MySQLへのユーザー情報保存</li>
</ul>



<h2 class="wp-block-heading"><span id="toc2">使用する構成</span></h2>



<p>今回は次のライブラリを使います。</p>



<ul class="wp-block-list">
<li><strong>Flask</strong>：Pythonの軽量Webフレームワーク</li>



<li><strong>Flask-SQLAlchemy</strong>：MySQLのデータをPythonのクラスとして扱う</li>



<li><strong>Flask-Migrate</strong>：テーブル構造の変更を管理する</li>



<li><strong>Flask-Login</strong>：ログイン状態を管理する</li>



<li><strong>Flask-WTF / WTForms</strong>：フォームとバリデーション、CSRF対策</li>



<li><strong>Flask-Mail</strong>：確認メールや再設定メールを送信する</li>



<li><strong>itsdangerous</strong>：有効期限付きの安全なトークンを作る</li>



<li><strong>PyMySQL</strong>：PythonからMySQLへ接続する</li>



<li><strong>python-dotenv</strong>：環境変数を読み込む</li>
</ul>



<h2 class="wp-block-heading"><span id="toc3">MVC形式の考え方</span></h2>



<p>Flaskにはフォルダ構成を強制する決まりがありません。そのため、すべてを1つの <code>app.py</code> に書くこともできます。しかし、機能が増えると修正箇所が分かりにくくなります。今回は役割ごとに分離します。</p>



<ul class="wp-block-list">
<li><strong>Model</strong>：MySQLのテーブルとデータ操作。今回は <code>models/user.py</code></li>



<li><strong>View</strong>：利用者に表示するHTML。今回は <code>templates/</code></li>



<li><strong>Controller</strong>：URLごとの処理、入力確認、画面遷移。今回は <code>controllers/auth_controller.py</code></li>
</ul>



<p>FlaskではControllerをBlueprintで分けると、認証機能を他の機能から切り離しやすくなります。</p>



<h2 class="wp-block-heading"><span id="toc4">完成時のファイル構成</span></h2>



<pre class="wp-block-code"><code>python-example/
├── run.py
├── requirements.txt
├── .env
├── .gitignore
├── migrations/
└── app/
    ├── __init__.py
    ├── config.py
    ├── extensions.py
    ├── models/
    │   ├── __init__.py
    │   └── user.py
    ├── forms/
    │   ├── __init__.py
    │   └── auth_form.py
    ├── controllers/
    │   ├── __init__.py
    │   └── auth_controller.py
    ├── services/
    │   ├── __init__.py
    │   ├── mail_service.py
    │   └── token_service.py
    └── templates/
        ├── base.html
        ├── index.html
        └── auth/
            ├── register.html
            ├── login.html
            ├── forgot_password.html
            └── reset_password.html</code></pre>



<h2 class="wp-block-heading"><span id="toc5">1. Pythonの仮想環境を作る</span></h2>



<pre class="wp-block-code"><code>mkdir python-example
cd python-example
python3 -m venv venv
source venv/bin/activate</code></pre>



<p><code>python3 -m venv venv</code> は、プロジェクト専用のPython環境を作るコマンドです。ほかのPythonアプリとライブラリのバージョンが混ざるのを防ぎます。Windowsの場合は <code>venv\Scripts\activate</code> で有効化します。</p>



<h2 class="wp-block-heading"><span id="toc6">2. 必要なライブラリをインストールする</span></h2>



<pre class="wp-block-code"><code>pip install Flask Flask-SQLAlchemy Flask-Migrate Flask-Login Flask-WTF Flask-Mail email-validator PyMySQL python-dotenv
pip freeze &gt; requirements.txt</code></pre>



<p><code>email-validator</code> は、メールアドレスの形式をWTFormsで確認するために必要です。インストール後の状態を <code>requirements.txt</code> に記録しておけば、別のサーバーでも <code>pip install -r requirements.txt</code> で同じ環境を作れます。</p>



<h2 class="wp-block-heading"><span id="toc7">3. MySQLにデータベースと専用ユーザーを作る</span></h2>



<p>MySQLへ管理権限のあるユーザーでログインします。</p>



<pre class="wp-block-code"><code>mysql -u root -p</code></pre>



<p>続いて、データベースとアプリ専用ユーザーを作成します。</p>



<pre class="wp-block-code"><code>CREATE DATABASE python_example
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

CREATE USER 'python_user'@'localhost'
  IDENTIFIED BY '十分に長く推測されにくいパスワード';

GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP
  ON python_example.* TO 'python_user'@'localhost';

FLUSH PRIVILEGES;
EXIT;</code></pre>



<p><code>utf8mb4</code> を指定すると、日本語だけでなく絵文字も保存できます。本番環境ではrootユーザーをアプリから使わず、対象データベースだけを操作できる専用ユーザーを使います。</p>



<h2 class="wp-block-heading"><span id="toc8">4. 環境変数を設定する</span></h2>



<p>パスワードやメールサーバーの認証情報をソースコードへ直接書かないように、プロジェクト直下へ <code>.env</code> を作ります。</p>



<pre class="wp-block-code"><code>SECRET_KEY=ここに長いランダム文字列
DATABASE_URL=mysql+pymysql://python_user:DBパスワード@localhost/python_example?charset=utf8mb4

MAIL_SERVER=smtp.example.com
MAIL_PORT=587
MAIL_USE_TLS=true
MAIL_USERNAME=no-reply@example.com
MAIL_PASSWORD=メール用パスワード
MAIL_DEFAULT_SENDER=no-reply@example.com</code></pre>



<p><code>SECRET_KEY</code> はCSRF対策やトークン署名に使います。次のコマンドで生成できます。</p>



<pre class="wp-block-code"><code>python -c "import secrets; print(secrets.token_hex(32))"</code></pre>



<p><code>.gitignore</code> には必ず次を記載します。</p>



<pre class="wp-block-code"><code>venv/
.env
__pycache__/
*.pyc</code></pre>



<h2 class="wp-block-heading"><span id="toc9">5. 設定ファイルを作る</span></h2>



<p><code>app/config.py</code> を作成します。</p>



<pre class="wp-block-code"><code>import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    SECRET_KEY = os.environ&#91;"SECRET_KEY"]
    SQLALCHEMY_DATABASE_URI = os.environ&#91;"DATABASE_URL"]
    SQLALCHEMY_TRACK_MODIFICATIONS = False

    MAIL_SERVER = os.environ&#91;"MAIL_SERVER"]
    MAIL_PORT = int(os.getenv("MAIL_PORT", "587"))
    MAIL_USE_TLS = os.getenv("MAIL_USE_TLS", "true").lower() == "true"
    MAIL_USERNAME = os.environ&#91;"MAIL_USERNAME"]
    MAIL_PASSWORD = os.environ&#91;"MAIL_PASSWORD"]
    MAIL_DEFAULT_SENDER = os.environ&#91;"MAIL_DEFAULT_SENDER"]</code></pre>



<p><code>os.environ["名前"]</code> は、設定がない場合にエラーとなります。重要な設定の不足に気づきやすいため、本番用の認証情報にはこの書き方が向いています。</p>



<h2 class="wp-block-heading"><span id="toc10">6. 拡張機能をまとめる</span></h2>



<p><code>app/extensions.py</code> を作ります。</p>



<pre class="wp-block-code"><code>from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_mail import Mail

db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
mail = Mail()

login_manager.login_view = "auth.login"
login_manager.login_message = "ログインしてください。"
login_manager.login_message_category = "warning"</code></pre>



<p>ここでは拡張機能の本体だけを作り、アプリとの接続は後ほど <code>create_app()</code> の中で行います。この形式はApplication Factoryパターンと呼ばれ、テストや設定の切り替えがしやすくなります。</p>



<h2 class="wp-block-heading"><span id="toc11">7. Userモデルを作る</span></h2>



<p><code>app/models/user.py</code> を作成します。</p>



<pre class="wp-block-code"><code>from datetime import datetime, timezone
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app.extensions import db, login_manager

class User(UserMixin, db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True, nullable=False, index=True)
    email = db.Column(db.String(255), unique=True, nullable=False, index=True)
    password_hash = db.Column(db.String(255), nullable=False)
    email_verified = db.Column(db.Boolean, nullable=False, default=False)
    created_at = db.Column(
        db.DateTime,
        nullable=False,
        default=lambda: datetime.now(timezone.utc)
    )

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

@login_manager.user_loader
def load_user(user_id):
    return db.session.get(User, int(user_id))</code></pre>



<p>データベースにはパスワードそのものではなく <code>password_hash</code> を保存します。<code>generate_password_hash()</code> は同じパスワードでも毎回異なるハッシュ値を作ります。ログイン時は <code>check_password()</code> で照合します。</p>



<p><code>email_verified</code> はメール確認が完了したかを示す項目です。登録直後は <code>False</code>、確認用URLを開いたあとに <code>True</code> へ変更します。</p>



<h2 class="wp-block-heading"><span id="toc12">8. フォームとバリデーションを作る</span></h2>



<p><code>app/forms/auth_form.py</code> を作成します。</p>



<pre class="wp-block-code"><code>from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import (
    DataRequired, Email, EqualTo, Length, Regexp, ValidationError
)
from app.models.user import User

class RegisterForm(FlaskForm):
    username = StringField(
        "ユーザー名",
        validators=&#91;
            DataRequired(),
            Length(min=3, max=50),
            Regexp(
                r"^&#91;A-Za-z0-9_]+$",
                message="半角英数字とアンダースコアのみ使用できます。"
            )
        ]
    )
    email = StringField(
        "メールアドレス",
        validators=&#91;DataRequired(), Email(), Length(max=255)]
    )
    password = PasswordField(
        "パスワード",
        validators=&#91;
            DataRequired(),
            Length(min=12, max=128),
            Regexp(r"^(?=.*&#91;a-z])(?=.*&#91;A-Z])(?=.*\d).+$",
                   message="英大文字・英小文字・数字を含めてください。")
        ]
    )
    password_confirm = PasswordField(
        "パスワード（確認）",
        validators=&#91;DataRequired(), EqualTo("password")]
    )
    submit = SubmitField("アカウントを作成")

    def validate_username(self, field):
        if User.query.filter_by(username=field.data.strip()).first():
            raise ValidationError("このユーザー名はすでに使われています。")

    def validate_email(self, field):
        email = field.data.strip().lower()
        if User.query.filter_by(email=email).first():
            raise ValidationError("このメールアドレスはすでに登録されています。")

class LoginForm(FlaskForm):
    login_id = StringField(
        "ユーザー名またはメールアドレス",
        validators=&#91;DataRequired(), Length(max=255)]
    )
    password = PasswordField("パスワード", validators=&#91;DataRequired()])
    submit = SubmitField("ログイン")

class ForgotPasswordForm(FlaskForm):
    email = StringField(
        "メールアドレス",
        validators=&#91;DataRequired(), Email(), Length(max=255)]
    )
    submit = SubmitField("再設定メールを送信")

class ResetPasswordForm(FlaskForm):
    password = PasswordField(
        "新しいパスワード",
        validators=&#91;
            DataRequired(),
            Length(min=12, max=128),
            Regexp(r"^(?=.*&#91;a-z])(?=.*&#91;A-Z])(?=.*\d).+$",
                   message="英大文字・英小文字・数字を含めてください。")
        ]
    )
    password_confirm = PasswordField(
        "新しいパスワード（確認）",
        validators=&#91;DataRequired(), EqualTo("password")]
    )
    submit = SubmitField("パスワードを変更")</code></pre>



<p>ブラウザ側の <code>required</code> だけでは不十分です。利用者はHTTPリクエストを直接送ることもできるため、必ずPython側でも確認します。主な確認内容は、必須入力、長さ、メール形式、使用可能文字、パスワード一致、重複登録です。</p>



<h2 class="wp-block-heading"><span id="toc13">9. 有効期限付きトークンを作る</span></h2>



<p><code>app/services/token_service.py</code> を作ります。</p>



<pre class="wp-block-code"><code>from flask import current_app
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired

def _serializer():
    return URLSafeTimedSerializer(current_app.config&#91;"SECRET_KEY"])

def create_token(user_id, purpose):
    return _serializer().dumps({"user_id": user_id}, salt=purpose)

def verify_token(token, purpose, max_age=3600):
    try:
        data = _serializer().loads(token, salt=purpose, max_age=max_age)
        return data.get("user_id")
    except (SignatureExpired, BadSignature):
        return None</code></pre>



<p><code>purpose</code> には、メール確認なら <code>email-confirm</code>、パスワード再設定なら <code>password-reset</code> を指定します。用途ごとにsaltを分けることで、確認用トークンをパスワード再設定へ流用できないようにします。<code>max_age=3600</code> は有効期限1時間です。</p>



<h2 class="wp-block-heading"><span id="toc14">10. メール送信処理を作る</span></h2>



<p><code>app/services/mail_service.py</code> を作成します。</p>



<pre class="wp-block-code"><code>from flask import url_for
from flask_mail import Message
from app.extensions import mail
from app.services.token_service import create_token

def send_verification_email(user):
    token = create_token(user.id, "email-confirm")
    verify_url = url_for(
        "auth.verify_email", token=token, _external=True
    )
    message = Message(
        subject="メールアドレスの確認",
        recipients=&#91;user.email],
        body=f"""アカウント作成ありがとうございます。
次のURLを1時間以内に開いてください。

{verify_url}

心当たりがない場合は、このメールを破棄してください。
"""
    )
    mail.send(message)

def send_password_reset_email(user):
    token = create_token(user.id, "password-reset")
    reset_url = url_for(
        "auth.reset_password", token=token, _external=True
    )
    message = Message(
        subject="パスワード再設定",
        recipients=&#91;user.email],
        body=f"""次のURLを1時間以内に開き、新しいパスワードを設定してください。

{reset_url}

依頼していない場合は、このメールを破棄してください。
"""
    )
    mail.send(message)</code></pre>



<p><code>_external=True</code> を指定すると、メール本文で利用できる絶対URLが作られます。本番環境ではHTTPSで公開し、正しいドメインが生成されるようにWebサーバー側のHost設定も確認します。</p>



<h2 class="wp-block-heading"><span id="toc15">11. Controllerを作る</span></h2>



<p><code>app/controllers/auth_controller.py</code> を作ります。</p>



<pre class="wp-block-code"><code>from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from sqlalchemy import or_
from app.extensions import db
from app.models.user import User
from app.forms.auth_form import (
    RegisterForm, LoginForm, ForgotPasswordForm, ResetPasswordForm
)
from app.services.mail_service import (
    send_verification_email, send_password_reset_email
)
from app.services.token_service import verify_token

auth_bp = Blueprint("auth", __name__, url_prefix="/auth")

@auth_bp.route("/register", methods=&#91;"GET", "POST"])
def register():
    if current_user.is_authenticated:
        return redirect(url_for("index"))

    form = RegisterForm()
    if form.validate_on_submit():
        user = User(
            username=form.username.data.strip(),
            email=form.email.data.strip().lower()
        )
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()

        send_verification_email(user)
        flash("確認メールを送信しました。メール内のURLを開いてください。", "success")
        return redirect(url_for("auth.login"))

    return render_template("auth/register.html", form=form)

@auth_bp.route("/verify-email/&lt;token&gt;")
def verify_email(token):
    user_id = verify_token(token, "email-confirm", max_age=3600)
    user = db.session.get(User, user_id) if user_id else None

    if not user:
        flash("確認URLが無効、または有効期限が切れています。", "danger")
        return redirect(url_for("auth.login"))

    if not user.email_verified:
        user.email_verified = True
        db.session.commit()

    flash("メールアドレスを確認しました。ログインできます。", "success")
    return redirect(url_for("auth.login"))

@auth_bp.route("/login", methods=&#91;"GET", "POST"])
def login():
    if current_user.is_authenticated:
        return redirect(url_for("index"))

    form = LoginForm()
    if form.validate_on_submit():
        login_id = form.login_id.data.strip()
        user = User.query.filter(
            or_(User.username == login_id, User.email == login_id.lower())
        ).first()

        if not user or not user.check_password(form.password.data):
            flash("ユーザー名、メールアドレス、またはパスワードが違います。", "danger")
            return render_template("auth/login.html", form=form)

        if not user.email_verified:
            flash("メールアドレスの確認が完了していません。", "warning")
            return render_template("auth/login.html", form=form)

        login_user(user)
        next_url = request.args.get("next")
        if next_url and next_url.startswith("/"):
            return redirect(next_url)
        return redirect(url_for("index"))

    return render_template("auth/login.html", form=form)

@auth_bp.route("/logout", methods=&#91;"POST"])
@login_required
def logout():
    logout_user()
    flash("ログアウトしました。", "success")
    return redirect(url_for("auth.login"))

@auth_bp.route("/forgot-password", methods=&#91;"GET", "POST"])
def forgot_password():
    form = ForgotPasswordForm()
    if form.validate_on_submit():
        user = User.query.filter_by(
            email=form.email.data.strip().lower()
        ).first()

        if user:
            send_password_reset_email(user)

        flash("登録済みの場合は、再設定メールを送信しました。", "success")
        return redirect(url_for("auth.login"))

    return render_template("auth/forgot_password.html", form=form)

@auth_bp.route("/reset-password/&lt;token&gt;", methods=&#91;"GET", "POST"])
def reset_password(token):
    user_id = verify_token(token, "password-reset", max_age=3600)
    user = db.session.get(User, user_id) if user_id else None

    if not user:
        flash("再設定URLが無効、または有効期限が切れています。", "danger")
        return redirect(url_for("auth.forgot_password"))

    form = ResetPasswordForm()
    if form.validate_on_submit():
        user.set_password(form.password.data)
        db.session.commit()
        flash("パスワードを変更しました。", "success")
        return redirect(url_for("auth.login"))

    return render_template("auth/reset_password.html", form=form)</code></pre>



<p>パスワード再設定では、メールアドレスが未登録の場合でも同じ案内を表示しています。「そのメールアドレスが登録されているか」を第三者に知られにくくするためです。また、ログアウトをGETではなくPOSTにしているのは、外部サイトのリンクを開いただけでログアウトさせられる動作を防ぐためです。</p>



<h2 class="wp-block-heading"><span id="toc16">12. Flaskアプリを初期化する</span></h2>



<p><code>app/__init__.py</code> を作成します。</p>



<pre class="wp-block-code"><code>from flask import Flask, render_template
from flask_login import login_required
from app.config import Config
from app.extensions import db, migrate, login_manager, mail

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)

    db.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)
    mail.init_app(app)

    from app.controllers.auth_controller import auth_bp
    app.register_blueprint(auth_bp)

    @app.route("/")
    def index():
        return render_template("index.html")

    @app.route("/mypage")
    @login_required
    def mypage():
        return "ログイン済みのユーザーだけが表示できます。"

    return app</code></pre>



<p><code>run.py</code> は次の内容です。</p>



<pre class="wp-block-code"><code>from app import create_app

app = create_app()

if __name__ == "__main__":
    app.run(debug=True)</code></pre>



<h2 class="wp-block-heading"><span id="toc17">13. HTMLテンプレートを作る</span></h2>



<p><code>app/templates/base.html</code> では、CSRFトークンを含むログアウトフォームとメッセージ表示を用意します。</p>



<pre class="wp-block-code"><code>&lt;!doctype html&gt;
&lt;html lang="ja"&gt;
&lt;head&gt;
  &lt;meta charset="utf-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;
  &lt;title&gt;{% block title %}Python Example{% endblock %}&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;nav&gt;
    &lt;a href="{{ url_for('index') }}"&gt;ホーム&lt;/a&gt;
    {% if current_user.is_authenticated %}
      &lt;form method="post" action="{{ url_for('auth.logout') }}"&gt;
        &lt;input type="hidden" name="csrf_token" value="{{ csrf_token() }}"&gt;
        &lt;button type="submit"&gt;ログアウト&lt;/button&gt;
      &lt;/form&gt;
    {% else %}
      &lt;a href="{{ url_for('auth.login') }}"&gt;ログイン&lt;/a&gt;
      &lt;a href="{{ url_for('auth.register') }}"&gt;アカウント作成&lt;/a&gt;
    {% endif %}
  &lt;/nav&gt;

  {% with messages = get_flashed_messages(with_categories=true) %}
    {% for category, message in messages %}
      &lt;p class="{{ category }}"&gt;{{ message }}&lt;/p&gt;
    {% endfor %}
  {% endwith %}

  {% block content %}{% endblock %}
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p>フォームは共通して、ラベル、入力欄、バリデーションエラーを表示します。例として <code>app/templates/auth/register.html</code> を示します。</p>



<pre class="wp-block-code"><code>{% extends "base.html" %}
{% block title %}アカウント作成{% endblock %}
{% block content %}
&lt;h1&gt;アカウント作成&lt;/h1&gt;
&lt;form method="post" novalidate&gt;
  {{ form.hidden_tag() }}

  {{ form.username.label }}
  {{ form.username(autocomplete="username") }}
  {% for error in form.username.errors %}
    &lt;p class="error"&gt;{{ error }}&lt;/p&gt;
  {% endfor %}

  {{ form.email.label }}
  {{ form.email(autocomplete="email") }}
  {% for error in form.email.errors %}
    &lt;p class="error"&gt;{{ error }}&lt;/p&gt;
  {% endfor %}

  {{ form.password.label }}
  {{ form.password(autocomplete="new-password") }}
  {% for error in form.password.errors %}
    &lt;p class="error"&gt;{{ error }}&lt;/p&gt;
  {% endfor %}

  {{ form.password_confirm.label }}
  {{ form.password_confirm(autocomplete="new-password") }}
  {% for error in form.password_confirm.errors %}
    &lt;p class="error"&gt;{{ error }}&lt;/p&gt;
  {% endfor %}

  {{ form.submit() }}
&lt;/form&gt;
{% endblock %}</code></pre>



<p><code>form.hidden_tag()</code> にはCSRFトークンが含まれます。第三者サイトから勝手にフォームを送信される攻撃を防ぐため、省略しないようにします。ログイン画面にはユーザー名またはメールアドレス、パスワード、ログインボタンと、パスワード再設定画面へのリンクを置きます。</p>



<h2 class="wp-block-heading"><span id="toc18">14. MySQLへテーブルを作成する</span></h2>



<p>モデルを読み込める状態にしてからマイグレーションを実行します。</p>



<pre class="wp-block-code"><code>export FLASK_APP=run.py
flask db init
flask db migrate -m "create users table"
flask db upgrade</code></pre>



<p><code>flask db init</code> は初回だけ実行します。<code>flask db migrate</code> はモデルとの差分から変更ファイルを作り、<code>flask db upgrade</code> が実際にMySQLへ反映します。本番環境では、生成された変更内容を確認してから適用してください。</p>



<p>テーブルを確認する場合は次のようにします。</p>



<pre class="wp-block-code"><code>mysql -u python_user -p python_example
SHOW TABLES;
DESCRIBE users;</code></pre>



<h2 class="wp-block-heading"><span id="toc19">15. 動作を確認する</span></h2>



<pre class="wp-block-code"><code>flask --app run.py run --debug</code></pre>



<p>ブラウザで <code>http://127.0.0.1:5000/auth/register</code> を開き、次の順番で確認します。</p>



<ol class="wp-block-list">
<li>未入力や不正なメール形式でエラーが表示される</li>



<li>短いパスワードや条件を満たさないパスワードが拒否される</li>



<li>正常に登録するとMySQLへユーザーが追加される</li>



<li>確認メールが届き、URLを開くと <code>email_verified</code> が有効になる</li>



<li>確認前はログインできず、確認後はログインできる</li>



<li>同じユーザー名やメールアドレスを重複登録できない</li>



<li>パスワード再設定メールが届き、新しいパスワードに変更できる</li>



<li>期限切れ、改ざん済みトークンが拒否される</li>



<li>未ログインで <code>/mypage</code> を開くとログイン画面へ移動する</li>
</ol>



<h2 class="wp-block-heading"><span id="toc20">本番公開前に追加したい対策</span></h2>



<p>ここまでで基本的な認証は動きますが、インターネットへ公開する場合は次の対策も重要です。</p>



<ul class="wp-block-list">
<li>必ずHTTPSを使用する</li>



<li>ログインや再設定メール送信へ回数制限を入れる</li>



<li>連続失敗を記録し、総当たり攻撃を検知する</li>



<li>セッションCookieへ <code>Secure</code>、<code>HttpOnly</code>、適切な <code>SameSite</code> を設定する</li>



<li>確認メールの再送機能を用意し、再送にも回数制限を入れる</li>



<li>メール送信はCeleryなどのバックグラウンド処理へ分離する</li>



<li>パスワード変更後に既存セッションを無効化する仕組みを検討する</li>



<li>ユーザー入力や認証結果をログへ残す際、パスワードやトークンを書かない</li>



<li>MySQLとアプリのバックアップ、復元手順を用意する</li>
</ul>



<p>Cookie設定の一例は次のとおりです。</p>



<pre class="wp-block-code"><code>SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
REMEMBER_COOKIE_SECURE = True
REMEMBER_COOKIE_HTTPONLY = True</code></pre>



<h2 class="wp-block-heading"><span id="toc21">よくあるエラー</span></h2>



<h3 class="wp-block-heading"><span id="toc22">Access denied for user</span></h3>



<p>MySQLのユーザー名、パスワード、接続元ホスト、GRANTの対象を確認します。<code>'python_user'@'localhost'</code> と <code>'python_user'@'127.0.0.1'</code> はMySQLでは別の接続元として扱われる場合があります。</p>



<h3 class="wp-block-heading"><span id="toc23">No module named PyMySQL</span></h3>



<p>仮想環境が有効か確認し、<code>pip install PyMySQL</code> を実行します。Gunicornやsystemdから起動する場合は、仮想環境内の実行ファイルを指定します。</p>



<h3 class="wp-block-heading"><span id="toc24">確認メールのURLがlocalhostになる</span></h3>



<p>本番ドメインやリバースプロキシの設定が正しくFlaskへ渡っていない可能性があります。ApacheやNginxのHostヘッダー、HTTPS判定、必要に応じてProxyFixの設定を確認します。</p>



<h3 class="wp-block-heading"><span id="toc25">CSRF token is missing</span></h3>



<p>テンプレート内のフォームに <code>{{ form.hidden_tag() }}</code> があるか確認します。通常のHTMLフォームを使う場合も、CSRFトークンのhidden項目が必要です。</p>



<h2 class="wp-block-heading"><span id="toc26">まとめ</span></h2>



<p>PythonとFlaskでログイン認証を作る場合は、ログイン処理だけでなく、登録時の入力確認、メールアドレス確認、パスワードのハッシュ化、再設定用トークンの期限管理までを一つの流れとして考えることが大切です。</p>



<p>MVC形式でModel、View、Controllerを分け、メール処理とトークン処理をServiceへ分離しておけば、機能追加やテストもしやすくなります。まずはローカル環境で登録から再設定まで一通り確認し、その後にHTTPS、回数制限、ログ管理など本番向けの安全対策を追加すると進めやすいでしょう。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.takeho.com/x0y9hpuj4w632wsf0pvk6n5ok0xyfioq/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
