Djangoの開発時、URLパラメータやフォームのhiddenフィールド、クッキーなどを通じてクライアントにデータを送信し、再度受け取る必要がある場合があります。このとき、「このデータが途中でユーザーによって変更されていないか?」という懸念が生じます。 django.core.signingは、まさにこの問題を解決するための強力なツールです。

このモジュールはデータの 暗号化(Encryption)ではなく暗号化署名(Cryptographic Signing)を提供します。

  • 暗号化 (X): データの内容を隠します。

  • 署名 (O): データの内容は露出される可能性がありますが、データが改ざんされていないことを保証します。


1. 重要な使用法: dumps()loads()



signingモジュールの核心はdumps()loads()です。Pythonオブジェクトを署名された文字列に変換したり、署名された文字列を再検証しオブジェクトに復元します。

dumps(): オブジェクトを署名された文字列に変換

dumps()は辞書、リストなどのJSON直列化が可能なオブジェクトを受け取り、URLセーフな署名された文字列を返します。

from django.core.signing import dumps

# 署名するデータ
user_data = {'user_id': 123, 'role': 'user'}

# データを署名して文字列にします。
signed_data = dumps(user_data)

print(signed_data)
# 出力例: eyJ1c2VyX2lkIjoxMjMsInJvbGUiOiJ1c2VyIn0:1q51oH:F... (データ:署名)

結果は [エンコードされたデータ]:[署名] の形です。前の部分は元のデータをBase64でエンコードしたもので、誰でもデコードして元を見ることができます。 後の部分がDjangoの SECRET_KEY で生成した署名(ハッシュ値)です。

loads(): 署名された文字列をオブジェクトに復元 (検証)

loads()dumps()で生成された文字列を受け取り署名を検証します。署名が有効な場合は元のオブジェクトを返し、有効でない場合は BadSignature 例外を発生させます。

from django.core.signing import loads, BadSignature

try:
    # 署名されたデータを元のオブジェクトに復元 (検証)
    unsigned_data = loads(signed_data)
    print(unsigned_data)
    # 出力: {'user_id': 123, 'role': 'user'}

except BadSignature:
    print("データが改ざんされているか署名が有効ではありません。")

# もしデータが1文字でも変更されたら?
tampered_data = signed_data.replace('user', 'admin')

try:
    loads(tampered_data)
except BadSignature:
    print("改ざんされたデータはBadSignature例外を発生させます。")

常に try...except BadSignature 構文で囲んで使用する必要があります。


2. 重要な特徴: データはどこに保存されるのか?

django.core.signingの最大の特徴は 「Stateless (ステートレス)」であることです。

dumps()で生成された文字列はサーバーのデータベースやキャッシュのどこにも保存されません。 すべての情報(データと署名)が署名された文字列そのものに含まれています。

サーバーはこの文字列をクライアントに(URL、クッキー、フォームフィールドなどで)渡し、その後クライアントがこの値を再提出するとSECRET_KEYを使って即座に有効性を検証するだけです。このおかげで、サーバーのストレージスペースを全く使用せず、非常に軽く効率的です。


3. 有効時間設定: max_ageの秘密



「データがサーバーに保存されないのに、どうやって有効時間を設定できるのですか?」

max_ageオプションは dumps() 時にタイムスタンプ(時間情報)をデータと一緒に署名に含めます。

loads()を呼び出すときにmax_age引数(秒単位)を渡すと、署名検証後に内蔵されたタイムスタンプを確認します。

  1. 現在の時間とタイムスタンプの差を計算します。

  2. この差が max_ageを超えると、署名が改ざんされていなくてもSignatureExpired例外を発生させます。

from django.core.signing import dumps, loads, SignatureExpired, BadSignature
import time

# 1. 署名生成(この時の時間が記録される)
signed_data = dumps({'user_id': 456})

# 2. 10秒有効期間で検証(即時) -> 成功
try:
    data = loads(signed_data, max_age=10)
    print(f"検証成功: {data}")
except SignatureExpired:
    print("署名が期限切れです。")
except BadSignature:
    print("署名が有効ではありません。")


# 3. 5秒待機
time.sleep(5)

# 4. 3秒有効期間で検証(既に5秒経過) -> 失敗
try:
    data = loads(signed_data, max_age=3)
    print(f"検証成功: {data}")
except SignatureExpired:
    print("署名が期限切れです。(max_age=3)")
except BadSignature:
    print("署名が有効ではありません。")

これもサーバーに有効期限を保存する方法ではなく、署名自体に含まれたタイムスタンプを活用するstatelessな方法です。


4. 重要! 注意事項と活用のヒント

  1. SECRET_KEYは命です。

    このすべての署名メカニズムはsettings.pyのSECRET_KEYを基に動作します。このキーが漏洩すれば、誰でも有効な署名を生成できてしまうため、絶対に外部に漏らしてはいけません。(Gitにアップロードしないでください!)

  2. 暗号化でないことを忘れないでください。

    署名されたデータの前の部分(Base64)は誰でも簡単にデコードして元の内容を視認することができます。パスワード、個人情報などの機密データを直接 dumps() に入れないでください。(例:user_idは問題ありませんが、user_passwordは駄目です。)

  3. saltで署名を分離しましょう。

    異なる目的で署名を使用する際はsalt引数を使用してください。saltが異なると同じデータでも署名結果が完全に異なります。

# 用途に応じて異なるsaltを使用
pw_reset_token = dumps(user.pk, salt='password-reset')
unsubscribe_link = dumps(user.pk, salt='unsubscribe')
こうすることで、「脱退リンク」用の署名を「パスワード再設定」に再利用するなどの攻撃を防ぐことができます。

5. 主要な活用事例

  • パスワードリセットURL: ユーザーのIDとタイムスタンプを署名してメールで送信(DBに一時トークンを保存する必要なし)

  • メール認証リンク: 新規登録時のメール認証リンク

  • 安全な next URL: ログイン後にリダイレクトされる?next=/private/ URLが悪意のあるサイトに変更されるのを防ぐ

  • 一時ダウンロードリンク: 特定の時間(max_age)のみ有効なファイルアクセスURLを生成

  • マルチステップフォーム (Form Wizard): 前のステップのフォームデータを次のステップに引き継ぐときにデータ改ざんを防止