FastAPIで簡単にログイン認証が実装できた
最近flutter、fastapiを使って個人開発を始めました。
ログイン周りの実装に入ろうとしたところで、そういえば認証の仕組みについて曖昧な点があること、初めて触るフレームワークであること、記事にするのに丁度いい機会だと感じたので書いてみます。
概要
「パスワード(およびハッシュ化)によるOAuth2、JWTトークンによるBearer」とタイトルにある通り、認可にOAuth2、認証にBearer、トークン形式はJWT、パスワードはハッシュ化して保存する仕組みです。
ざっくりと説明すると、ログイン時にのみパスワードを使用して、以降のリクエストはアクセストークンをパスワード代わりに使用する仕組みです。
ポイント
ハッシュ化
ハッシュ化とは文字列を不規則な文字列に変換することです。もし第三者が不正にパスワードへアクセスしたとしても、悪用されるのを防ぐことができます。
不可逆性という特性を持つため、一度ハッシュ化すると平文に戻せません。
データベースのpasswordはハッシュ化して保存します。
// bcryptはハッシュ化アルゴリズムの一つ
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
// 平文とハッシュ済みパスワードの比較
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
// 平文からハッシュ化の変換、ユーザ登録する際に使用します
def get_password_hash(password):
return pwd_context.hash(password)
def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
JWT
JWTとはjsonをエンコードしたトークンの仕様です。
// JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
subがユーザの識別子です。例ではusernameが使用されています。
expires_deltaで有効期限も指定できるようです。
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
Bearer
OAuth 2.0ではよく使われる認証方式で、ヘッダーにアクセストークンを含めてAPIリクエストし、トークンが有効であればリクエストを処理します。
OAuth2PasswordBearerはRequestのAuthorization headerのtokenを取得します。
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
実際に動かしてみた
ここからは例のコードを参考にして、手元で動かしてみました。
まずはユーザ登録処理を作成してみました。
def get_password_hash(password):
return pwd_context.hash(password)
@router.post("/signup", response_model=user_schema.UserCreateResponse)
def create_user(user_body: user_schema.UserCreate):
user_body.password = get_password_hash(user_body.password)
return user_db.create(user_body)
早速ログインしてみます。ログイン処理はこの辺り。
OAuth2PasswordBearerのtokenUrlは自由に設定可能なので/loginにしてみました。
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
def authenticate_user(mail: str, password: str):
user = user_db.findByMail(mail)
if not user:
return False
if not verify_password(password, user.password):
return False
return user
@router.post("/login")
def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.id}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
無事にログイン完了!
flutterからもログインしてアクセストークン取得までしてみます。
class UserLoginModel {
final String username;
final String password;
UserLoginModel({required this.username, required this.password});
Map<String, String> toBodyField() {
return {
'grant_type': 'password',
'username': username,
'password': password,
};
}
}
final response = await http.post(Uri.parse("http://127.0.0.1:8000/login"),
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: UserLoginModel.toBodyField(),
encoding: Encoding.getByName('utf-8'));
先ほどと同じユーザでログインしてみると、こちらもtokenが返却されログインできました!
Discussion