ログイン処理(認証)の実装で躓いた点を書く。
実行環境
処理の流れ
- 新規登録画面からユーザ ID、パスワードを入力
- 新規登録ボタン押下
- 入力チェック OK なら、パスワードをハッシュ化してテーブルへ登録しログイン画面へ遷移(NG なら新規登録画面へ遷移)
- ログイン画面にユーザ ID、パスワードを入力
- 認証して OK なら、別の画面に遷移(NG ならログイン画面へ遷移)
5 が今回作ったところ。認証には werkzeug.security.check_password_hash() を使う
出来上がったソースコードたち
一応期待通りに動いている。
app.py
from flask import Flask, render_template, redirect, g, request from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField from wtforms.validators import DataRequired, EqualTo import sqlite3 from werkzeug.security import generate_password_hash, check_password_hash from IPython.core.display import display app = Flask(__name__) # 秘密鍵設定 app.config['SECRET_KEY'] = 'mysecretkey' # DB 指定 DATABASE='pog-draft-project/pog.db' # DB 接続 def connect_db(): rv = sqlite3.connect(DATABASE) rv.row_factory = sqlite3.Row return rv # DB 取得 def get_db(): if not hasattr(g,'sqlite_db'): g.sqlite_db = connect_db() return g.sqlite_db # 新規登録の form クラス class registrationForm(FlaskForm): # 各項目と入力チェックの内容を設定 username = StringField('ユーザ名', validators=[DataRequired()]) password = PasswordField('パスワード', validators=[DataRequired()]) pw_check = PasswordField('パスワード確認', validators=[DataRequired(), EqualTo('password')]) submit = SubmitField('新規登録') # 新規登録処理 @app.route('/index', methods=['GET','POST']) def index(): form = registrationForm() # 入力チェックを実施して問題なければ users テーブルに格納 if request.method == 'POST': if form.validate_on_submit(): # パスワードをハッシュ値に変換 pw_hash = generate_password_hash(form.password.data, method='pbkdf2:sha256') # DB へユーザ情報を登録 con = get_db() cur = con.cursor() sql = "INSERT INTO users(user_name, password) VALUES(?, ?)" param = [form.username.data, pw_hash] cur.execute(sql, param) con.commit() con.close() return redirect('/login') return render_template('index.html', form=form) return render_template('index.html', form=form) # ログイン画面の form クラス class loginForm(FlaskForm): # 各項目と入力チェックの内容を設定 username = StringField('ユーザ名', validators=[DataRequired()]) password = PasswordField('パスワード', validators=[DataRequired()]) submit = SubmitField('ログイン') # ログイン処理 @app.route('/login', methods=['GET','POST']) def login(): form = loginForm() if request.method == 'POST': # Users テーブルから対象ユーザの password を取得 con = get_db() cur = con.cursor() sql = "SELECT password FROM users WHERE user_name = ?" param = form.username.data res = cur.execute(sql, (param,)).fetchone() # tuple -> string res = ''.join(res) con.close() # パスワード認証 pw_auth = check_password_hash(pwhash=res, password=form.password.data) print(pw_auth) if pw_auth == True: # 認証 OK の場合 return redirect('/selection') msg = 'ログインエラー:ユーザ名またはパスワードが一致しません' return render_template('login.html', form=form, msg=msg) return render_template('login.html', form=form) if __name__ == "__main__": app.run(debug=True)
index.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>新規登録</title> </head> <body> <h2>新規登録</h2> <div> <form method="POST"> {{ form.hidden_tag() }} <!-- 入力フォーム設定 --> <div>{{ form.username.label }} {{ form.username() }}</div> <div>{{ form.password.label }} {{ form.password() }}</div> <div> {{ form.pw_check.label }} {{ form.pw_check() }} {{ form.pw_check.error }} </div> {{ form.submit() }} <div> {% if form.pw_check.errors %} <ul> {% for error in form.pw_check.errors %} <li>{{ form.pw_check.label ~' '~ error }}</li> {% endfor %} </ul> {% endif %} </div> <div><a href="/login">ログイン画面はこちら</a></div> </form> </div> </body> </html>
login.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>ログイン</title> </head> <body> <h2>ログイン</h2> <div> <form method="POST"> <!-- 入力フォーム設定 --> <div>{{ form.username.label }} {{ form.username() }}</div> <div>{{ form.password.label }} {{ form.password() }}</div> <div>{{ form.submit() }}</div> <div><a href="/index">新規登録画面はこちら</a></div> <div>{{ msg }}</div> </form> </body> </html>
werkzeug.security.check_password_hash() について
generate_password_hash() とセットで使う。generate_password_hash() については、以下記事を参照。
check_password_hash() のパラメータと戻り値は以下の通り。
Parameters: pwhash (str) – a hashed string like returned by generate_password_hash().
password (str) – the plaintext password to compare against the hash.
Return type: bool
Utilities — Werkzeug Documentation (2.2.x)
- 第一引数: 新規登録処理で generate_password_hash()でハッシュ化してテーブルに登録したパスワードの値
- 第二引数: ログイン画面でユーザが入力した平文パスワード
check_password_hash() が第二引数の平文パスワードをハッシュ化し、第一引数のハッシュ値(つまり正解の値)と比較してくれる。比較した結果、2 つの値が一致していれば True, 不一致であれば False を返してくれる。シンプルな仕組みで分かりやすい。
ログイン処理の実装で躓いた点
前提
以下、1 ~ 3 が実行済みであり、テーブルに以下のレコードが格納されていること。
- 新規登録画面からユーザ ID、パスワードを入力
- 新規登録ボタン押下
- 入力チェック OK なら、パスワードをハッシュ化してテーブルへ登録しログイン画面へ遷移(NG なら新規登録画面へ遷移)
テーブル状態(正常終了時)
user_id | user_name | password | user_role | updated_at |
---|---|---|---|---|
1 | panco | pbkdf2:sha256:260000$aA42TAOrOJ1mYet1$16b8aa3305971337a49db43b1ff9624589d5963c10c546941ec7692ac36d0228 | 0 | 2023-10-14 16:39:08 |
躓き ① 第一引数の取得で ProgrammingError
sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 1, and there are 5 supplied.
ProgrammingError になるコード
if request.method == 'POST': # Users テーブルから対象ユーザの password を取得 con = get_db() cur = con.cursor() sql = "SELECT password FROM users WHERE user_name = ?" param = form.username.data res = cur.execute(sql, param).fetchone() con.close()
修正後のコード
ProgrammingError は解消したが、後述する AttributeError が発生した。
if request.method == 'POST': # Users テーブルから対象ユーザの password を取得 con = get_db() cur = con.cursor() sql = "SELECT password FROM users WHERE user_name = ?" param = form.username.data # (sql, (param,)) と書く必要がある res = cur.execute(sql, (param,)).fetchone() con.close()
躓き ② 第一引数の取得で AttributeError
AttributeError: 'sqlite3.Row' object has no attribute 'count'
AttributeError になるコード
if request.method == 'POST': # Users テーブルから対象ユーザの password を取得 con = get_db() cur = con.cursor() sql = "SELECT password FROM users WHERE user_name = ?" param = form.username.data # (sql, (param,)) と書く必要がある res = cur.execute(sql, (param,)).fetchone() con.close()
テーブルに格納された password は SQL で取得できているが、取得結果(変数 res)の値が以下のようになっていた。
<sqlite3.Row object at 0x0000019D20857970>
期待値は以下の文字列となるが、'sqlite3.Row'という異なる型で出力してしまった。str 型ではないため count が使えず怒られている。
pbkdf2:sha256:260000$aA42TAOrOJ1mYet1$16b8aa3305971337a49db43b1ff9624589d5963c10c546941ec7692ac36d0228
補足
count は check_password_hash() で、第一引数の "$" の数を数える際に使っている。
werkzeug>security.py
def check_password_hash(pwhash: str, password: str) -> bool: if pwhash.count("$") < 2: return False
修正後のコード
if request.method == 'POST': # Users テーブルから対象ユーザの password を取得 con = get_db() cur = con.cursor() sql = "SELECT password FROM users WHERE user_name = ?" param = form.username.data res = cur.execute(sql, (param,)).fetchone() # tuple -> str に型変換 res = ''.join(res) con.close()
型を str に変換することで解消した。
こうすることで、変数 res には期待通りに以下の値が入った。
pbkdf2:sha256:260000$aA42TAOrOJ1mYet1$16b8aa3305971337a49db43b1ff9624589d5963c10c546941ec7692ac36d0228
以上で、正常に第一引数を渡せた。第二引数はログイン画面から入力されたパスワードを渡すだけのため特に引っかかる点はなし。無事に True or False が返るようになった。