👥

【Flask】Userクラス

3 min read

本格的な開発現場の実務経験が少ないせいで、我流で開発をやっています。
もし第三者から指摘があればというのを期待して、汎用部分の自分のコードを晒しておきます。

Userクラス

私が現在作成しているアプリケーションはとてもシンプルなものです。
FlaskのWebアプリケーションですが、Userクラスがこんな感じです。

# db.users
from flask_login import UserMixin
from .dbcon import db

class User(UserMixin):
    def __init__(self, record, require_passhash=False):
        self.id = record[0]
        self.email = record[1]
        self.name = record[2]
        self.status = record[3]
        if require_passhash:
            self.passhash = record[4]

def get(user_id):
    sql = '''
    select * from `users`
    where `id` = %s
    '''
    params = (user_id,)
    data = db.query(sql, params)
    if data is None or len(data) == 0:
        return None
    return User(data[0])

def get_by_email(email, require_passhash=False):
    sql = '''
    select * from `users`
    where `email` = %s
    '''
    params = (email,)
    data = db.query(sql, params)
    if data is None or len(data) == 0:
        return None
    return User(data[0], require_passhash=require_passhash)

DBのusersテーブルからデータを取得する際にUserクラスとして渡すような作りになっています。flask_loginの基本的な用法に沿っているつもりです。
ちょっと微妙なのが、ユーザーログイン時にパスワードのハッシュ値を取得して検証しないといけないのですが、必要な時以外はUserクラスに持たせたくないのでrequire_passhashなんて変なことをやっています。
statusはロック状態とかの基本的なアクセス権に関わる情報で、定数のどれかを保存しています。

Userクラスはユーザーの認証・認可のためのデータと考えているので、アプリケーション的にユーザーに付属する詳細情報は別テーブル・別クラスにて作成します。

クラスと言いながら、メソッドの無いただの構造体です。
処理内容はファイル(モジュール)に直接関数定義しているので、DBに直結したクラスで何かメソッドが必要になることはないと思っています。

TempUserクラス

本アプリケーションではメールアドレスをユニークなユーザー情報として要請しています。
そのため、登録時は仮登録メールを発生させ、メール記載のトークンが送信されてくれば本登録完了となります。

トークンを管理するために、仮登録状態のユーザーは本来の利用ユーザーとは別テーブル・別クラスで管理することにしました。

# db.tempusers
from .dbcon import db

class TempUser:
    def __init__(self, record):
        self.token = record[0]
        self.type = record[1]
        self.user_id = record[2]
        self.email = record[3]
        self.name = record[4]
        self.status = record[5]
        self.passhash = record[6]

def get(token):
    sql = '''
    select * from `tempusers`
    where `token` = %s and `expires_at` > now()
    '''
    params = (token,)
    data = db.query(sql, params)
    if data is None or len(data) == 0:
        return None
    return TempUser(data[0])

本登録時の操作はこんな感じです。(簡略化したもの)

    tempuser = db.tempusers.get(token)
    if tempuser is None or tempuser.type != TEMPUSER_TYPE_REGISTER:
        return ###(エラーメッセージ)
    if not db.users.register(tempuser.email, tempuser.name, tempuser.status, tempuser.passhash):
        return ###(エラーメッセージ)
    return ###(OKメッセージ)

TempUserのtypeは、登録時以外にもトークンが要求されるメール変更やパスワードリセットで使うつもりだからです。

tokenとかhashの生成

tokenは適当です。パスワードはbcryptでハッシュ化して保存しています。

# util
import random
import bcrypt

def generate_token(length):
    approvals = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    applen = len(approvals)
    token = ''
    for i in range(length):
        r = random.randrange(applen)
        token = token + approvals[r]
    return token

def hash_password(password):
    salt = bcrypt.gensalt(rounds=10, prefix=b'2a')
    passhash = bcrypt.hashpw(password.encode(), salt).decode()
    return passhash

def check_password(password, passhash):
    return bcrypt.checkpw(password.encode(), passhash.encode())

余談

https://speakerdeck.com/minodriven/kusokododong-hua-userkurasu-dekao-eruji-shu-de-fu-zhai-jie-xiao-falseguan-dian
twitterで流れていたので、この資料のプレゼン映像を見ました。

ここでは個人顧客と法人という立場の違うものを1つのユーザークラスにしていたせいで問題が発生し、結論として「個人アカウント」「法人アカウント」とのように違うクラスにしましょうということになっています。
もっともですが、仮に個人アカウントも法人アカウントも共通のログイン機構でログイン処理を行ったり、共通の機構でアクセス管理を行ったりするのであれば、ユーザー識別のための抽象化したクラスは必要で、そういうのもありなんじゃないかな、と思いました。