データベースではオーバースペックになってしまい、負荷に応じたスケールまで考えるとRDSではコストが見合わない時、DynamoDBが選択肢の一つになると思います。ここでは、DynamoDBで管理しているユーザを利用したFlask-Loginによる認証処理のサンプルをまとめます。

DynamoDB

テーブルと項目

ユーザ管理用のテーブルを作成し、ユーザIDに相当する id をパーティションキーとします。その後、テストで利用するために以下の属性を持つ項目を作成しておきます。なお、パスワードは何かしらの規則に則ってハッシュ化した文字列を入れるべきですが、ここでは平文で入れてしまいます。

  • id : Partition Key, String
  • name : String
  • password : String

IAMユーザ

作成したテーブルを操作できるIAMユーザを作成します。

boto3

PythonからDynamoDBを操作するためにboto3をインストールし、作成したIAMユーザでアクセスできるように準備します。

Approach

Flask-Loginは、公式サイトに以下のように明記されているとおり、ユーザ管理方法には干渉せず、ユーザをロードする方法も利用者側で実装する必要があります。

However, it does not:

  • Impose a particular database or other storage method on you. You are entirely in charge of how the user is loaded.
  • Restrict you to using usernames and passwords, OpenIDs, or any other method of authenticating.
  • Handle permissions beyond “logged in or not.”
  • Handle user registration or account recovery.

ユーザ情報の保存先がDynamoDBであっても当然考え方は同様になりますので、以下のようなフローで処理で進めることにしました。

  1. ユーザが入力したユーザ名とパスワードを受け取る
  2. ユーザ名を利用してDynamoDBの項目を検索する
  3. 該当する項目が存在すればその情報でdictを作る
  4. ユーザが入力したパスワードとdictに入れたパスワードを照合する
  5. 一致すればdictの情報を使ってUserクラスのインスタンスを作成する
  6. 作成したインスタンスを利用してログイン処理を行う

User Class

UserMixinクラスを利用すればFlask-Loginが必要とする各メソッドが利用できますので、以下のようにコンストラクタを定義します。

from flask_login import UserMixin

class User(UserMixin):
  def __init__(self, id, name, password):
    self.id = id
    self.name = name
    self.password = password

Search Item

あらかじめDynamoDBから項目を検索する関数を用意しておきます。DynamoDBでは、別途インデックスを作成しない限り、パーティションキーまたはパーティションキーとソートキーの組み合わせによる検索のみ行うことができます。ここではパーティションキーとして id を指定し、ソートキーは定義していませんので、id による検索を用意します。

import boto3
from boto3.dynamodb.conditions import Key

session = boto3.Session(profile_name="iamuser")  # iamuserは検索用のIAMユーザ
dynamodb = boto3.resource("dynamodb", region_name='ap-northeast-1')  # 東京リージョンの場合
table = dynamodb.Table("user_table")  # user_tabelは項目を作成してあるテーブル名

def get_user_by_id(id):
  response = table.get_item(Key={"id": id})
  try:
    user = response['Item']
  except KeyError:
    user = {}
  return user

Login

ログインで必要な処理は以下のようになります。Flask-Loginが用意しているlogin_user()にはUserクラスのインスタンスを引き渡す必要がありますので、DynamoDBから取得した情報を元にUserクラスのインスタンスを作成しています。

@app.route("/login/", methods=["POST"])
def login():
  username = request.form.get("username")
  password = request.form.get("password")

  # Search user
  loaded_user = get_user_by_id(username)

  # Check password
  if loaded_user.get("password") == password:
    # Create user instance
    user = User(loaded_user.get("id"),
                loaded_user.get("name"),
                loaded_user.get("password"))
    # Login by Flask-Login
    login_user(user)
    return render_template("after_login.html")
  else:
    error = "Login failed"
    return render_template("login.html", error=error)

Loading User

先述のとおり、Flask-LoginではUserクラスのインスタンスを応答するload_user()も用意する必要があります。

@login_manager.user_loader
def load_user(id):
  loaded_user = get_user_by_id(id)
  user = User(loaded_user.get("id"),
              loaded_user.get("name"),
              loaded_user.get("password"))
  return user

Note

データベースとは異なり、DynamoDBでは検索できるキーに制限があることや、いわゆるリレーションを扱うべきではないフラットな構造のため、ユーザの属性や関連付けたいデータが増えてくるとどうしても不便になってしまう可能性があります。そのため、データ設計の段階ではDynamoDBの属性にJSONデータを入れてしまうことも考えられます。