Day 73:認証の基礎 ― 「守りながら動かす」感覚を身につける
Day 73では、Web/API開発に欠かせないテーマ、認証の基礎 を扱います。
キーワードはこの4つです。
- パスワード管理の基本
- ハッシュ化
- トークン認証の概念
- 環境変数による秘密情報管理
ここまで、FastAPIとSQLiteでCRUD APIを作り、 「データを作る・読む・更新する・削除する」流れを体験してきました。
でも、現実のサービスでは、 「誰でも自由に触れていいAPI」ばかりではありません。
- ログインしたユーザーだけが見られる情報
- 管理者だけが操作できる機能
- 外部に漏れてはいけない秘密情報
など、「守らなければいけないもの」がたくさんあります。
今日は、その入り口となる 認証の基礎 を、 一歩ずつ、丁寧にかみ砕いていきます。
パスワード管理の基本 ― 「生のパスワードは絶対に保存しない」
まず、絶対に覚えてほしいルール
認証の話をするときに、 いちばん最初に、そしていちばん強く伝えたいルールがあります。
「生のパスワード(平文パスワード)を、そのまま保存してはいけない」
これは、どんな規模のサービスでも共通の大原則です。
もし、ユーザーのパスワードをそのままデータベースに保存してしまうと、
- DBが漏れた瞬間、全ユーザーのパスワードが丸見えになる
- 他サービスでも同じパスワードを使っている場合、連鎖的に被害が広がる
という、とても危険な状態になります。
では、どうするべきか。
そこで登場するのが ハッシュ化 です。
ハッシュ化 ― 「元に戻せない形に変えてから保存する」
ハッシュって何?
ハッシュ(hash) とは、
「あるデータを、特定のアルゴリズムで“変換”した結果」
のことです。
ここで重要なのは、
- 同じ入力 → 同じハッシュ値
- 違う入力 → ほぼ必ず違うハッシュ値
- ハッシュ値から元の入力を復元することは、基本的にできない(現実的ではない)
という性質です。
この「元に戻せない」という性質を利用して、 パスワードを安全に扱います。
パスワードをハッシュ化して保存する流れ
ユーザー登録時の流れを、ざっくり書くとこうなります。
- ユーザーがパスワードを入力する(例:
"secret123") - サーバー側で、そのパスワードをハッシュ化する(例:
"af3d9..."のような長い文字列) - データベースには「ハッシュ値」だけを保存する
- ログイン時には、入力されたパスワードを同じ方法でハッシュ化し、 保存されているハッシュ値と一致するかどうかをチェックする
つまり、サーバー側は 「パスワードそのもの」ではなく「ハッシュ値」だけを覚えておく というわけです。
Pythonでのハッシュ化のイメージ(初心者向け)
実際のサービスでは、bcrypt や argon2 などの専用ライブラリを使うことが多いですが、 ここではイメージをつかむために、標準ライブラリの hashlib を使った例を見てみます。
# day73_hash_example.py
import hashlib
def hash_password(password: str) -> str:
"""
パスワードをハッシュ化する簡単な例です。
実際のサービスでは、より安全なアルゴリズム(bcryptなど)を使います。
ここではイメージをつかむために、SHA-256を使っています。
"""
# 文字列をバイト列に変換します。
password_bytes = password.encode("utf-8")
# SHA-256でハッシュ化します。
hash_object = hashlib.sha256(password_bytes)
# ハッシュ値を16進数文字列として取得します。
hashed = hash_object.hexdigest()
return hashed
if __name__ == "__main__":
raw = "secret123"
hashed = hash_password(raw)
print("元のパスワード:", raw)
print("ハッシュ値:", hashed)
Pythonこのコードを実行すると、 secret123 のような短い文字列が、 長いハッシュ値に変換されて表示されます。
ポイントは、
- 同じパスワード → 同じハッシュ値
- ハッシュ値から元のパスワードを復元することはできない
というところです。
※実際の認証では、ソルト(ランダムな文字列)を混ぜたり、 より安全なアルゴリズムを使ったりしますが、 Day 73ではまず「ハッシュ化して保存する」という基本の考え方を押さえておきましょう。
トークン認証の概念 ― 「一度認証したら、しばらくは“通行証”でやり取りする」
毎回パスワードを送るのは危険で面倒
ユーザーがログインするとき、 最初は「メールアドレス+パスワード」で認証します。
でも、その後のAPI呼び出しで、 毎回パスワードを送るのは、
- セキュリティ的にもリスクがある
- ユーザー体験としても面倒
ですよね。
そこで登場するのが トークン認証 です。
トークンって何?
トークン(token) は、 ざっくり言うと 「一時的な通行証」 です。
流れとしては、こんなイメージです。
- ユーザーが「メールアドレス+パスワード」でログインする
- サーバー側が認証に成功したら、「トークン」を発行する
- クライアントは、その後のAPI呼び出しで、 HTTPヘッダなどにトークンを付けてリクエストを送る
- サーバー側は、「このトークンは有効か?誰のものか?」をチェックして、 有効なら処理を続行する
つまり、「最初だけパスワード」「その後はトークン」 という二段構えになります。
トークンの簡単なイメージ例(FastAPI風)
ここでは、概念をつかむためのシンプルな例を見てみます。
# day73_token_example.py
from fastapi import FastAPI, HTTPException, Header
app = FastAPI()
# 仮の「有効なトークン」を1つだけ用意しておきます。
VALID_TOKEN = "example-token-123"
@app.post("/login")
def login(username: str, password: str):
"""
とても簡略化したログインAPIの例です。
実際には、DBからユーザーを検索し、ハッシュ化されたパスワードを照合します。
ここでは、固定のユーザー名・パスワードで認証に成功したと仮定します。
"""
if username == "demo" and password == "password":
# 認証成功時にトークンを返します。
return {"token": VALID_TOKEN}
raise HTTPException(status_code=401, detail="Invalid credentials")
@app.get("/secure-data")
def secure_data(authorization: str | None = Header(default=None)):
"""
トークン認証が必要なAPIの例です。
HTTPヘッダ "Authorization" にトークンが含まれているかをチェックします。
"""
if authorization is None:
raise HTTPException(status_code=401, detail="Authorization header missing")
# ここでは "Bearer <token>" 形式を想定します。
try:
scheme, token = authorization.split(" ")
except ValueError:
raise HTTPException(status_code=401, detail="Invalid Authorization header format")
if scheme != "Bearer":
raise HTTPException(status_code=401, detail="Invalid auth scheme")
if token != VALID_TOKEN:
raise HTTPException(status_code=401, detail="Invalid token")
# トークンが有効な場合のみ、秘密のデータを返します。
return {"secret": "これはトークン認証されたユーザーだけが見られるデータです。"}
Pythonこの例では、
/loginに正しいユーザー名・パスワードを送ると、トークンが返ってくる/secure-dataにアクセスするときは、Authorization: Bearer example-token-123のようなヘッダを付ける必要がある- トークンが正しければ、秘密のデータが返ってくる
という流れになっています。
実際のサービスでは、 トークンとして JWT(JSON Web Token) を使ったり、 DBにトークンを保存して有効期限を管理したりしますが、 Day 73ではまず 「トークン=通行証」 というイメージを持ってもらえれば十分です。
環境変数による秘密情報管理 ― 「コードに書かないという優しさ」
秘密情報って何?
認証やセキュリティの話になると、 「絶対に外に漏れてはいけない情報」 がいくつか登場します。
例えば、
- データベースの接続パスワード
- トークンを暗号化・署名するための秘密鍵
- 外部サービスのAPIキー
などです。
これらを コードの中にベタ書きする のは、とても危険です。
- GitHubなどにコードを公開したときに、秘密情報まで公開されてしまう
- チーム開発で、全員に見えてしまう
- 環境ごとに値を変えたいときに、コードを書き換えないといけない
など、いろいろな問題が起きます。
そこで登場するのが 環境変数 です。
環境変数って何?
環境変数(environment variable) は、
OSや実行環境に設定しておく「名前付きの値」
のことです。
例えば、
DB_PASSWORD=super-secretSECRET_KEY=abc123xyz
のような形で、 OS側に「この名前の値はこれだよ」と設定しておきます。
Python側では、os.environ を使ってそれを読み取ります。
Pythonで環境変数を使う基本
# day73_env_example.py
import os
def get_secret_key() -> str:
"""
環境変数から秘密鍵を取得する例です。
環境変数 SECRET_KEY が設定されていない場合はエラーにします。
"""
secret_key = os.environ.get("SECRET_KEY")
if secret_key is None:
# 実際のアプリでは、ログを出したり、起動時にチェックしたりします。
raise RuntimeError("環境変数 SECRET_KEY が設定されていません。")
return secret_key
if __name__ == "__main__":
# 実行前に、ターミナルで次のように設定しておきます(例):
# export SECRET_KEY="my-super-secret-key"
key = get_secret_key()
print("取得した秘密鍵:", key)
Pythonこのようにしておくと、
- コードの中には秘密鍵を書かない
- 実行環境ごとに、環境変数の値を変えるだけで設定を切り替えられる
- Gitなどでコードを共有しても、秘密情報は含まれない
という、かなり健全な状態になります。
FastAPIで環境変数を使うイメージ
例えば、トークンの署名に使う秘密鍵を環境変数から読み込む場合。
# day73_env_fastapi_example.py
import os
from fastapi import FastAPI, HTTPException
app = FastAPI()
SECRET_KEY = os.environ.get("SECRET_KEY")
if SECRET_KEY is None:
# 起動時にチェックしておくと安心です。
raise RuntimeError("環境変数 SECRET_KEY が設定されていません。")
@app.get("/secret-key-check")
def secret_key_check():
"""
環境変数から読み込んだ秘密鍵が使えるかどうかを確認するための簡単なAPIです。
実際には、秘密鍵そのものを返すことはしません。
ここでは学習用に、長さだけを返しています。
"""
return {"secret_key_length": len(SECRET_KEY)}
Pythonこのように、
- 秘密情報は環境変数に置く
- コード側では「環境変数から読む」だけにする
というスタイルを身につけておくと、 セキュリティ的にも、運用的にも、とても扱いやすくなります。
Day 73ミニテンプレート ― 認証の基礎をひとまとめにしたサンプル
最後に、今日の内容をぎゅっとまとめた 「認証の基礎セット」ミニテンプレート を載せておきます。
- パスワードのハッシュ化
- ログイン+簡易トークン
- トークン認証が必要なAPI
- 環境変数から秘密情報を読む
という流れを、ひとつのファイルにまとめたイメージです。
# day73_auth_basic_template.py
import os
import hashlib
from fastapi import FastAPI, HTTPException, Header, status
from pydantic import BaseModel
app = FastAPI()
# 環境変数から「トークン用の秘密キー」を読み込みます。
SECRET_KEY = os.environ.get("SECRET_KEY")
if SECRET_KEY is None:
raise RuntimeError("環境変数 SECRET_KEY が設定されていません。")
class User(BaseModel):
"""
とても簡略化したユーザーモデルです。
実際にはDBに保存しますが、ここではメモリ上に1ユーザーだけ持つ例にします。
"""
username: str
password_hash: str
# 仮のユーザー1件(本来はDBから取得します)
# パスワードは "password" をハッシュ化したものとします。
def hash_password(password: str) -> str:
"""
パスワードをハッシュ化する関数です。
実際のサービスでは、bcryptなどの専用ライブラリを使うことを推奨します。
ここではSHA-256でイメージをつかみます。
"""
return hashlib.sha256(password.encode("utf-8")).hexdigest()
DEMO_USER = User(username="demo", password_hash=hash_password("password"))
def generate_token(username: str) -> str:
"""
とても簡略化したトークン生成関数です。
実際にはJWTなどを使いますが、ここでは「ユーザー名+秘密キー」を元に
ハッシュ値をトークンとして返します。
"""
raw = f"{username}:{SECRET_KEY}"
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
# メモリ上に「有効なトークン」を保存しておく簡易的な仕組み
valid_tokens: dict[str, str] = {} # token -> username
class LoginRequest(BaseModel):
username: str
password: str
class LoginResponse(BaseModel):
token: str
@app.post("/login", response_model=LoginResponse, status_code=status.HTTP_200_OK)
def login(login_req: LoginRequest):
"""
ログイン用のAPIです。
- ユーザー名とパスワードを受け取ります。
- パスワードをハッシュ化して、保存されているハッシュ値と比較します。
- 一致すればトークンを発行し、レスポンスとして返します。
"""
if login_req.username != DEMO_USER.username:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username or password")
# 入力されたパスワードをハッシュ化して比較します。
if hash_password(login_req.password) != DEMO_USER.password_hash:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username or password")
# 認証成功時にトークンを生成します。
token = generate_token(login_req.username)
# 有効なトークンとして保存しておきます。
valid_tokens[token] = login_req.username
return LoginResponse(token=token)
@app.get("/me")
def get_me(authorization: str | None = Header(default=None)):
"""
トークン認証が必要なAPIの例です。
- HTTPヘッダ "Authorization: Bearer <token>" を期待します。
- トークンが有効かどうかをチェックし、有効ならユーザー情報を返します。
"""
if authorization is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization header missing")
try:
scheme, token = authorization.split(" ")
except ValueError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Authorization header format")
if scheme != "Bearer":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid auth scheme")
username = valid_tokens.get(token)
if username is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token")
# 認証済みユーザーの情報を返します(ここでは簡略化してユーザー名だけ)。
return {"username": username, "message": "トークン認証に成功しました。"}
Pythonこのテンプレートを動かす前に、 ターミナルで環境変数を設定しておきます(例):
bash
export SECRET_KEY="my-super-secret-key"
uvicorn day73_auth_basic_template:app --reload
POST /loginに{"username": "demo", "password": "password"}を送ると、トークンが返ってくる- そのトークンを
Authorization: Bearer <token>として/meに送ると、認証済みユーザー情報が返ってくる
という流れを体験できます。
Day 73のまとめ ― 「守ることは、難しいだけじゃなくて、ちょっと誇らしい」
今日の主役は、
- パスワード管理の基本:生のパスワードは絶対に保存しない
- ハッシュ化:元に戻せない形に変えてから保存する
- トークン認証の概念:最初だけパスワード、その後は通行証(トークン)
- 環境変数による秘密情報管理:コードに秘密を書かず、環境に持たせる
でした。
認証やセキュリティの話は、 どうしても「難しそう」「怖そう」という印象を持たれがちですが、 今日扱った内容は、その中でも いちばん土台になる部分 です。
- パスワードをそのまま保存しない
- トークンという通行証のイメージを持つ
- 秘密情報は環境変数に逃がす
この3つを意識するだけでも、 作るサービスの「守りの強さ」が、目に見えて変わってきます。
Day 73でその感覚をつかんだことで、 この先の認証・認可・セキュリティの学びが、 ただの「難しい話」ではなく、 「自分のサービスをちゃんと守るための、大事な技術」 として、 少し誇らしい気持ちで向き合えるようになっていくはずです。
