【Python特化】おすすめのオンラインプログラミングスクール

【Flask】flask-loginによるログイン機能を実装した認証アプリ開発

flask-login
本記事の要点
  • flask-loginの使い方を理解する
  • サインアップ/ログイン認証/ログアウト機能の実装方法を理解する
  • バリデーションチェックやデータベース操作を理解する

上記をまとめ、具体的なコードやflask-loginの使い方を重点的に解説します。

目次

認証アプリのディレクトリ構成

本記事では、以下のディレクトリ構成で認証アプリを開発します。

flask-login
├── .env
├── apps
│   ├── app.py
│   ├── config.py
│   ├── static
│   │   └── bootstrap.min.css
│   └── auth
│       ├── __init__.py
│       ├── forms.py
│       ├── models.py
│       ├── static/css
│       │   └── style.css
│       ├── templates
│       │   └── auth
│       │       ├── base.html
│       │       ├── index.html
│       │       ├── login.html
│       │       └── signup.html
│       └── views.py
├── local.sqlite
└── migrations
ディレクトリ構成
  • flask-login:開発用ディレクトリ名
  • apps:格納されたアプリを管理するディレクトリ
  • static/css:静的ファイルのディレクトリ
  • auth:認証アプリの関連ファイルを格納したディレクトリ
  • templates:HTMLファイルを格納したディレクトリ

各ファイルについては、後述の中で解説していきます。

認証アプリの完成画面は、以下になります。

flask-login

Flaskを利用するためのPython環境構築

Pythonで開発を実施する際、開発プロジェクトごとに専用の実行環境を作成し切り替えることができます。

また、開発用として一時的に作成する実行環境を「仮想環境」と呼びます。

ローカル環境で開発を進めるにあたり、venvモジュールを利用し仮想環境を構築します。

venvは、Pythonに標準搭載された仮想環境用モジュールです。

venvを利用することで、プロジェクトごとに分離したPython実行環境を構築できるため、各実行環境でそれぞれのパッケージを管理できます。

Pythonによる仮想環境の構築方法が知りたい人は、「【Python】ダウンロードとインストール方法から開発環境構築まで解説!」を一読ください。

Flaskのインストール

本記事では、WindowsPCによる任意のディレクトリにてFlaskをインストールしています。

インストール自体は簡単で、以下のコマンドを実行しましょう。

pip install flask

上記のコマンドを実行すると、インストールが完了します。

FlaskによるWebアプリ開発の事前準備

任意で作成したディレクトリにcdコマンドで移動後、仮想環境を作成します。

py -m venv venv

次に、仮想環境をアクティベート(有効化)します。

./venv/Scripts/Activate

pip listを利用するとパッケージの状況が確認できます。

インストールしたのちにpip listでパッケージ状況を確認すると以下のように表示されます。

(venv) PS C:\Users\sugir\Documents\flask-login> pip list
Package      Version
------------ -------
blinker      1.8.2
click        8.1.7
colorama     0.4.6
Flask        3.0.3
itsdangerous 2.2.0
Jinja2       3.1.4
MarkupSafe   2.1.5
pip          24.0
Werkzeug     3.0.4

さらに、本プログラムでは環境変数の設定ファイルとして.envファイルを利用します。

.envファイルを利用すると、アプリ単位で環境変数を設定できます。

以下のコマンドでpython-dotenvをインストールします。

pip install python-dotenv

pip listで確認します。

(venv) PS C:\Users\sugir\Documents\flask-login> pip list
Package       Version
------------- -------
blinker       1.8.2
click         8.1.7
colorama      0.4.6
Flask         3.0.3
itsdangerous  2.2.0
Jinja2        3.1.4
MarkupSafe    2.1.5
pip           24.0
python-dotenv 1.0.1
Werkzeug      3.0.4

flaskのインストールが完了したので認証アプリを開発する準備が整いました。

ログイン機能を実装した認証アプリ開発

本記事では、以下の流れで開発を進めます。

ログイン認証アプリ開発の工程
  • Blueprintによるアプリ登録
  • SQLAlchemyとMigrateの事前準備
  • コンフィグ設定
  • データベースを使ったCRUD機能作成
  • サインアップ機能の作成
  • ログイン機能の作成
  • ログアウト機能の作成

上記の流れで開発していきます。

Blueprintによるアプリ登録

始めに、以下のディレクトリとファイルを作成します。

flask-login
├── .env
└── apps
    ├── app.py
    └── auth
        └── views.py

Blueprintによるアプリ登録を実施します。

# Flaskのインポート
from flask import Flask

def create_app():
    # Flaskのインスタンス作成
    app = Flask(__name__)

    # authパッケージからviewsをインポート
    from apps.auth import views as auth_views

    # register_blueprintを利用してviewsのauthappをアプリへ登録
    app.register_blueprint(auth_views.auth, url_prefix="/auth")

    return app
app.py
  • flaskのインポート
  • flaskのインスタンス作成
  • authパッケージからviewsをインポート
  • register_blueprintを利用してviewsのauthappをアプリへ登録
from flask import Flask

flaskをインポートし、Flaskフレームワークの機能を利用できるようにします。

def create_app():
    app = Flask(__name__)
    from apps.auth import views as auth_views
    app.register_blueprint(auth_views.auth, url_prefix="/auth")
    return app

create_app関数を作成し、appへFlaskインスタンスを作成します。

本プログラムで開発するauthパッケージからviews.pyをインポートします。

Blueprint機能によるregister_blueprint関数を利用し、authアプリを登録します。

また、url_prefixに/authを指定し、views.pyのエンドポイント全てのURLがauthから始まるよう設定します。

次に、views.pyにてBlueprintによるアプリを作成します。

from flask import Blueprint

auth = Blueprint(
    "auth",
    __name__,
    template_folder = "templates",
    static_folder = "static",
)

@auth.route("/")
def index():
    return 'Hello World!'
views.py
  • Blueprintのインポート
  • Blueprintにてauthを作成
  • デコレータ関数にてURLマッピング
  • index関数で任意の文字列を出力
from flask import Blueprint

作成するauthアプリのviews.pyでは、Blueprintをインポートします。

auth = Blueprint(
    "auth",
    __name__,
    template_folder = "templates",
    static_folder = "static",
)

authアプリをBlueprintオブジェクトで生成します。

@auth.route("/")
def index():
    return 'Hello World!'

ひとまず、デコレータ関数にてURLマッピングしており、任意の文字列を出力しておきます。

また、.envファイルにて環境変数を設定します。

FLASK_APP = apps.app:create_app
FLASK_DEBUG = 1

アプリを生成するのが関数の場合、「モジュール:関数」と指定します。

また、アプリを生成する関数名がcreate_app関数の場合、Flaskが自動でcreate_app関数を呼び出せます。

これらのディレクトリ/ファイルを起点にコードを追加していきます。

一度、flask routesコマンドでルーティングの確認します。

(venv) PS C:\Users\sugir\Documents\flask-login> flask routes
Endpoint     Methods  Rule
-----------  -------  ----------------------------
auth.index   GET      /auth/
auth.static  GET      /auth/static/<path:filename>
static       GET      /static/<path:filename>

ルートが割り当てられていることが分かります。

次に、flask runコマンドを実行し127.0.0.5000/auth/にアクセスしてみましょう。

SQLAlchemyとMigrateの事前準備

次に、データベースを利用するための事前準備を行います。

データベースを利用するための事前準備
  • SQLAlchemyのインストール
  • Migrateのインストール

上記どちらもflaskの拡張機能としてインストールできます。

SQLAlchemyとMigrateのインストール

pip installコマンドでSQLAlchemyとMigrateをインストールします。

pip install flask-sqlalchemy
pip install flask-migrate

pip listコマンドでパッケージを確認します。

(venv) PS C:\Users\sugir\Documents\flask-login> pip list
Package           Version
----------------- -------
alembic           1.13.2
blinker           1.8.2
click             8.1.7
colorama          0.4.6
Flask             3.0.3
Flask-Migrate     4.0.7
Flask-SQLAlchemy  3.1.1
greenlet          3.1.0
itsdangerous      2.2.0
Jinja2            3.1.4
Mako              1.3.5
MarkupSafe        2.1.5
pip               24.0
python-dotenv     1.0.1
SQLAlchemy        2.0.35
typing_extensions 4.12.2
Werkzeug          3.0.4

インストールが完了したのでapp.pyにて利用準備を実施します。

SQLAlchemyとMigrateの利用準備

ここからはSQLAlchemyとMigrateの利用準備のため、app.pyにコードを追加します。

# Flaskのインポート
from flask import Flask
# SQLAlchemyのインポート
from flask_sqlalchemy import SQLAlchemy
# Migrateのインポート
from flask_migrate import Migrate

# SQLAlchemyのインスタンス作成
db = SQLAlchemy()

def create_app():
    # Flaskのインスタンス作成
    app = Flask(__name__)

    # SQLalchemyとアプリを連携
    db.init_app(app)
    # Migrateとアプリを連携
    Migrate(app, db)

    # authパッケージからviewsをインポート
    from apps.auth import views as auth_views

    # register_blueprintを利用してviewsのauthappをアプリへ登録
    app.register_blueprint(auth_views.auth, url_prefix="/auth")

    return app
app.py
  • SQLAlchemyのインポート
  • Migrateのインポート
  • SQLAlchemyのインスタンス作成
  • SQLalchemyとアプリを連携
  • Migrateとアプリを連携
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

先ほどインストールしたSQLAlchemyとMigrateをインポートします。

db = SQLAlchemy()

変数dbへSQLAlchemyをインスタンス化します。

db.init_app(app)
Migrate(app, db)

create_app関数内にて、SQLAlchemyとMigrateをアプリに連携させます。

models.pyの作成と実装

次に、以下のディレクトリに__init__.pyとmodels.pyを追加で作成します。

flask-login
├── .env
└── apps
    ├── app.py
    └── auth
        ├── __init__.py
        ├── models.py
        └── views.py

ファイル作成が完了したら、models.pyにコードを追加します。

from apps.app import db
from werkzeug.security import generate_password_hash

# db.Modelを継承したUserクラス作成
class User(db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String, index=True)
    email = db.Column(db.String, unique=True, index=True)
    password_hash = db.Column(db.String)

    @property
    def password(self):
        raise AttributeError("読み取り不可")
    
    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)
models.py
  • apps.appからdbをインポート
  • generate_password_hashをインポート
  • db.Modelを継承したUserクラス作成
  • テーブル名の指定
  • カラムの定義
  • パスワードをセットするプロパティ作成
  • passwordのsetter関数でハッシュ化したパスワードをセット
from apps.app import db
from werkzeug.security import generate_password_hash

apps.appからSQLAlchemyをインスタンス化したdbをインポートします。

また、werkzeug.securityから入力されたパスワードをハッシュ化するgenerate_password_hash関数をインポートします。

class User(db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String, index=True)
    email = db.Column(db.String, unique=True, index=True)
    password_hash = db.Column(db.String)

    @property
    def password(self):
        raise AttributeError("読み取り不可")
    
    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

db.Modelを継承したUserクラスを作成し、テーブル名を指定します。

作成したいカラム(列)を定義し、パスワードをセットするプロパティを作成します。

また、passwordのsetter関数にてパスワードをハッシュ化してpassword_hashに値をセットしています。

__init__.pyによるモデルの宣言

モデルを作成できたら、__init__.pyにてmodels.pyをインポートします。

import apps.auth.models

コンフィグ設定

次に、以下のディレクトリにconfig.pyを追加で作成します。

flask-login
├── .env
└── apps
    ├── app.py
    ├── config.py
    └── auth
        ├── __init__.py
        ├── models.py
        └── views.py

ファイル作成が完了したら、config.pyにコードを追加します。

from pathlib import Path

basedir = Path(__file__).parent.parent

# BaseConfigクラスを作成
class BaseConfig:
    SECRET_KEY = "{SECRET_KEY}"

# BaseConfigクラスを継承しLocalConfigクラスを作成
class LocalConfig(BaseConfig):
    SQLALCHEMY_DATABASE_URI = f"sqlite:///{basedir/'local.sqlite'}"
    SQLALCHEMY_TRACK_MODIFICATIONS = False

# config辞書にマッピング
config = {
    "local": LocalConfig,
}
config.py
  • Pathをインポート
  • baseのパスになるbasedirを作成
  • BaseConfigクラス作成
  • config辞書を作成

config設定は、アプリにおいてどのように管理するか重要です。

開発環境・ステージング環境・本番環境など各環境で設定すべきコンフィグの値が変わるためです。

本記事では、環境を簡易に変更できるfrom_object形式で設定しています。

以下は、コンフィグ設定時に利用される環境設定の方法です。

コンフィグを設定する方法
  • from_objectを使う方法
  • from_mappingを使う方法
  • from_envvarを使う方法
  • from_pyfileを使う方法
  • from_fileを使う方法

また、データベースを利用する際にセッションを利用するため、SECRET_KEYの設定が必要になります。

そのため、config設定したあとにapp.pyにインポートしcreate_appへ引数を持たせ、コンフィグを読み込ませます。

# Flaskのインポート
from flask import Flask
# SQLAlchemyのインポート
from flask_sqlalchemy import SQLAlchemy
# Migrateのインポート
from flask_migrate import Migrate
# configをインポート
from apps.config import config

# SQLAlchemyのインスタンス作成
db = SQLAlchemy()

def create_app(config_key):
    # Flaskのインスタンス作成
    app = Flask(__name__)

    # config_keyにマッチする環境のコンフィグを読み込む
    app.config.from_object(config[config_key])

    # SQLalchemyとアプリを連携
    db.init_app(app)
    # Migrateとアプリを連携
    Migrate(app, db)

    # authパッケージからviewsをインポート
    from apps.auth import views as auth_views

    # register_blueprintを利用してviewsのauthappをアプリへ登録
    app.register_blueprint(auth_views.auth, url_prefix="/auth")

    return app
app.py
  • configのインポート
  • create_appに引数config_keyを持たせる
  • config_keyにマッチする環境のコンフィグを読み込む
from apps.config import config

ppsディレクトリに存在するconfig.pyをインポートします。

app.config.from_object(config[config_key])

create_app関数にconfig_keyといった引数を持たせ、config.pyで作成した辞書から指定の値を読み込めるよう設定しています。

次に、環境設定ファイルである.envファイルに変更を追加します。

FLASK_APP = apps.app:create_app("local")
FLASK_DEBUG = 1

config.pyで作成したconfig辞書からlocalを読み込むように設定しています。

データベースの初期化とマイグレーション

次に、データベースの初期化とマイグレーションファイルを作成します。

マイグレーションファイルは、データベースの設計書のようなものです。

マイグレーションファイルを実行することで記述してきた内容がデータベースに反映されます。

データベースの初期化とマイグレーションを実施するflask dbコマンドは以下になります。

flask dbコマンド
  • flask db init
  • flask db migrate
  • flask db upgrade
  • flask db downgrade

以下のコマンド例は環境変数FLASK_APPを適切に設定している場合になります。

flask db init

flask db initコマンドは、データベースを初期化します。

ここでは、flask db initコマンドを実行するとappsディレクトリ配下にmigrationsディレクトリが作成されます。

(venv) PS C:\Users\sugir\Documents\flask-login> flask db init
Creating directory 'C:\\Users\\sugir\\Documents\\flask-login\\migrations' ...  done
Creating directory 'C:\\Users\\sugir\\Documents\\flask-login\\migrations\\versions' ...  done
Generating C:\Users\sugir\Documents\flask-login\migrations\alembic.ini ...  done
Generating C:\Users\sugir\Documents\flask-login\migrations\env.py ...  done
Generating C:\Users\sugir\Documents\flask-login\migrations\README ...  done
Generating C:\Users\sugir\Documents\flask-login\migrations\script.py.mako ...  done
Please edit configuration/connection/logging settings in 'C:\\Users\\sugir\\Documents\\flask-login\\migrations\\alembic.ini' before proceeding.

flask db migrate

flask db migrateコマンドは、データベースのマイグレーションファイルを作成します。

flask db migrateコマンドを実行すると、Model定義をもとにmigrations/versions配下にPythonファイルでデータベースに適用する前の情報が生成されます。

(venv) PS C:\Users\sugir\Documents\flask-login> flask db migrate
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'users'
INFO  [alembic.autogenerate.compare] Detected added index ''ix_users_email'' on '('email',)'
INFO  [alembic.autogenerate.compare] Detected added index ''ix_users_username'' on '('username',)'
Generating C:\Users\sugir\Documents\flask-login\migrations\versions\e488d28ab7fa_.py ...  done

flask db upgrade

flask db upgradeコマンドは、マイグレーション情報を実際にデータベースに反映します。

ここでは、flask db upgradeコマンドを実行するとusersテーブルが生成されます。

(venv) PS C:\Users\sugir\Documents\flask-login> flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> e488d28ab7fa, empty message

VSCodeでデータベースを確認する

VSCodeでデータベースの内容を確認できるように設定する方法です。

VSCodeの左サイドバーにある拡張機能アイコンをクリックし、検索バーで「SQLite」を検索します。

作成されたデータベースファイル(ここではlocal.sqlite)を右クリックし、「Open Database」を選択すると「SQLITE EXPLORER」が左下に表示されます。

データベースの作成と操作が実行できるようになりました。

データベースを使ったCRUD機能作成

ここでは、データベースを利用して以下の機能を作成していきます。

実際には、Create(ユーザー作成)とRead(読み込み)になります。

データベースを利用した各機能
  • サインアップ機能の作成
  • ログイン機能の作成
  • ログアウト機能の作成

また、上記の各機能を作る上で必要になるflask-loginをインストールします。

pip install flask-login
(venv) PS C:\Users\sugir\Documents\flask-login> pip list
Package           Version
----------------- -------
alembic           1.13.2
blinker           1.8.2
click             8.1.7
colorama          0.4.6
Flask             3.0.3
Flask-Login       0.6.3
Flask-Migrate     4.0.7
Flask-SQLAlchemy  3.1.1
greenlet          3.1.0
itsdangerous      2.2.0
Jinja2            3.1.4
Mako              1.3.5
MarkupSafe        2.1.5
pip               24.0
python-dotenv     1.0.1
SQLAlchemy        2.0.35
typing_extensions 4.12.2
Werkzeug          3.0.4

flask-loginのインストールが確認できました。

サインアップ機能の作成

サインアップ機能作成における主な手順は以下になります。

サインアップ機能作成の手順
  • flask-loginと連携
  • サインアップ機能のフォームクラス作成
  • Userモデルの更新
  • サインアップ機能のエンドポイント作成
  • サインアップ機能のテンプレート作成
  • 動作確認

flask-loginと連携

始めに、app.pyにてflask-loginをインポート及び連携を図ります。

# Flaskのインポート
from flask import Flask
# SQLAlchemyのインポート
from flask_sqlalchemy import SQLAlchemy
# Migrateのインポート
from flask_migrate import Migrate
# configをインポート
from apps.config import config
# flask-loginのLoginManagerクラスをインポート
from flask_login import LoginManager

# SQLAlchemyのインスタンス作成
db = SQLAlchemy()
# LoginManagerをインスタンス化する
login_manager = LoginManager()
# login_view属性に未ログイン時にリダイレクトするエンドポイントを指定
login_manager.login_view = "auth.signup"
# login_message属性にログイン後に表示するメッセージを指定
# ここでは何も表示しないよう空を指定
login_manager.login_message = ""

def create_app(config_key):
    # Flaskのインスタンス作成
    app = Flask(__name__)

    # config_keyにマッチする環境のコンフィグを読み込む
    app.config.from_object(config[config_key])

    # SQLalchemyとアプリを連携
    db.init_app(app)
    # Migrateとアプリを連携
    Migrate(app, db)
    # login_managerとアプリ連携
    login_manager.init_app(app)

    # authパッケージからviewsをインポート
    from apps.auth import views as auth_views

    # register_blueprintを利用してviewsのauthappをアプリへ登録
    app.register_blueprint(auth_views.auth, url_prefix="/auth")

    return app
app.py
  • flask-loginのLoginManagerクラスをインポート
  • LoginManagerをインスタンス化する
  • login_view属性に未ログイン時にリダイレクトするエンドポイントを指定
  • ここでは何も表示しないよう空を指定
  • login_managerとアプリ連携
from flask_login import LoginManager

インストールしたflask_loginからLoginManagerをインストールします。

login_manager = LoginManager()
login_manager.login_view = "auth.signup"
login_manager.login_message = ""

任意の変数へLoginManagerをインスタンス化します。

また、login_view属性に未ログイン時にリダイレクトするエンドポイントを指定します。

login_message属性は記述しない場合、デフォルトで英語のメッセージが出力されます。

そのため、何も表示しないように空を設定しています。

login_manager.init_app(app)

create_app関数内にて、login_managerをアプリ連携します。

サインアップ機能のフォームクラス作成

ここでは、サインアップ機能のフォームクラスを作成するため、forms.pyのファイル作成します。

また、フォームの拡張機能であるflask-wtfを利用します。

flask-wtfはバリデーションチェックやCSRF対策の機能を持ったフォーム作成できます。

フォーム作成時のメリットがあるため、インストールしておきましょう。

さらに、email属性のバリデータをチェックする際にはemail-validatorをインストールする必要があります。

こちらもインストールしておきましょう。

pip install flask-wtf
pip install email-validator
(venv) PS C:\Users\sugir\Documents\flask-login> pip list
Package           Version
----------------- -------
alembic           1.13.2
blinker           1.8.2
click             8.1.7
colorama          0.4.6
dnspython         2.6.1
email_validator   2.2.0
Flask             3.0.3
Flask-Login       0.6.3
Flask-Migrate     4.0.7
Flask-SQLAlchemy  3.1.1
Flask-WTF         1.2.1
greenlet          3.1.0
idna              3.10
itsdangerous      2.2.0
Jinja2            3.1.4
Mako              1.3.5
MarkupSafe        2.1.5
pip               24.0
python-dotenv     1.0.1
SQLAlchemy        2.0.35
typing_extensions 4.12.2
Werkzeug          3.0.4
WTForms           3.1.2

インストールの確認ができたら、ファイルを作成します。

flask-login
├── .env
└── apps
    ├── app.py
    ├── config.py
    └── auth
        ├── __init__.py
        ├── forms.py
        ├── models.py
        └── views.py
# flask_wtfからFlaskFormクラスをインポート
from flask_wtf import FlaskForm
# wtformsから各フィールドをインポート
from wtforms import PasswordField, StringField, SubmitField
# wtformsから各バリデータをインポート
from wtforms.validators import DataRequired, Email, Length

# サインアップフォームクラス作成
class SignUpForm(FlaskForm):
    username = StringField(
        "ユーザー名",
        validators = [
            DataRequired("ユーザー名は必須です。"),
            Length(1, 30, "30文字以内で入力してください。"),
        ],
    )
    email = StringField(
        "メールアドレス",
        validators = [
            DataRequired("メールアドレスは必須です。"),
            Email("メールアドレス形式で入力してください。"),
        ],
    )
    password = PasswordField(
        "パスワード",
        validators = [
            DataRequired("パスワードは必須です。"),
        ],
    )
    submit = SubmitField("Sign up")
forms.py
  • flask_wtfからFlaskFormクラスをインポート
  • wtformsから各フィールドをインポート
  • wtformsから各バリデータをインポート
  • サインアップフォームクラス作成
from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Email, Length

flask_wtfからFlaskFormをインポートします。

また、HTMLフォームのパスワード、テキスト、サブミットフィールドをインポートします。

さらに、HTMLフォームからサブミットされた際にバリデートするために必要なバリデータをインポートしています。

class SignUpForm(FlaskForm):
    username = StringField(
        "ユーザー名",
        validators = [
            DataRequired("ユーザー名は必須です。"),
            Length(1, 30, "30文字以内で入力してください。"),
        ],
    )
    email = StringField(
        "メールアドレス",
        validators = [
            DataRequired("メールアドレスは必須です。"),
            Email("メールアドレス形式で入力してください。"),
        ],
    )
    password = PasswordField(
        "パスワード",
        validators = [
            DataRequired("パスワードは必須です。"),
        ],
    )
    submit = SubmitField("Sign up")

FlaskFormを継承したサインアップフォームクラスを作成します。

また、ユーザー名、メールアドレス、パスワードをDataRequiredにて必須項目としています。

ユーザー名は30文字制限、メールアドレスは形式制限してます。

Userモデルの更新

model.pyのコード追加を実施します。

ここでは、Userモデルをログイン機能で利用するために更新します。

flask-loginは、UserMixinクラスが用意されています。

UserモデルにUserMixinを継承することで、以下のプロパティ/メソッドを定義せず利用できるメリットがあります。

スクロールできます
プロパティ/メソッド説明
is_authenticatedログイン時はtrue,、未ログイン時はfalseを返す関数
is_activeユーザーがアクティブ時はtrue、非アクティブ時はfalseを返す関数
is_anonymousログインユーザーはfalse、匿名ユーザーはtrueを返す関数
get_idログインユーザーのユニークIDを取得するプロパティ
flask-loginのUserMixinクラスのプロパティ/関数
# login_managerのインポート
from apps.app import db, login_manager
# flask-loginからUserMixinクラスをインポート
from flask_login import UserMixin
# check_password_hashを追加インポート
from werkzeug.security import generate_password_hash, check_password_hash

# db.Model, UserMixinを継承したUserクラス作成
class User(db.Model, UserMixin):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String, index=True)
    email = db.Column(db.String, unique=True, index=True)
    password_hash = db.Column(db.String)

    @property
    def password(self):
        raise AttributeError("読み取り不可")
    
    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    # パスワードチェックする
    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)
    
    # メールアドレス重複チェックする
    def is_duplicate_email(self):
        return User.query.filter_by(email=self.email).first() is not None
    
# ログインユーザー情報を取得する関数作成
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(user_id)
models.py
  • login_managerのインポート
  • flask-loginからUserMixinクラスをインポート
  • check_password_hashを追加インポート
  • パスワードチェック用関数作成
  • メールアドレス重複チェック用関数作成
  • ログインユーザー情報を取得する関数作成
from apps.app import db, login_manager

apps.appからlogin_managerを追加インポートします。

from werkzeug.security import generate_password_hash, check_password_hash

werkzeug.securityから入力されたパスワードが正しいかチェックするために、check_password_hashを追加インポートします。

class User(db.Model, UserMixin):

flask-loginで用意されているUserMixinクラスを利用するため、Userクラスに継承させます。

def verify_password(self, password):
    return check_password_hash(self.password_hash, password)
    
def is_duplicate_email(self):
    return User.query.filter_by(email=self.email).first() is not None

パスワードをチェックするverify_password関数を作成し、入力されたパスワードがDBのハッシュ化されたパスワードと一致してるかチェックします。

一致する場合はtrueを返し、一致しない場合はfalseを返します。

また、メールアドレス重複をチェックするis_duplicate_email関数を作成します。

DBに同一のメールアドレスを持つレコードがある場合はtrueを返し、レコードがない場合はfalseを返します。

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(user_id)

ログインしているユーザー情報を取得するload_user関数を作成し、@login_manager.user_loaderといったデコレータを付与しています。

load_user関数はflask_loginがログインユーザー情報を取得し利用するため、ユーザーのユニークIDを引数としDBから特定ユーザーを取得し返す必要があります。

サインアップ機能のエンドポイント作成

認証機能の作成が完了したので、サインアップ機能のエンドポイントを作成します。

from apps.app import db
from apps.auth.forms import SignUpForm
from apps.auth.models import User
from flask import Blueprint, render_template, flash, url_for, redirect, request
from flask_login import login_user, login_required

auth = Blueprint(
    "auth",
    __name__,
    template_folder = "templates",
    static_folder = "static",
)

@auth.route("/")
def index():
    # TOPページへのアクセスはサインアップページに遷移させる
    return redirect(url_for("auth.signup"))

@auth.route("/auth_ok")
@login_required
def auth_ok():
    return render_template("index.html")

# signupエンドポイントを作成する
@auth.route("/signup", methods=["GET", "POST"])
def signup():
    # SignUpFormをインスタンス化
    signform = SignUpForm()
    if signform.validate_on_submit():
        user = User(
            username = signform.username.data,
            email = signform.email.data,
            password = signform.password.data,
        )
        # メールアドレス重複チェック
        if user.is_duplicate_email():
            flash("指定のメールアドレスは登録済みです")
            return redirect(url_for("auth.signup"))
        # ユーザー情報を登録する
        db.session.add(user)
        db.session.commit()
        # ユーザー情報をセッションに格納
        login_user(user)
        # GETパラメータにnextキーが存在し値がない場合にToDo一覧へ
        next_ = request.args.get("next")
        if next_ is None or not next_.startswith("/"):
            next_ = url_for("auth.auth_ok")
        return redirect(next_)
    
    return render_template("auth/signup.html", form=signform)
views.py
  • dbのインポート
  • SignUpFormのインポート
  • render_template, flash, url_for, redirect, requestのインポート
  • login_user, login_requiredのインポート
  • signupエンドポイント作成
  • SignUpFormのインスタンス作成
  • メールアドレス重複チェック
  • ユーザー情報をデータベースへ登録
  • ユーザー情報をセッションに格納
  • nextキーによるリダイレクト
from apps.app import db
from apps.auth.forms import SignUpForm
from apps.auth.models import User
from flask import Blueprint, render_template, flash, url_for, redirect, request
from flask_login import login_user, login_required

app.pyからdbをインポートしています。

form.pyで作成したSignUpFormクラスをインポートします。

models.pyで作成したUserクラスをインポートします。

flaskからそれぞれrender_template、flash、url_for、redirect、requestをインポートします。

flask_loginからlogin_userとlogin_requiredをインポートします。

@auth.route("/signup", methods=["GET", "POST"])
def signup():
    signform = SignUpForm()
    if signform.validate_on_submit():
        user = User(
            username = signform.username.data,
            email = signform.email.data,
            password = signform.password.data,
        )
        if user.is_duplicate_email():
            flash("指定のメールアドレスは登録済みです")
            return redirect(url_for("auth.signup"))
        db.session.add(user)
        db.session.commit()
        login_user(user)
        next_ = request.args.get("next")
        if next_ is None or not next_.startswith("/"):
            next_ = url_for("auth.auth_ok")
        return redirect(next_)
    
    return render_template("auth/signup.html", form=signform)

デコレータにてsignupエンドポイントを作成し、methodsにGETとPOSTを指定します。

signup関数では、SignUpformクラスをインスタンス化します。

もし、サブミットされた場合はフォーム内容をバリデートチェックします。

フォームデータが通過すれば、ユーザークラスを生成し、変数に入力された各データを格納します。

次に、メールアドレス重複チェックを実施しています。

バリデートチェックとメールアドレス重複チェックを通過すれば、DBにユーザーを登録します。

@auth.route("/auth_ok")
@login_required
def auth_ok():
    return render_template("index.html")

ユーザー認証を確認するため、上記のエンドポイントを作成しています。

@login_requiredといったデコレータ関数を追加することで、該当のエンドポイントはログインしないとアクセスできない仕様にできます。

サインアップ機能のテンプレート作成

次に、サインアップ機能を反映させたテンプレートを作成します。

また、apps/staticにはCSSフレームワークであるBootStrapを採用しています。

以下、BootStrapのダウンロードサイトになります。

flask-login
├── .env
└── apps
    ├── app.py
    ├── config.py
    ├── static
    │   └── bootstrap.min.css
    └── auth
        ├── __init__.py
        ├── forms.py
        ├── models.py
        ├── static/css
        │   └── style.css
        ├── templates
        │   └── auth
        │       ├── base.html
        │       ├── index.html
        │       └── signup.html
        └── views.py

テンプレート作成では、共通テンプレートとしてbase.htmlから作成します。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>AuthApp</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
    <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}" />
    <link rel="stylesheet" href="{{ url_for('auth.static', filename='css/style.css') }}" />
  </head>
  <body>
    <nav class="navbar navbar-dark bg-dark">
      <div class="container-fluid">
        <ul class="navbar-nav flex-row">
          <li class="nav-item mt-1">
            <a class="navbar-brand" href="{{ url_for('auth.auth_ok') }}">AuthApp</a>
          </li>
        </ul>
        {% if current_user.is_authenticated %}
        <ul class="navbar-nav flex-row">
          <li class="nav-item pe-3">
            <span class="nav-link">{{ current_user.username }}</span>
          </li>
          <li class="nav-item pe-2">
            <a href="{{ url_for('auth.logout') }}" class="nav-link">Logout</a>
          </li>
        </ul>
        {% else %}
        <ul class="navbar-nav flex-row">
          <li class="nav-item pe-3">
            <a href="{{ url_for('auth.signup') }}" class="nav-link">Sign up</a>
          </li>
          <li class="nav-item pe-2">
            <a href="{{ url_for('auth.login') }}" class="nav-link">Login</a>
          </li>
        </ul>
        {% endif %}
      </div>
    </nav>
    {% block content %}{% endblock %}
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
  </body>
</html>

以下、signup.htmlとindex.htmlはbase.htmlを継承しています。

{% extends "auth/base.html" %}
{% block content %}
<div class="container">
  <div class="card card-container">
    <img id="profile-img" class="profile-img-card" src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" />
    <p id="profile-name" class="profile-name-card"></p>
    <form action="{{ url_for('auth.signup', next=request.args.get(next)) }}" class="form-signin" method="POST" novalidate="novalidate">
      {% for message in get_flashed_messages() %}
      <p style="color: red";>{{ message }}</p>
      {% endfor %}
      {{ form.hidden_tag() }}
      {% for error in form.username.errors %}
      <span style="color: red;">{{ error }}</span>
      {% endfor %}
      {{ form.username(class="form-control", placeholder="User name") }}
      {% for error in form.email.errors %}
      <span style="color: red;">{{ error }}</span>
      {% endfor %}
      {{ form.email(class="form-control", placeholder="Email address") }}
      {% for error in form.password.errors %}
      <span style="color: red;">{{ error }}</span>
      {% endfor %}
      {{ form.password(class="form-control", placeholder="Password") }}
      {{ form.submit(class="btn btn-lg btn-primary btn-block btn-signin") }}
    </form>
  </div>
</div>
{% endblock %}
{% extends "auth/base.html" %}
{% block content %}
<h1>ユーザー認証完了ページ</h1>
{% endblock %}

BootStrap以外にも、style.cssを記述しています。

.card-container.card {
    max-width: 350px;
    padding: 40px 40px;
}

/*
 * Card component
 */
 .card {
    background-color: #F7F7F7;
    /* just in case there no content*/
    padding: 20px 25px 30px;
    margin: 0 auto 25px;
    margin-top: 50px;
    /* shadows and rounded borders */
    -moz-border-radius: 2px;
    -webkit-border-radius: 2px;
    border-radius: 2px;
    -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
    -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
    box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}

.profile-img-card {
    width: 96px;
    height: 96px;
    margin: 0 auto 10px;
    display: block;
    -moz-border-radius: 50%;
    -webkit-border-radius: 50%;
    border-radius: 50%;
}

/*
 * Form styles
 */
.profile-name-card {
    font-size: 16px;
    font-weight: bold;
    text-align: center;
    margin: 10px 0 0;
    min-height: 1em;
}

.form-signin input,
.form-signin button {
    width: 100%;
    display: block;
    margin-bottom: 10px;
    z-index: 1;
    position: relative;
    -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
}

.form-signin .form-control:focus {
    border-color: rgb(104, 145, 162);
    outline: 0;
    -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgb(104, 145, 162);
    box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgb(104, 145, 162);
}

.btn.btn-signin,
.btn.btn-login {
    background-color: rgb(104, 145, 162);
    padding: 0px;
    font-weight: 700;
    font-size: 14px;
    height: 36px;
    -moz-border-radius: 3px;
    -webkit-border-radius: 3px;
    border-radius: 3px;
    border: none;
    -o-transition: all 0.218s;
    -moz-transition: all 0.218s;
    -webkit-transition: all 0.218s;
    transition: all 0.218s;
}

.btn.btn-signin:hover,
.btn.btn-signin:active,
.btn.btn-signin:focus {
    background-color: #d63384;
}

.btn.btn-login:hover,
.btn.btn-login:active,
.btn.btn-login:focus {
    background-color: #0d6efd;
}

動作確認

サインアップしていない(ログインしていない)状態だと、以下のように左上のAuthAppといったナビリンクを押下してもページ遷移できません。

また、何も入力していない状態でサインアップしてもバリデーションチェックによってエラーメッセージを表示しています。

サインアップ後は、ログイン状態になりログイン認証後のページに遷移しています。

ログイン機能の作成

ログイン機能作成における主な手順は以下になります。

ログイン機能作成の手順
  • ログイン機能のフォームクラス作成
  • ログイン機能のエンドポイント作成
  • ログイン機能のテンプレート作成
  • 動作確認

ログイン機能のフォームクラス作成

すでにサインアップ機能のフォームクラスを作成したため、forms.pyにログイン機能のコードを追加します。

# flask_wtfからFlaskFormクラスをインポート
from flask_wtf import FlaskForm
# wtformsから各フィールドをインポート
from wtforms import PasswordField, StringField, SubmitField
# wtformsから各バリデータをインポート
from wtforms.validators import DataRequired, Email, Length

# サインアップフォームクラス作成
class SignUpForm(FlaskForm):
    username = StringField(
        "ユーザー名",
        validators = [
            DataRequired("ユーザー名は必須です。"),
            Length(1, 30, "30文字以内で入力してください。"),
        ],
    )
    email = StringField(
        "メールアドレス",
        validators = [
            DataRequired("メールアドレスは必須です。"),
            Email("メールアドレス形式で入力してください。"),
        ],
    )
    password = PasswordField(
        "パスワード",
        validators = [
            DataRequired("パスワードは必須です。"),
        ],
    )
    submit = SubmitField("Sign up")

# ログインフォームクラス作成
class LoginForm(FlaskForm):
    email = StringField(
        "メールアドレス",
        validators = [
            DataRequired("メールアドレスは必須です。"),
            Email("メールアドレス形式で入力してください。"),
        ],
    )
    password = PasswordField(
        "パスワード",
        validators = [
            DataRequired("パスワードは必須です。"),
        ],
    )
    submit = SubmitField("Login")
forms.py
  • ログインフォームクラス作成
class LoginForm(FlaskForm):
    email = StringField(
        "メールアドレス",
        validators = [
            DataRequired("メールアドレスは必須です。"),
            Email("メールアドレス形式で入力してください。"),
        ],
    )
    password = PasswordField(
        "パスワード",
        validators = [
            DataRequired("パスワードは必須です。"),
        ],
    )
    submit = SubmitField("Login")

モデルの定義にてメールアドレスをユニークキーに指定してるため、メールアドレスとパスワードを入力してログイン可能にしています。

ログイン機能のエンドポイント作成

ログイン機能のエンドポイントを作成します。

from apps.app import db
from apps.auth.forms import SignUpForm, LoginForm
from apps.auth.models import User
from flask import Blueprint, render_template, flash, url_for, redirect, request
from flask_login import login_user, login_required

auth = Blueprint(
    "auth",
    __name__,
    template_folder = "templates",
    static_folder = "static",
)

@auth.route("/")
def index():
    # TOPページへのアクセスはサインアップページに遷移させる
    return redirect(url_for("auth.signup"))

@auth.route("/auth_ok")
@login_required
def auth_ok():
    return render_template("index.html")

# signupエンドポイントを作成する
@auth.route("/signup", methods=["GET", "POST"])
def signup():
    # SignUpFormをインスタンス化
    signform = SignUpForm()
    if signform.validate_on_submit():
        user = User(
            username = signform.username.data,
            email = signform.email.data,
            password = signform.password.data,
        )
        # メールアドレス重複チェック
        if user.is_duplicate_email():
            flash("指定のメールアドレスは登録済みです")
            return redirect(url_for("auth.signup"))
        # ユーザー情報を登録する
        db.session.add(user)
        db.session.commit()
        # ユーザー情報をセッションに格納
        login_user(user)
        # GETパラメータにnextキーが存在し値がない場合にToDo一覧へ
        next_ = request.args.get("next")
        if next_ is None or not next_.startswith("/"):
            next_ = url_for("auth.auth_ok")
        return redirect(next_)
    
    return render_template("auth/signup.html", form=signform)

# loginエンドポイントを作成する
@auth.route("/login", methods=["GET", "POST"])
def login():
    loginform = LoginForm()
    if loginform.validate_on_submit():
        # メールアドレスからユーザー取得
        user = User.query.filter_by(email=loginform.email.data).first()
        # ユーザーが存在しパスワード一致ならログイン許可
        if user is not None and user.verify_password(loginform.password.data):
            login_user(user)
            return redirect(url_for("auth.auth_ok"))
        # ログイン失敗メッセージを設定する
        flash("メールアドレスかパスワードが不正です")
        
    return render_template("auth/login.html", form=loginform)
views.py
  • LoginFormのインポート
  • loginエンドポイント作成
  • LoginFormのインスタンス作成
  • メールアドレスからユーザー取得
  • ユーザーが存在しパスワード一致ならログイン許可
  • ログイン失敗メッセージを設定する
@auth.route("/login", methods=["GET", "POST"])
def login():
    loginform = LoginForm()
    if loginform.validate_on_submit():
        # メールアドレスからユーザー取得
        user = User.query.filter_by(email=loginform.email.data).first()
        # ユーザーが存在しパスワード一致ならログイン許可
        if user is not None and user.verify_password(loginform.password.data):
            login_user(user)
            return redirect(url_for("auth.auth_ok"))
        # ログイン失敗メッセージを設定する
        flash("メールアドレスかパスワードが不正です")
        
    return render_template("auth/login.html", form=loginform)

デコレータにてloginエンドポイントを作成し、methodsにGETとPOSTを指定します。

login関数では、LoginFormクラスをインスタンス化しサブミット時のバリデートチェックを実施します。

サインアップと違い、すでに登録されたユーザー照合になるため、DBからユーザーを取得しています。

ユーザーは存在しており、かつパスワード一致すれば通過しログイン/リダイレクトされます。

ログイン機能のテンプレート作成

次に、ログイン機能を反映させたテンプレートを作成します。

flask-login
├── .env
└── apps
    ├── app.py
    ├── config.py
    ├── static
    │   └── bootstrap.min.css
    └── auth
        ├── __init__.py
        ├── forms.py
        ├── models.py
        ├── static/css
        │   └── style.css
        ├── templates
        │   └── auth
        │       ├── base.html
        │       ├── index.html
        │       ├── login.html
        │       └── signup.html
        └── views.py

login.htmlを作成します。

{% extends "auth/base.html" %}
{% block content %}
<div class="container">
  <div class="card card-container">
    <img id="profile-img" class="profile-img-card" src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" />
    <p id="profile-name" class="profile-name-card"></p>
    <form action="{{ url_for('auth.login') }}" class="form-signin" method="POST" novalidate="novalidate">
      {% for message in get_flashed_messages() %}
      <p style="color: red";>{{ message }}</p>
      {% endfor %}
      {{ form.hidden_tag() }}
      {% for error in form.email.errors %}
      <span style="color: red;">{{ error }}</span>
      {% endfor %}
      {{ form.email(class="form-control", placeholder="Email address") }}
      {% for error in form.password.errors %}
      <span style="color: red;">{{ error }}</span>
      {% endfor %}
      {{ form.password(class="form-control", placeholder="Password") }}
      {{ form.submit(class="btn btn-lg btn-primary btn-block btn-login") }}
    </form>
  </div>
</div>
{% endblock %}

動作確認

サインアップ機能の動作確認でログイン状態になっているため、ブラウザをシークレットウィンドウで開き確認します。

右上のログインボタンを押下すると、以下のようにログイン認証ページに遷移します。

メールアドレスあるいはパスワードを間違えてログイン認証すると、以下のメッセージが表示されます。

ログアウト機能の作成

ログアウト機能作成における主な手順は以下になります。

ログアウト機能作成の手順
  • ログアウト機能のエンドポイント作成
  • 動作確認

ログアウト機能のエンドポイント作成

ログアウト機能のエンドポイントを作成します。

from apps.app import db
from apps.auth.forms import SignUpForm, LoginForm
from apps.auth.models import User
from flask import Blueprint, render_template, flash, url_for, redirect, request
from flask_login import login_user, login_required, logout_user

auth = Blueprint(
    "auth",
    __name__,
    template_folder = "templates",
    static_folder = "static",
)

@auth.route("/")
def index():
    # TOPページへのアクセスはサインアップページに遷移させる
    return redirect(url_for("auth.signup"))

@auth.route("/auth_ok")
@login_required
def auth_ok():
    return render_template("auth/index.html")

# signupエンドポイントを作成する
@auth.route("/signup", methods=["GET", "POST"])
def signup():
    # SignUpFormをインスタンス化
    signform = SignUpForm()
    if signform.validate_on_submit():
        user = User(
            username = signform.username.data,
            email = signform.email.data,
            password = signform.password.data,
        )
        # メールアドレス重複チェック
        if user.is_duplicate_email():
            flash("指定のメールアドレスは登録済みです")
            return redirect(url_for("auth.signup"))
        # ユーザー情報を登録する
        db.session.add(user)
        db.session.commit()
        # ユーザー情報をセッションに格納
        login_user(user)
        # GETパラメータにnextキーが存在し値がない場合にToDo一覧へ
        next_ = request.args.get("next")
        if next_ is None or not next_.startswith("/"):
            next_ = url_for("auth.auth_ok")
        return redirect(next_)
    
    return render_template("auth/signup.html", form=signform)

# loginエンドポイントを作成する
@auth.route("/login", methods=["GET", "POST"])
def login():
    loginform = LoginForm()
    if loginform.validate_on_submit():
        # メールアドレスからユーザー取得
        user = User.query.filter_by(email=loginform.email.data).first()
        # ユーザーが存在しパスワード一致ならログイン許可
        if user is not None and user.verify_password(loginform.password.data):
            login_user(user)
            return redirect(url_for("auth.auth_ok"))
        # ログイン失敗メッセージを設定する
        flash("メールアドレスかパスワードが不正です")
        
    return render_template("auth/login.html", form=loginform)

# logoutエンドポイントを作成する
@auth.route("/logout")
def logout():
    logout_user()
    return redirect(url_for("auth.login"))
views.py
  • logout_userのインポート
  • logoutエンドポイント作成
from flask_login import login_user, login_required, logout_user

flask_loginからlogout_userを追加インポートします。

@auth.route("/logout")
def logout():
    logout_user()
    return redirect(url_for("auth.login"))

デコレータにてlogoutエンドポイントを作成しています。

logout関数では、logout_user関数を呼び出すだけでログインセッションをリセットできます。

最終的にリダイレクトにてログイン画面にページ遷移します。

動作確認

以下はログイン認証後の画面になります。

右上のログアウトボタンを押下すると、ログイン認証画面にページ遷移します。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

sugiのアバター sugi SUGI

【経歴】玉川大学工学部卒業→新卒SIer企業入社→2年半後に独立→プログラミングスクール運営/受託案件→フリーランスエンジニア&SEOコンサル→Python特化のコンテンツサイトJob Code運営中

コメント

コメントする

目次