本日の目的・ゴール
目的
ユーザー情報の機密性を高めるためにパスワードをハッシュ化させたい。ハッシュ化することでユーザーのパスワードが平文で流出することを防ぐことが出来る。
ゴール
11/15に作成したユーザー管理APIのログインパスワードをハッシュ化する
学んだこと
11/15に作成したユーザー管理APIにパスワードをハッシュ化する機能付与したコードをChatGPTを補助的に活用しつつ、構造や処理の意味を理解しながら実装した。
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from passlib.context import CryptContext
app = FastAPI()
# --- パスワードのハッシュ設定 ---
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# --- ユーザーモデル ---
class User(BaseModel):
username: str
email: str
password: str
# --- 仮のDB(リストで代用) ---
users_db = []
# --- パスワードをハッシュ化する関数 ---
def hash_password(password: str):
return pwd_context.hash(password)
# --- パスワードを検証する関数 ---
def verify_password(plain_password: str, hashed_password: str):
return pwd_context.verify(plain_password, hashed_password)
# --- ユーザー登録 ---
@app.post("/register")
def register_user(user: User):
for u in users_db:
if u.email == user.email:
raise HTTPException(status_code=400, detail="Email already registered")
# パスワードをハッシュ化して保存
hashed_pw = hash_password(user.password)
user.password = hashed_pw
users_db.append(user)
return {"message": "User registered successfully"}
# --- ユーザー一覧取得(デバッグ用)---
@app.get("/users")
def get_users():
return users_db
# --- ログイン ---
@app.post("/login")
def login(user: User):
for u in users_db:
if u.email == user.email:
if verify_password(user.password, u.password):
return {"message": "Login successful!"}
else:
raise HTTPException(status_code=401, detail="Incorrect password")
raise HTTPException(status_code=404, detail="User not found")
- ハッシュ化の設定インスタンスの作成方法
pwd_context:変数、ハッシュ化するための設定を保存する
CryptContext:passlibのクラスで、どんな方式でハッシュ化するかの設定をまとめた箱
bcrypt:ハッシュ化の計算を指定したもの
depredated:古い方式が利用されていた場合自動で新しい方式(bcrypt)に更新する
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
2.ハッシュ化、ハッシュの検証方法
下記コードでユーザーが入力したパスワードをハッシュ化
return pwd_context.hash(password)
.hash('パスワード')
下記でユーザーが入力したパスワードとリストに保存されているハッシュ化済みのパスワードが一致するか検証する
return pwd_context.verify(plain_password, hashed_password
.verify('入力されたパスワード','保存されたハッシュ')
私の疑問
.hashはユーザー登録のフローの中に、.verifyはログインフローの記述の中にまとめて記載すればよいのでは?なぜわざわざ外に出しているのか?
回答→理由は4つある
①再利用性
ハッシュ化や検証はほかの場面(パスワード変更時、リセット時など)で利用することがあるので毎度いちいち書くのではなく、共通の設定を一か所にまとめておくことでどこでも使えるようにしている
②責務の分離
登録またはログインの処理は認証に集中するべきという考えから
cryptContextはセキュリティポリシーに関するものなのでごっちゃにするとコードの役割がごっちゃになってしまう
③保守性
他のハッシュ方式に変更したい時など一か所を修正すれば全体が修正されることになるので簡単
④バグの切り分け
ログイン時のバグとハッシュ化のバグを切り分けやすくなる
3.パスワードをハッシュ化して保存
ユーザーが入力した生のパスワード (user.password) をハッシュ化して、
hashed_pw という新しい変数に入れている」処理。ハッシュ化したパスワードを一時的に保存している
hashed_pw = hash_password(user.password)
もともとユーザーが入力した生のパスワードを、ハッシュ値に上書きしている」処理
user.password = hashed_pw
二つの処理をまとめることも可能だが、デバック時などにhash_passwordの関数が正しく動いているかの確認ができないため、ふたつに分けて書いている。
躓いたところ
1.main3.pyのサーバー起動に失敗した。
ModuleNotFoundError: No module named 'passlib'
passlibモジュールをインストールしてい無かったので、下記でインストールを実施
pip install passlib[bcrypt]
[bcrypt]を末尾に付けることで、passlibモジュールのbcryptを使えるようになる。
bcrypt付きでインストールする理由:
ハッシュ化アルゴリズムとして bcrypt を利用しているため、passlib[bcrypt] と指定する必要があります。
2.passlibとbcryptに互換性がないことによるエラーが発生。
1.でインストールしたpasslibモジュールとbcryptでは互換性がないという内容のエラーが表示された。
(trapped) error reading bcrypt version
AttributeError: module 'bcrypt' has no attribute '__about__'
エラー内容でwebで検索すると解決方法に関する情報が沢山出てくる
今回はbcryptのバージョンを下げることでエラーに対応する。
passlibは2020年以降更新されていないようなので、今後もそれぞれのバージョンによる互換性のエラーには気をつける
pip install 'bcrypt==4.0.1' --force-reinstall
pip install 'passlib==1.7.4' --force-reinstall
所感
今日は、ユーザー管理システムにおける重要な要素である「パスワードのハッシュ化」について学習した。
ハッシュ化の計算自体は非常に複雑だが、ライブラリを利用することで安全な実装を簡潔に行える点に驚いた。実際の仕組みをすべて理解しなくても実装は可能だが、今後は内部の仕組みにも興味を持って学んでいきたい。
また、今回の学習を通して、設計段階で「再利用性」「職務の分離」「保守性」「バグの切り分け」を意識してコードを構築する重要性を実感した。
今後は、単に動くコードを目指すのではなく、保守性が高く、他者から見ても理解しやすい設計を意識して学習を進めていきたい。