Slide 1

Slide 1 text

Djangoにおける複数ユーザー 種別認証の設計アプローチ DjangoCongress JP 2025 オンライン 2025/2/22(土) 奥寺 政貴

Slide 2

Slide 2 text

自己紹介 • 奥寺政貴(おくでらまさたか) • 株式会社ビープラウド所属(2020〜) • 受託開発メイン • バックエンドエンジニア • 仕事でのDjango使用歴は約4年 • 趣味: インド料理の食べ歩き

Slide 3

Slide 3 text

話すこと WEBサービスの開発で以下のような要件がある場合のDjangoでの ユーザー認証の設計アプローチ 【要件】 • 2つ以上のユーザー種別が存在する • ユーザー種別ごとにシステムの使い方(≒ユースケース)が大きく 異なる ※ どういうことか次スライドで例を挙げて説明

Slide 4

Slide 4 text

2つ以上のユーザー種別の要件の例 サービス ユーザー種別1 システムの使い方 ユーザー種別2 システムの使い方 ECサイト 販売者 • 商品登録 • 在庫変更 購入者 • カートに追加 • 購入 記事配信サイト ライター • 記事執筆 • 公開管理 購読者 • 記事にコメント • 有料記事の購入 ホテル予約サイト 事業者 • 予約管理 • 部屋情報の更新 宿泊者 • 予約 • キャンセル 2つ以上のユーザー種別 こういう要件がある時のDjangoでのユーザー認証の設計アプローチについて話す システムの使い方(≒ユー スケース)が大きく異なる

Slide 5

Slide 5 text

ターゲット • ターゲット • Django中級者以上向け • 業務でDjangoを使っていて、設計も担当する人 • 想定している前提知識のレベル • Djangoの基本(Model、View...)は理解してる • ざっくりとでもいいので、DjangoのUserモデルのカスタマイ ズ方法を理解している

Slide 6

Slide 6 text

本発表の目的 • Djangoでの複数ユーザー種別認証の知見の体系化 • 巷で紹介されているパターン • 弊社内で実績があるパターン • → 議論や設計検討時の叩き台に使ってもらいたい • ※ 逆にベストプラクティスの提示ではない

Slide 7

Slide 7 text

話さないこと • ユーザー種別ごとにサービスを分割するマイクロサービスチッ クな話はしません(=モノリス前提)

Slide 8

Slide 8 text

話さないこと: 理由 端的にいうと私に知見がないから • 前提: 弊社は受託開発がメイン事業 • 0→1の新規開発や事業が軌道に乗りきる前のお客さんが多い • → アーキテクチャは素早くユーザーに価値を提供できるモノリス を選択することが多い • サービス分割的な設計はお客さんの事業フェーズ的に選択肢に 入ってこないことが多い • ※ 直接ユーザーに価値を提供しない部分に時間を取られる • サービス間でデータやり取りする用のAPIの開発 • サービスを跨いだトランザクションの考慮 • etc...

Slide 9

Slide 9 text

お品書き • 複数ユーザー種別の認証について考えることの重要性 • Djangoにおける複数ユーザー種別の認証 • 具体的な設計パターンの話 • 1.typeフィールドパターン • 2.認証モデル共有パターン • 3.認証モデル切り替えパターン • 4.独自実装パターン • 全体のまとめ 課題設定の話

Slide 10

Slide 10 text

複数ユーザー種別認証の設計につ いて考えることの重要性 ※ WEB開発一般の話

Slide 11

Slide 11 text

複数ユーザー種別認証の設計について考 え始めたきっかけ • 弊社にPJ横断的な相談部屋があり、そこによく複数ユーザー種 別認証の設計の相談が持ち込まれることに気づいた

Slide 12

Slide 12 text

考えてみた • 「なぜ複数ユーザー種別の認証」という要件によくエンカウン トするのか • → 偶然ではなく必然的な理由があるのでは?

Slide 13

Slide 13 text

考えてみた • 我々はWEBが主戦場のエンジニアなので、「WEBサービス」を 開発する仕事が多い • → 「サービス」という言葉の定義と関係があるのでは?

Slide 14

Slide 14 text

「サービス」という言葉の定 義って意外と考えたことがな い

Slide 15

Slide 15 text

サービスとは、一方が他方に 対して提供する行為や行動で ある コトラー&ケラーのマーケティング・マネジメント基本編 第3版 p246

Slide 16

Slide 16 text

コトラーの「サービス」という言葉の定 義から分かること サービスとは、一方が他方に対して提供する行為や行動である • 一方:サービスの提供者 • 他方:サービスの受け手 → 「サービス」には言葉の定義からして提供者と受け手がいる

Slide 17

Slide 17 text

【再掲】2つ以上のユーザー種別の例 • サービスの「提供者」と「受け手」という観点で改めて見てみ る サービス 提供者 システムの使い方 受け手 システムの使い方 ECサイト 販売者 • 商品登録 • 在庫変更 購入者 • カートに追加 • 購入 記事配信サイト ライター • 記事執筆 • 公開管理 購読者 • 記事にコメント • 有料記事の購入 ホテル予約サイト 事業者 • 予約管理 • 部屋情報の更新 宿泊者 • 予約 • キャンセル 提供者と受け手はシステムの使い方 (≒ユースケース)が大きく異なる

Slide 18

Slide 18 text

複数ユーザー種別認証の設計について考 えることの重要性 • 回答: 「複数ユーザー種別の認証」という要件はWEBサービス 開発において再現性が高いので、設計パターンとして考えてお く価値がありそう • 再現性が高い理由: 「サービス」という言葉の定義からして、 そもそも「提供者」と「受け手」という2つのユーザー種別を 含んでいるから

Slide 19

Slide 19 text

Djangoにおける複数ユーザー 種別の認証

Slide 20

Slide 20 text

「Django」という文脈で複数ユーザー種 別の認証を議論する意義 Djangoはauth.Userモデルを前提とした認証の仕組みが存在する • メリット: 1つのユーザー種別かつID/パスワード認証であればサード パーティライブラリを使わずにセキュアな認証を簡単に実装できる • デメリット: 「複数ユーザー種別の認証」のような単純にDjangoの レールに乗るだけでは実現できない要件が入ってくると制約にもな る → フレームワークの特性とどう付き合うかという観点が入ってくるの で「Django」という文脈で議論する意義がある

Slide 21

Slide 21 text

ここからDjangoでの設計パ ターンの紹介

Slide 22

Slide 22 text

紹介する設計パターン 1. typeフィールドパターン(非推奨) 2. 認証モデル共有パターン 3. 認証モデル切り替えパターン 4. 自前実装パターン ※ (参考)私の業務経験 • 2,4の設計で作られた本番稼働してるシステムにそれぞれ関わった経 験がある • 3は業務経験はないが社内でアイディアとしては時々聞くので PoC(Proof of Concept)を作って検証してみた

Slide 23

Slide 23 text

題材とするWEBサービスの要件 サンプルコードを示したりする上で題材とするWEBサービスの要 件が必要 • 以下のような2つのユーザー種別を持つミニマムのECサイトを 想定 • 販売者(Merchant) • 購入者(Customer) • それぞれのユーザー種別のみが可能な操作がある • 販売者: 商品登録 • 購入者: カートに追加

Slide 24

Slide 24 text

各パターンの説明へ 1. typeフィールドパターン(非推奨) 2. 認証モデル共有パターン 3. 認証モデル切り替えパターン 4. 自前実装パターン

Slide 25

Slide 25 text

1.typeフィールドパターン(非 推奨) ※ 非推奨な理由を軽く説明するだけ

Slide 26

Slide 26 text

typeフィールドパターンの概要 1. Userモデルをカスタマイズしてtypeフィールドを定義 2. typeフィールドにユーザー種別を設定 3. 各ユーザー種別固有のフィールドも全てUserモデルに定義す る

Slide 27

Slide 27 text

モデルの実装例 AbstractUserを継承 typeフィールドにユーザー種別を定義 販売者固有のフィールド 購入者固有のフィールド

Slide 28

Slide 28 text

typeフィールドパターンの問題点 • ①モデルの制約によるデータ保護ができない • (例)merchant_employee_no( 従業員番号) • → 販売者の場合は必ず必要なフィールド • →販売者と購入者でモデルを共有しているからフィール ドで必須制約がかけられない • merchant_employee_no = models.CharField("従業員番号", max_length=32, blank=True) • ②販売者/購入者それぞれに固有のフィールドを全てShopUserモ デルに持つことによるモデルの肥大化による保守性の低下を招 く

Slide 29

Slide 29 text

typeフィールドパターンの結論 • プロダクションレベルに導入するのは厳しい • 理由①:モデルの制約によるデータ保護ができない • 理由②:モデルの肥大化による保守性の低下

Slide 30

Slide 30 text

ここからプロダクションレベ ルでも実用的なパターン

Slide 31

Slide 31 text

2.認証モデル共有パターン

Slide 32

Slide 32 text

認証モデル共有パターンの概要 • AUTH_USER_MODELに渡すモデル(=認証モデル)を2つ以上のユー ザーモデルで共有するパターン 認証モデル(= AUTH_USER_MODELに渡す) ER図イメージ customerとmechantは username/passwordを持たずに authenticationテーブルに外部化 ※ auth_groups, auth_user_permissions などのDjango Admin用 のテーブルが紐づく

Slide 33

Slide 33 text

やること 1. Authenticationモデルの定義 2. MerchantモデルとCustomerモデルの定義 3. 新規会員登録機能の実装 4. ログイン機能の実装 5. [Option]独自ミドルウェアの実装 6. 独自LoginRequiredMixinの実装

Slide 34

Slide 34 text

1.Authenticationモデルの定義

Slide 35

Slide 35 text

Authenticationモデルの定義 • AbstractBaseUserを継承してAuthenticationという名前でモデル を定義する • → AuthenticationモデルをDjangoの認証モデルに設定する • AUTH_USER_MODEL = "{アプリ名}.Authentication" • https://docs.djangoproject.com/ja/5.1/topics/auth/customizing/# substituting-a-custom-user-model

Slide 36

Slide 36 text

モデルの実装 • モデル名: Authentication • AbstractBaseUserを継承

Slide 37

Slide 37 text

補足説明 • ①Authenticationという命名の理由 • ②なぜAbstractUserではなくAbstractBaseUserを継承するのか

Slide 38

Slide 38 text

①Authenticationという命名の理由 • なぜShopUserなどではないのか? • → 回答: あくまで認証情報であり、「ユーザー」ではないから xxxUserのような命名はそぐわない • →次のスライドでER図で説明

Slide 39

Slide 39 text

ER図 ユーザー主体(購入者) ユーザー主体(販売者) 認証情報 あくまで認証情報で あって「ユーザー」 ではない

Slide 40

Slide 40 text

①Authenticationという命名の理由 • Authenticationはあくまで「認証情報」であり、「ユーザー」で はない • → ShopUserのような命名にすると実態にそぐわない(テーブル定 義書やモデルのコードを読んだ人はユーザーが3種類いるよう に誤認してしまう) • → 「認証情報」という概念にマッチした名前にする ※ 命名大事

Slide 41

Slide 41 text

②なぜAbstractUserではなく AbstractBaseUserを継承するのか • 前提知識: Djangoの組み込みのユーザーモデルは以下の継承関 係の上に成り立っている • AbstractBaseUser > AbstractUser > User(デフォルト) • → 下の階層に行くほどDjango側で定義されているフィールドや メソッドが増える

Slide 42

Slide 42 text

②なぜAbstractUserではなく AbstractBaseUserを継承するのか • ①のAuthenticationの命名の理由と関連する • 前提: Authenticationの責務は認証情報を持つことなので、でき るだけ関係のない情報は保持したくない(理想はログインIDとパ スワードだけ) • → AbstractUserはlast_nameとfirst_nameを保持している • → これらは認証とは関係がないのでAuthenticationには持たせた くない • (アカウント作成に氏名は不要とか逆にミドルネームも考慮 したいとかあるかもしれない) • → AbstractBaseUserを継承

Slide 43

Slide 43 text

2.MerchantモデルとCustomer モデルの定義

Slide 44

Slide 44 text

MerchantモデルとCustomerモデルを定義 それぞれのユーザー種別に特有のフィールドを、 別々のモデルに分けて定義できる & 制約もかけら れるのがtypeフィールドパターンと比較した際の メリット

Slide 45

Slide 45 text

Merchant/CustomerからAuthenticationに OneToOneFieldを張る ※ on_deleteをSET_NULLにしておくと退会処理が実装しやすい (時間があったら後で解説)

Slide 46

Slide 46 text

[Option]Django Adminへの登録 • オペミス防止のためauthenticationをDjango Admin上で非表示に しておくのがおすすめ

Slide 47

Slide 47 text

[Option] Authentication側にユーザー種別 を判別するメソッドを持たせると便利 → 後で使う

Slide 48

Slide 48 text

3.新規会員登録機能の実装

Slide 49

Slide 49 text

新規会員登録機能を実装 • 販売者側と顧客側それぞれのユーザーの新規会員登録機能を別 のURLで提供する • 販売者側を例に解説

Slide 50

Slide 50 text

新規会員登録Viewの実装 • UserManagerにcreate_user()という 便利メソッドがあるので使う • https://docs.djangoproject.com/j a/5.1/topics/auth/default/#creati ng-users • Autentication.usernameがDjangoの仕 様でユニークなので、重複を防ぐ ためにprefixをつけた方がいい • -> やらない場合「 販売者側で 使用済みのユーザー名を購入 者ユーザーが登録できない」 という事象が発生(仕様として 許容するかは要件次第) ②作成したAuthenticationを渡して Merchantを作成 ①Authentication を作成

Slide 51

Slide 51 text

4.ログイン機能の実装

Slide 52

Slide 52 text

ログインViewの実装 • authenticate()関数とlogin() 関数を使用 • https://docs.djangoproj ect.com/ja/5.1/topics/a uth/default/#how-to- log-a-user-in • authenticate()関数に渡す ユーザー名にprefixをつけ る

Slide 53

Slide 53 text

5.[Option]独自ミドルウェア実 装

Slide 54

Slide 54 text

なぜ独自ミドルウェアを実装した方がい いのか? • Viewの中で毎回以下のようなコードを書くことになる • if request.user.is_authenticated and request.user.is_merchant: • merchant = request.user.merchant • merchant.do_something() • → 処理の本質ではないコードを毎回書かないといけなくて無駄 なコスト & 可読性・保守性が下がる • → Viewに渡る前に共通処理にして見通しをよくしたい

Slide 55

Slide 55 text

独自ミドルウェア実装 • AuthenticationのForeign Keyで取得 したユーザーモデルをrequestオ ブジェクトに詰めてあげる • Viewからは request.merchant.do_something() のように使える • request.userはdelしてViewから参照できなく してしまうとよさそう(案件での実績なし) • 理由:コーディング規約で「request.userは使 わないこと」と書くより、コードレベルで参 照できなくした方が強制力が強い

Slide 56

Slide 56 text

【補足】勝手にrequestにフィールド生や していいのか? • 結論: OK • そもそもdjnago.http.HttpRequestクラスにuserというフィールド は存在しない • Django本体のソースを読むと、AuthenticationMiddlewareの中 でrequestにuserフィールドを追加している • https://github.com/django/django/blob/main/django/contrib/au th/middleware.py

Slide 57

Slide 57 text

【補足】Djangoがrequest.userを追加して いる仕組み • → Pythonは「クラス定義には存在しないフィールドでも、イン スタンス生成後に動的に追加が可能」という言語仕様を持って いる • https://docs.python.org/ja/3.11/tutorial/classes.html#instance- objects • → この仕様を利用してRequestのクラス定義には存在しないuser フィールドを後から追加している

Slide 58

Slide 58 text

【補足】勝手に詰め替えていいの? • 結論: OK • → request.merchantとrequest.customerの追加もDjango本体の実 装とやってることは同じ (=我流のハックじゃないよ)

Slide 59

Slide 59 text

作成したMiddlewareの登録 request.userを設定するのは AuthenticationMiddlewareなの で、それよりも後ろに追加す る

Slide 60

Slide 60 text

6.独自LoginRequiredMixin実装

Slide 61

Slide 61 text

独自LoginRequiredMixin実装 • LoginRequiredMixinのMerchantとCustomer版を作る • https://docs.djangoproject.com/ja/5.1/topics/auth/default/#the- loginrequiredmixin-mixin • ※ 関数ビューを使う場合はlogin_requiredデコレータのMerchant とCustomer版が必要 • https://docs.djangoproject.com/ja/5.1/topics/auth/default/#the- login-required-decorator

Slide 62

Slide 62 text

独自LoginRequiredMixin実装 • 独自Middlewareでmerchantとcustomerに詰め 替えているので、request.merchant/customerの 存在チェックするだけでOK

Slide 63

Slide 63 text

使い方も公式のLoginRequiredMixinと同じ 商品登録画面のビュー (販売者しかアクセスできない)

Slide 64

Slide 64 text

ここまでが認証モデル共有パ ターンの基本的な説明

Slide 65

Slide 65 text

発展的なトピック

Slide 66

Slide 66 text

発展的なトピック 1. ユーザー種別ごとに認証方式を変えたい 2. 退会の実装(時間があったら)

Slide 67

Slide 67 text

1.ユーザー種別ごとに認証方式を変えた い • Authenticationモデルを共有しているので、「片方のユーザー種 別の認証方式だけを変えたい場合に困るのでは?」という疑問 が出てくる • (例) 顧客ユーザー側だけ、「電話での本人確認が済んでいる か」を認証の条件に追加したい • → 回答: 独自の認証バックエンドを実装する

Slide 68

Slide 68 text

認証バックエンドとは? • Djangoのログイン処理時に裏で呼ばれている、認証の条件を実 装したもの • DjangoのデフォルトではModelBackendが使われている • https://docs.djangoproject.com/ja/5.1/ref/contrib/auth/#django.c ontrib.auth.backends.ModelBackend

Slide 69

Slide 69 text

認証バックエンドの独自実装 • 認証バックエンドは独自実装したものを使うことができる • https://docs.djangoproject.com/ja/5.1/topics/auth/customizing/# writing-an-authentication-backend • 最低限やることは以下 • django.contrib.auth.backends.BaseBackendを継承したクラスを 定義する • authenticate()とget_user()メソッドを実装する

Slide 70

Slide 70 text

(例)顧客側のみ認証条件を追加した独自 認証バックエンドの実装 ID/パスワード認証に認 証条件を追加するので、 ModelBackendを継承し て親のauthenticateの結 果を使う 顧客ユーザーの場合に電 話での本人確認済みかの チェック処理を追加

Slide 71

Slide 71 text

独自認証バックエンドの登録 • settings.pyのAUTHENTICATION_BACKENDSに配列で登録する • 認証バックエンドは複数登録できるので、肥大化してきたら ユーザー種別ごとに認証バックエンドを分けることも可能 • ※ 認証に成功するまで上から順に呼ばれる仕組み

Slide 72

Slide 72 text

2.退会の実装(時間があったら)

Slide 73

Slide 73 text

退会機能って意外と難しい • 退会フラグは避けたい • 理由1: 退会フラグ = Falseをつけ忘れたら即重大なバグへ • 理由2: 個人情報がDB上に丸っと残るのでサービスによって は個人情報保護ポリシーなどに抵触する可能性がある • 全部物理削除 & FKにSET_NULLする方式だと情報の追跡可能性に 問題が出る • この商品登録したの誰だろう? • 退職した誰かっかっぽいけど分からないですね... • 物理削除する情報と残す情報を分けるのが理想 • → 削除する情報と残す情報の仕分けが難しい

Slide 74

Slide 74 text

認証モデル共有パターンでは割ときれい に表現できる • 退会 = authenticationテーブルのレコード削除で表現できる • メリット1: 認証情報は物理削除するから、退会したユーザーが エンバグでログインできる余地がない • メリット2: merchant、customerは残るので情報の追跡可能性は 維持できる

Slide 75

Slide 75 text

モデルとビューの実装 SET_NULLでAuthentication削 除時にカスケードデリート されないようにする Authenticationの有 無で退会を表現す るメソッドを実装 退会処理では authenticationのみ を物理削除

Slide 76

Slide 76 text

追跡可能性は維持できる Merchant側に保持 した情報は残る

Slide 77

Slide 77 text

認証モデル共有パターンのま とめ

Slide 78

Slide 78 text

メリット/デメリット • メリット • Djangoの認証機能を使える • 擬似的に複数種別のユーザーを表現しているだけで内部的に は1つの認証ユーザー • → Djangoのレールに乗っているから安心 • カスタマイズ実装が必要な箇所はあるがそんなに難しくない • デメリット? • フレームワークの制約でDB設計が歪む • -> どういうことか次のスライドで説明

Slide 79

Slide 79 text

Djangoでの実装を意識せずに素直にテーブル 設計から考えると、左のようになるはず 素直にテーブル設計した場合 Djangoの制約に引きずられている図 • それぞれのユーザーテーブルが認証情報を持つ • authenticationテーブルは不要 • Djangoの制約に引きづられている部分

Slide 80

Slide 80 text

DB設計がフレームワークの制約に引きず られる気持ち悪さ ①テーブル定義書を作る ②テーブル定義書をインプットにModelやDaoを実装する というガチガチのウォーターフォールに馴染んでる人(過去の私) には、「DB設計がフレームワーク(=実装)の制約に引きずられて いる」のが生理的に気持ち悪いかもしれない

Slide 81

Slide 81 text

本当にデメリットなのか? • 認証テーブルを共有する設計の「具体的なデメリット」を挙げ るのは意外と難しい • ※ 具体的なデメリット=要件が満たせない、保守性が下がる、 性能に影響が出る、etc... • 少なくともサービスがある程度成長するまでは認証モデル共有 パターンで十分では?

Slide 82

Slide 82 text

認証モデル共有パターンのまとめ • Djangoのレールに乗っていて安心 • 致命的な欠点がなさそう • → 実用的で優秀なパターンだと思う

Slide 83

Slide 83 text

(補足)認証モデル共有パターンの別のア プローチ • Django中級者以上向けの有名な本である『Two Scoops of Django』の 著者の方が動画(2つで約30分)で紹介しているアプローチ • Multiple User Types With Custom Data Fields | Django • https://youtu.be/f0hdXr2MOEA • Multiple User Types With Custom Data Fields | Django(続編) • https://youtu.be/V3zRZ6XRols • プロキシモデルを使用したアプローチ • https://docs.djangoproject.com/ja/5.1/topics/db/models/#proxy- models • この動画も観て本発表と比較してみると面白いと思います!

Slide 84

Slide 84 text

3.認証モデル切り替えパター ン

Slide 85

Slide 85 text

※ このパターンは実務経験がな いので私が自分でPoC実装したレ ベルの知見しか話せません → その前提で聞いてください

Slide 86

Slide 86 text

認証モデル切り替えパターンの概要 • ユーザー種別分の環境を用意する • AUTH_USER_MODELに渡すモデルを環境ごとに切り替える • 前提: ソースコードは1つ イメージ

Slide 87

Slide 87 text

生成されるテーブルのイメージ • 各ユーザーテーブ ルにusernameと passwordを持つ • →auththentication テーブル的なもの は不要 • Django Admin用に はauth.Userをその まま使用 customer, merchant, auth_userの3ユーザー種別のテーブルが作成される

Slide 88

Slide 88 text

やること 1. MerchantモデルとCustomerモデルの定義 2. 各環境にAUTH_USER_MODELを設定 3. 新規会員登録機能の実装 4. ログイン機能の実装 5. Djangoのアプリを分ける 6. ユニットテスト 7. マイグレーション 本パターン特有のトピック

Slide 89

Slide 89 text

1.MerchantモデルとCustomer モデルの定義

Slide 90

Slide 90 text

Merchantモデルの定義 • AbstractBaseUserを継承 • 今回もAbstractUserではない(理由は後述) • Merchant、Customer自体が認証モデル(=AUTH_USER_MODELに渡 すモデル)になるのでOneToOneFieldの定義は不要 ※ Customerモデルも同様なので省略

Slide 91

Slide 91 text

なぜAbstractUserではないのか? • 認証モデル共有パターンの時は「よい設計」という観点で AbstractBaseUserを採用 • 認証モデル切り替えパターンではそもそもAbstractUserを継承 するとエラーになる

Slide 92

Slide 92 text

【解説】なぜAbstractUserではないのか? • AbstractUserはdjango.contrib.auth.modelsのGroup、Permissionと ManyToManyFieldの関連がある PermissionsMixinを継承

Slide 93

Slide 93 text

【解説】なぜAbstractUserではないのか? • AbstractUserを継承した3つのユーザーモデルがGroup、 Permissionとそれぞれ関連を作成しようとしてrelated_name重複 エラーが起きる $ docker compose exec admin_app python manage.py makemigrations SystemCheckError: System check identified some issues: ERRORS: auth.User.groups: (fields.E304) Reverse accessor 'Group.user_set' for 'auth.User.groups' clashes with reverse accessor for 'shop.Customer.groups'. HINT: Add or change a related_name argument to the definition for 'auth.User.groups' or 'shop.Customer.groups' (...省略)

Slide 94

Slide 94 text

2.各環境にAUTH_USER_MODEL を設定

Slide 95

Slide 95 text

各環境にAUTH_USER_MODELを設定 • 環境ごとにAUTH_USER_MODELに渡すユーザーモデルを切り替 える • サンプル実装コードではDocker Composeで異なるポートにコン テナを3つ建てて擬似的に再現 ※ イメージ

Slide 96

Slide 96 text

本番環境での稼働時のイメージ • 環境変数の種類分だけ実行環境(≒サーバー,コンテナ)を用意し て、ドメインやプロキシサーバーで振り分ける

Slide 97

Slide 97 text

3.新規会員登録機能の実装

Slide 98

Slide 98 text

新規会員登録の実装 • ModelがAuthenticationとMerchantに分かれない分、認証モデル 共有パターンよりもシンプルに実装できる

Slide 99

Slide 99 text

4.ログイン機能の実装

Slide 100

Slide 100 text

ログイン機能の実装 • request.userにそのままMerchantモデルが入ってくる • → DjangoのジェネリックビューのLoginViewが使える • https://docs.djangoproject.com/ja/5.1/topics/auth/default/#djang o.contrib.auth.views.LoginView • → 認証モデル共有パターンよりも実装コストが少し減る

Slide 101

Slide 101 text

5.Djangoのアプリを分ける

Slide 102

Slide 102 text

Djangoのアプリを分ける • 前提: 一般的な話としてDjangoのアプリは適切な粒度で分けたほ うがいい(tell-kさんのDjangoCongress JP 2022の発表を参照) • 『Djangoのアプリはどういう単位で作るべきか?』 • https://tell-k.github.io/djangocongressjp2022/ • ただ特に「認証モデル切り替えパターン」では認証の主体にな るユーザー種別ごとにアプリを分けたほうがいい理由がある

Slide 103

Slide 103 text

「認証の主体になるユーザー種別ごとに アプリを分けるとはどういうこと」か? merchant/views.py customer/views.py request.userが Merchant前提 のViewは merchantアプ リに定義 request.userが Customer前提 のViewは customerアプ リに定義

Slide 104

Slide 104 text

認証の主体になるユーザー種別ごとにア プリを分けたほうがいい理由 • 理由1: request.userがどのユーザー種別を指しているのかコード から読み取れないので見分けられるものが必要 • 理由2: ユニットテストの実行(6.ユニットテストの話の時に後述)

Slide 105

Slide 105 text

request.userがどのユーザー種別を指している のかコードから読み取れないので見分けられ るものが必要 認証モデル共有パターン 認証モデル切り替えパターン ソースコードから 対象のユーザー種 別が分かる • コードを読んで も、どのユー ザー種別を対象 にした処理か パッと分からな い • → Djangoアプリ の所属で判別で きるのがより重 要

Slide 106

Slide 106 text

6.ユニットテスト

Slide 107

Slide 107 text

前提 • ちゃんとユニットテストを書いた場合、以下の3種類のテスト が出来上がるはず • AUTH_USER_MODEL=Merchantで実行される前提のテスト • AUTH_USER_MODEL=Customerで実行される前提のテスト • AUTH_USER_MODEL=auth.Userで実行される前提のテスト

Slide 108

Slide 108 text

ユニットテスト実行時の課題 • ユニットテストを実行するとその実行環境の AUTH_USER_MODELが使われる • → 全てのテストを一括実行した場合、前提とする AUTH_USER_MODELが異なるテストは全て失敗する • (例) docker compose exec merchant_app python manage.py test を 実行 • → AUTH_USER_MODELがCustomer or auth.User前提のテストは 全て失敗

Slide 109

Slide 109 text

ユニットテスト実行時の課題の対策 • 方法1: 全てのテストに@override_settings="shop.Customer"など をつける • 方法2: テスト実行をAUTH_USER_MODELの種別ごとに分ける

Slide 110

Slide 110 text

方法1: 全てのテストに@override_settings をつける • メリット • テスト実行が1コマンドで済む • デメリット • 毎回デコレータを付与するのが手間 • 環境変数が増えたときに全テストにデ コレータ追加する必要がある

Slide 111

Slide 111 text

テスト実行をAUTH_USER_MODELの種別 ごとに分ける • AUTH_USER_MODELがMerchantモデル前提のテスト • docker compose exec merchant_app python manage.py test merchant/ • AUTH_USER_MODELがCustomerモデル前提のテスト • docker compose exec customer_app python manage.py test customer/ • ※前提: AUTH_USER_MODELごとにテストケースがディレクトリ でグルーピングされている必要がある • → さっきのDjangoアプリを分けるのが大事という話と繋がる

Slide 112

Slide 112 text

方法2: テスト実行をAUTH_USER_MODELの 種別ごとに分ける • メリット • デコレータの付与が不要 • デメリット • 全テスト実行する場合にコマンドを複数回実行する必要があ る • →全テスト実行はCIでやればいいから大きなデメリットではな いので、方法2の方がよさそう(私の意見)

Slide 113

Slide 113 text

7.マイグレーション

Slide 114

Slide 114 text

マイグレーション適用時の注意 ※ 認証モ デル切り替えパターン特有の話 • AUTH_USER_MODELに渡しているモデルによってマイグレー ションの実行結果が変わる場合がある • → マイグレーション適用の実行環境が大事

Slide 115

Slide 115 text

意図しないマイグレーション結果の例 • AUTH_USER_MODELがCustotmerモデルの状態で初回マイグレー ション適用してしまう • docker compose exec customer_app python manage.py migrate • → customerテーブルにDjango Admin関連のテーブル群が関連し てしまう

Slide 116

Slide 116 text

本当はこうしたかったはず • AUTH_USER_MODEL=auth.Userで初回マイグレーション適用する • docker compose exec admin_app python manage.py migrate • → 期待通りauth_userテーブルにDjango Admin関連のテーブル群 が関連する

Slide 117

Slide 117 text

【解説】なぜ起きたのか? • django.contrib.authの初回マイグレーションファイルが AUTH_USER_MODELを参照している • https://github.com/django/django/blob/main/django/contrib/aut h/migrations/0001_initial.py • → AUTH_USER_MODELの値によって結果が変わってしまう

Slide 118

Slide 118 text

認証モデル切り替えパターン のまとめ

Slide 119

Slide 119 text

一見メリットが多そうに見える • ユニットテストやマイグレーションで多少気をつけるべき点は ある • が、request.userをそのまま使えるので、全体的に「認証モデル 共有パターン」よりカスタマイズ実装する量が減っている • テーブル設計的にもmerchantとcustomerを完全に独立させられ ていてきれい(<->認証モデル共有パターンのDjangoの制約に引 きづられたauthenticationテーブル) • AUTH_USER_MODELで切り替えるのもカッコいい • → 「認証モデル共有パターン」も十分実用的なパターンだった がよりベターにみえる

Slide 120

Slide 120 text

弊社の知見者の方々の意見 • 理論上やPoC実装レベルだとできそうに見えて、実際の案件に 適用すると、いろいろはまるらしい • shimizukawaさんのメモ • https://scrapbox.io/shimiz ukawa/Djangoの認証機構 で複数のユーザー種別を 扱う

Slide 121

Slide 121 text

なんでなんだろう

Slide 122

Slide 122 text

Django側が想定しているAUTH_USER_MODEL の使い方の範疇を超えている • AUTH_USER_MODELは自前でカスタマイズしたUserモデルを認 証モデルとして設定可能にするためのもの • → 1つのソースコードで環境ごとに認証ユーザーを切り替える 用途まではユースケースとして公式に書かれていない • https://docs.djangoproject.com/ja/5.1/topics/auth/customizing/# substituting-a-custom-user-model • Django doesn't have multiple users. (Jazzband所属の人の回答) • https://stackoverflow.com/questions/25841712/django-best- approach-for-creating-multiple-type- users/25842236#25842236

Slide 123

Slide 123 text

認証モデル切り替えパターンのまとめ • 一見「認証モデル共有パターン」よりベターにみえるが実案件 に適用するとハマりどころがあるらしい • Django側が想定しているAUTH_USER_MODELの使い方の範疇を超 えている(=ややハック気味...?) • ※ チームにDjangoの熟練者が多くてハマっても乗り越えて行く 覚悟があるなら採用してもいいかも...?

Slide 124

Slide 124 text

4.自前実装パターン

Slide 125

Slide 125 text

自前実装パターンの概要 • django.contrib.authのソースコードを参考にして、認証機能を自 前実装する • Djangoが提供している関数は使う前提(≠完全なフルスクラッチ) • 例: make_password, session.cycle_key()など • auth.UserはDjango Admin専用で使う(Django Adminは使うなら)

Slide 126

Slide 126 text

そもそもセキュリティ観点で独自実装は 選択肢としてありなのか? • 回答: おすすめしない(後述)が論外と言えるほどではない • 例えば、IPAはセッション管理の仕組みの独自実装は推奨してい ない • 安全なウェブサイトの作り方 - 1.4 セッション管理の不備 • 但し、本パターンは本当にゼロから認証機能を自作するわけで はない • django.contrib.authのソースコードを参考にする • Djangoが提供している認証機能用の関数は使う

Slide 127

Slide 127 text

本パターンの具体的な実装例 は示すと大変なのでやりませ ん

Slide 128

Slide 128 text

モデル実装例だけ Djangoの認証モデルと 関係ない素朴なModel を定義する

Slide 129

Slide 129 text

メリット • Djangoの認証の仕組みから距離を取れる • → フレームワークへの依存を抑えていると言える? • Djangoの認証機能で対応できないセキュリティ要件にも対応で きる • → 今のところそういう要件に出会ったことがない

Slide 130

Slide 130 text

自前実装することへの個人的な意見 • 前提: 私自身も自前実装された案件に関わった経験もある • 結論: → あんまりおすすめはしない • 以下のshimizukawaさんのメモに理由はだいたい書いてある • https://scrapbox.io/shimizukawa/Djangoの認証を独自に実装し てはいけない

Slide 131

Slide 131 text

私が考えるおすすめしない理由 • 初期開発フェーズ • 運用フェーズ • プロジェクト全体的な観点

Slide 132

Slide 132 text

私が考えるおすすめしない理由(初期開発 フェーズ) • django.contrib.authのソースコードを読み解く工数がかかる • → django.contrib.authはセキュリティ対策面で作り込まれて いるので、どこまで取り入れるかの判断で悩んで時間取られ そう • 参考にして実装する工数 & 実装過程でバグが混入するリスク • セキュリティテストを重点的にやる必要があるのでテスト工数 が増える

Slide 133

Slide 133 text

私が考えるおすすめしない理由(運用 フェーズ) • django.contrib.authにセキュリティパッチが入った時に、Django のバージョンを上げるだけでは追従できない場合がある • ※ パッチが入った箇所による

Slide 134

Slide 134 text

私が考えるおすすめしない理由(プロジェ クト全体的な観点) • 認証機能はそれ自体がユーザーに価値を提供するものではない • → 特にサービスが軌道に乗るまでは色んな機能を作ってユー ザーのニーズを知るとかのほうが大事 • → 認証機能を自作するリソースで他の機能作ったり、サービス インの時期を早めたりした方がいいのでは?

Slide 135

Slide 135 text

(参考)弊社で過去に自前のユーザー認証 ライブラリを作ろうとした試み • https://github.com/beproud/django-newauth

Slide 136

Slide 136 text

紹介したいパターンは終わり

Slide 137

Slide 137 text

全体的なまとめ

Slide 138

Slide 138 text

今回紹介したパターン 1. typeフィールドパターン(非推奨) 2. 認証モデル共有パターン 3. 認証モデル切り替えパターン 4. 自前実装パターン

Slide 139

Slide 139 text

私が設計担当者ならどれから考えるか • まずは認証モデル共有パターンから考える • 多くのケースで特に不満はなさそう • 「1つの認証ユーザー」というDjangoのレールにも乗ってるので 安心 • 認証モデル切り替えパターン: • ハック気味なパターン(主観)なので見えてないリスクがありそう で怖い • 認証モデル共有パターンと比較して得られるメリットを考えた時 に、リスクを冒すに値するほどのメリットはなさそう • 自前実装パターン: • 工数を割いたりセキュリティリスクを背負ったりしてまでやる価 値のあるケースは少なそう

Slide 140

Slide 140 text

最後に • 本発表はベストプラクティスの提示ではない • Djangoの複数ユーザー認証の設計の議論の叩き台に便利に使っ てもらえたらうれしい • 以下のようなフィードバックもらえるとうれしいです • 各パターンに関して私が言及できてないメリット/デメリッ ト • 業務でこのパターン使ったよ(感じたメリデメもあれば) • 他にこういうパターンもありそう • etc... • ありがとうございました!

Slide 141

Slide 141 text

(おまけ) django.contrib.authの ソースコードを読んでて面白 かったところ ※ django.contrib.authのソースコードリーディングは勉強になるのでおすす め

Slide 142

Slide 142 text

constant_time_compare関数 • constant_time_compare関数 • https://github.com/django/django/blob/main/django/utils/crypto .py • 文字列長によって文字列の比較の実行時間が異なることを利用 した攻撃があり、その対策のための文字列長に依存しない比較 関数らしい • https://stackoverflow.com/questions/35910793/in-crypto- constant-time-compare-why-the-time-taken-is-independent-of- the-number

Slide 143

Slide 143 text

一見意味のないハッシュ化処理 • verify_passwordの中でpasswordがfalsy(=ユーザーが存在しない場 合)もランダム文字列をハッシュ化してからFalseを返している

Slide 144

Slide 144 text

一見意味のないハッシュ化: 理由 コードコメントに解説が書いてある • ユーザーが存在する場合だけハッシュ化すると、ハッシュ化す る分時間がかかるから処理速度(≒レスポンスタイム)に差が出る • → 攻撃者に「ユーザーが存在しない」というヒントを与えてし まうリスクがある • → 対策でユーザーが存在しない場合もハッシュ化を実行してい る

Slide 145

Slide 145 text

参考資料

Slide 146

Slide 146 text

複数ユーザー種別認証の設計について考 えることの重要性 • https://www.maruzen-publishing.co.jp/book/b10111867.html • 『コトラー&ケラーのマーケティング・マネジメント基本編 第3版』丸善出版 • 「サービス」の定義の引用に使用

Slide 147

Slide 147 text

typeフィールドパターン • https://www.youtube.com/watch?v=V3zRZ6XRols • 『Multiple User Types With Custom Data Fields | Django』 • Django中級者以上向けの有名な本である『Two Scoops of Django』の著者の方の動画 • 動画内では推奨しないパターンとして紹介されている • 4:30頃から

Slide 148

Slide 148 text

認証モデル共有パターン • https://youtu.be/f0hdXr2MOEA • 『Multiple User Types | Django』 • Django中級者以上向けの有名な本である『Two Scoops of Django』の著者の方の動画 • 続編: https://www.youtube.com/watch?v=V3zRZ6XRols • https://nwpct1.hatenablog.com/entry/django-auth-patterns • 『 Django における認証処理実装パターン』 • c-bataさんの記事 • ユーザーモデルのカスタマイズ方法や認証バックエンドの話 など勉強になる

Slide 149

Slide 149 text

認証モデル共有パターン • https://speakerdeck.com/moro/identifying-user-idenity • 『Identifying User Idenity』 • Kaigi on Rails 2024の発表 • usersテーブルとuser_credentialsテーブルを分ける設計や退会 の表現方法などのアイディアを参考にさせて頂いた

Slide 150

Slide 150 text

認証モデル差し替えパターン • https://github.com/shimizukawa/django-multiple-type-user-auth • 弊社のshimizukawaさんの同パターンのPoC実装 • https://tell-k.github.io/djangocongressjp2022/ • 『Djangoのアプリはどういう単位で作るべきか?』 • 「5.Djangoのアプリを分ける」で言及した弊社tell-kさんの DjangoCongress JP 2022の発表 • https://stackoverflow.com/questions/25841712/django-best- approach-for-creating-multiple-type-users/25842236#25842236 • “django best approach for creating multiple type users”という質 問へのJazzband所属の人の回答

Slide 151

Slide 151 text

自前実装パターン • https://www.ipa.go.jp/security/vuln/websecurity/session- management.html • 『安全なウェブサイトの作り方 - 1.4 セッション管理の不備』 • 引用したIPAのドキュメント • https://scrapbox.io/shimizukawa/Djangoの認証を独自に実装してはいけ ない • shimizukawaさんがDjangoの認証を独自で実装すべきではないと考 えている理由のメモ • https://github.com/beproud/django-newauth • 弊社でDjangoの認証ライブラリの自作を試みたもの

Slide 152

Slide 152 text

私のサンプル実装コード • 認証モデル共有パターン • https://github.com/delhi09/multi-user-one-to-one-field • 認証モデル差し替えパターン • https://github.com/delhi09/multi-user-auth_user_model