• トップ
  • ブログ一覧
  • 【第3回】FastAPIチュートリアル: toDoアプリを作ってみよう【認証・ユーザ登録編】
  • 【第3回】FastAPIチュートリアル: toDoアプリを作ってみよう【認証・ユーザ登録編】

    広告メディア事業部広告メディア事業部
    2019.11.18

    IT技術

    FastAPIチュートリアル: toDoアプリを作ってみよう~第3回~

    前回の記事「【第2回】FastAPIチュートリアル: ToDoアプリを作ってみよう【モデル構築編】」の続きです。

    今回も、ToDoアプリを作る過程で、FastAPIの使い方を学んでいきましょう!

    第1回はこちら

    featureImg2019.11.14【第1回】FastAPIチュートリアル: ToDoアプリを作ってみよう【環境構築編】FastAPIチュートリアル: toDoアプリを作ってみるFastAPI は、最近注目を集めている WebAPIフレー...

    認証

    FastAPIでは、HTTP BasicAuth(いわゆるベーシック認証)とOAuth2(オーオース)と呼ばれる認証をサポートしています。

    本アプリケーションは、あまり凝ったアプリケーションでもないので、ベーシック認証を利用することにします。

    今の時代、「ログインフォームなどを作成してログイン」する形のほうが一般的ではありますが、今回はご容赦ください。

    さて、FastAPIでベーシック認証を使用するにあたり、下準備がいくつかあります。

    新たにimportするモジュール

    controller.py に、以下のモジュールを新しくインポートしてください。

    (重複しているものは、適宜無視してください)

    1from fastapi import FastAPI, Depends, HTTPException  # new
    2from fastapi.security import HTTPBasic, HTTPBasicCredentials  # new
    3
    4from starlette.templating import Jinja2Templates
    5from starlette.requests import Request
    6from starlette.status import HTTP_401_UNAUTHORIZED  # new
    7
    8import db  # new
    9from models import User, Task  # new
    10
    11import hashlib  # new

    上から2つは、FastAPIでベーシック認証を使うのに必要なクラスです。

    後半の新しく追加したものは、入力された「ユーザ名」や「パスワード」がデータベースにあるものと一致するかを検証するのに使います。

    管理者ページのコントローラを修正する

    次に、管理者ページのビューを表示する前のコントローラadmin() を修正していきます。

    というより、ほとんど変更しますので、上からコピペでも構いません。

    1def admin(request: Request, credentials: HTTPBasicCredentials = Depends(security)):
    2    # Basic認証で受け取った情報
    3    username = credentials.username
    4    password = hashlib.md5(credentials.password.encode()).hexdigest()
    5
    6    # データベースからユーザ名が一致するデータを取得
    7    user = db.session.query(User).filter(User.username == username).first()
    8    task = db.session.query(Task).filter(Task.user_id == user.id).all() if user is not None else []
    9    db.session.close()
    10
    11    # 該当ユーザがいない場合
    12    if user is None or user.password != password:
    13        error = 'ユーザ名かパスワードが間違っています'
    14        raise HTTPException(
    15            status_code=HTTP_401_UNAUTHORIZED,
    16            detail=error,
    17            headers={"WWW-Authenticate": "Basic"},
    18        )
    19
    20    # 特に問題がなければ管理者ページへ
    21    return templates.TemplateResponse('admin.html',
    22                                      {'request': request,
    23                                       'user': user,
    24                                       'task': task})

    各処理の詳細はコメントで書いたので、読んでいただければ理解できるかと思います。

    SQLAlchemyでは、filter() でSQL構文でいう「WHERE」を扱うことができます。

    また、first() で該当クエリを上から1つ、all() ですべてをリスト化して取得できます。

    いずれもなければ、None が返ってきます。

    確認

    では、動作確認をしてみましょう!

    Basic認証画面

    うまく動作していそうです!

    わざと間違えると、簡単なエラーが返ってくることも確認できました。

    ユーザをWeb上で登録できるようにする

    現状、ユーザを登録するには、Pythonコードから一つ一つデータベースにコミットしなければなりません。

    このままでは、誰もが使えるアプリケーションではないので、Web上からユーザを新たに登録できるようにします。

    ルーティング

    そうと決まれば新たに「register」という名前で『URL』『コントローラ』『ビュー』を作りましょう。

    1# urls.py
    2# FastAPIのルーティング用関数
    3app.add_api_route('/', index)
    4app.add_api_route('/admin', admin)
    5app.add_api_route('/register', register, methods=['GET', 'POST'])  # new

    今回は、「GETメソッド」と「POSTメソッド」で処理を分けるので、上記のように引数で指定してあげます。

    とりあえず、コントローラの処理は後回しにします。

    1# controllers.py
    2def register(request: Request):
    3    if request.method == 'GET':
    4        pass
    5
    6    if request.method == 'POST':
    7        pass

    ビューも「templates/resiter.html」というファイルを作っておきましょう。

    コントローラのGETメソッド処理を書く

    つぎに、GETメソッドで受け取ったときの処理を書きます。

    しかし、ここでは「単にビューを返すだけ」なので特に面白いことはありません…。

    1if request.method is 'GET':
    2        return templates.TemplateResponse('register.html',
    3                                          {'request': request,
    4                                           'username': '',
    5                                           'error': []})

    これだけです。

    ビューを作る

    次は、「ビュー」を作りましょう。

    必要なのは、データベースに登録するための情報だけなので、それに見合ったフォームを書けばよいだけです!

    今回は、「利用規約に同意するチェックボックス」や、「メール認証」などは今回は省きます。

    (そんなに難しい実装でもないので、余力のある人は是非実装してみてください)

    1{% extends "layout.html" %}
    2{% block content %}
    3<br>
    4<div class="row">
    5    <div class="col-md-10">
    6        <h1>User Registration</h1>
    7    </div>
    8    <div class="col-md-2">
    9        <a href="/admin" class="btn btn-primary">ログイン</a>
    10    </div>
    11</div>
    12
    13<hr>
    14<form action="/register" method="post">
    15    <br>
    16    <p>
    17        ユーザ名<br>
    18        <input type="text" size="30" maxlength="20" name="username" value="{{username}}" placeholder="4文字以上20文字以下の半角英数字">
    19    </p>
    20    <br>
    21    <p>
    22        パスワード<br>
    23        <input type="password" size="30" maxlength="20" name="password" placeholder="6文字以上20文字以下の半角英数字">
    24    </p>
    25    <br>
    26    <p>
    27        パスワード (確認用)<br>
    28        <input type="password" size="30" maxlength="20" name="password_tmp" placeholder="6文字以上20文字以下の半角英数字">
    29    </p>
    30    <br>
    31    <p>
    32        メールアドレス<br>
    33        <input type="email" size="40" maxlength="60" name="mail" placeholder="sample@example.com">
    34    </p>
    35    <br><br>
    36    <button type="submit" class="btn btn-primary">登録</button>
    37</form>
    38
    39<p class="text-danger">{% for e in error %}{{ e }}<br>{% endfor %}</p>
    40
    41{% endblock %}

    後々のことを考えて、エラー表示する部分も実装しています。

    登録フォームのビュー

    コントローラのPOSTメソッドを書く前に...

    ここからが本題です。

    先ほど作ったフォームから、POSTとしてデータが渡されました。

    これらのデータは、非同期処理として処理されるため、コントローラの関数の先頭にasync をつけます

    そして、フォームデータの受け取り方は以下のように行います。

    1async def register(request: Request):
    2    if request.method == 'GET':
    3        return templates.TemplateResponse('register.html',
    4                                          {'request': request})
    5
    6    if request.method == 'POST':
    7        # POSTデータ
    8        data = await request.form()
    9        username = data.get('username')
    10        password = data.get('password')
    11        password_tmp = data.get('password_tmp')
    12        mail = data.get('mail')

    このように、starletteのRequestクラスのform() 関数を使うことで実現できます。

    今回は、GETとPOSTの処理を同じURLで兼任しましたが、もしURL(コントローラ)を分ける場合、FastAPIでサポートされているForm() という関数を利用しても実現可能なようです。

    そちらが気になる方は、以下の公式ドキュメントを参照してみてください。

    【Form Data - FastAPI】
    http://fastapi.tiangolo.com/tutorial/request-forms/

    POSTメソッド処理を書く

    下準備はできたので、実装していきましょう!

    まずは、入力されたデータが「正しい」か「否」かを判定します。

    そのために、正規表現を使ってみましょう。

    新しくcontrollers.pyに、以下の「re」というモジュールをインポートし、各パターンを作ります

    コードを見た方が早いですね。

    1# controllers.py
    2import re  # new
    3pattern = re.compile(r'\w{4,20}')  # 任意の4~20の英数字を示す正規表現
    4pattern_pw = re.compile(r'\w{6,20}')  # 任意の6~20の英数字を示す正規表現
    5pattern_mail = re.compile(r'^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$')  # e-mailの正規表現

    文字列の先頭にr がついているのは、バックスラッシュなどをエスケープしなくても良いようにするためです。

    そうしたら、POSTメソッドの処理を書きましょう!

    1async def register(request: Request):
    2    if request.method == 'GET':
    3        return templates.TemplateResponse('register.html',
    4                                          {'request': request,
    5                                           'username': '',
    6                                           'error': []})
    7
    8    if request.method == 'POST':
    9        # POSTデータ
    10        data = await request.form()
    11        username = data.get('username')
    12        password = data.get('password')
    13        password_tmp = data.get('password_tmp')
    14        mail = data.get('mail')
    15
    16        # new ここから
    17        error = []
    18
    19        tmp_user = db.session.query(User).filter(User.username == username).first()
    20
    21        # 怒涛のエラー処理
    22        if tmp_user is not None:
    23            error.append('同じユーザ名のユーザが存在します。')
    24        if password != password_tmp:
    25            error.append('入力したパスワードが一致しません。')
    26        if pattern.match(username) is None:
    27            error.append('ユーザ名は4~20文字の半角英数字にしてください。')
    28        if pattern_pw.match(password) is None:
    29            error.append('パスワードは6~20文字の半角英数字にしてください。')
    30        if pattern_mail.match(mail) is None:
    31            error.append('正しくメールアドレスを入力してください。')
    32
    33        # エラーがあれば登録ページへ戻す
    34        if error:
    35            return templates.TemplateResponse('register.html',
    36                                              {'request': request,
    37                                               'username': username,
    38                                               'error': error})
    39
    40        # 問題がなければユーザ登録
    41        user = User(username, password, mail)
    42        db.session.add(user)
    43        db.session.commit()
    44        db.session.close()
    45
    46        return templates.TemplateResponse('complete.html',
    47                                          {'request': request,
    48                                           'username': username})

    コードは大したことをやっていないので、読んでいただくと処理が理解できるかと思います!

    登録完了のビューをつくる

    最後に、「登録完了の画面」を作成しましょう。

    これも、そんなに凝ったものでなくて大丈夫ですが、そのままスムーズにログインに移行できるものが良いですね。

    1{% extends "layout.html" %}
    2{% block content %}
    3<br>
    4<div class="row">
    5    <div class="col-md-10">
    6        <h1>Complete your registration!</h1>
    7    </div>
    8    <div class="col-md-2">
    9        <a href="/" class="btn btn-primary">トップページへ</a>
    10    </div>
    11</div>
    12
    13<hr>
    14<p>{{ username }}さん。toDo アプリケーションへようこそ。</p>
    15<p>早速ログインしてみましょう!</p>
    16<br>
    17<p><a href="/admin" class="btn btn-primary btn-lg">ログイン</a></p>
    18
    19
    20{% endblock %}

    確認

    適当にユーザ登録してみる
    登録完了

    第3回:さいごに

    ここで第3回は、終わりです。

    今回は、「認証」と「ユーザ登録」について実装をしました。

    FastAPIの場合での、シンプルなベーシック認証についてご理解いただけたかと思います。

    次回は、管理者ページ(ユーザページ)をもっとToDoアプリケーションらしくしていきます!

    第4回の記事はこちら

    featureImg2019.12.03【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】FastAPIチュートリアル: toDoアプリを作ってみよう~第4回~前回の記事「【第3回】FastAPIチュートリア...

    ResponderとFastAPIを実際に使って比較してみた

    featureImg2020.01.10ResponderとFastAPIを実際に使って比較してみたResponderとFastAPIを比較したい!Webアプリケーションといえば、PHPの「CakePHP」、Pytho...

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...

    featureImg2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...

    広告メディア事業部

    広告メディア事業部

    おすすめ記事