• トップ
  • ブログ一覧
  • 【第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...

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    メディアチーム
    メディアチーム
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background