🆔
[Django] cuidを使って共有リンクを推測しづらくする
背景
あるアイテムを誰かに共有するためのリンクを作る時、
https://sample.com?item_id=1
のように、連番のIDを含むリンクだと非公開なアイテムを推測される恐れがあります.
uuidのような非連続なIDを利用することでこの問題を回避できます.
https://sample.com?item_uuid=gsahpri739b..
対応
どのidを使うか検討
非連続なidにも種類があるため、ユースケースに沿ったものを選択します。
ID名 | メリット | デメリット |
---|---|---|
uuid | ||
ulid | ||
cuid | ||
nanoid |
モデルにcuidを追加
pip install cuid2
from cuid2 import cuid_wrapper
CUID_GENERATOR = cuid_wrapper()
class Item(models.Model):
uid = models.CharField(
max_length=24,
unique=True,
editable=False,
default=CUID_GENERATOR,
)
defaultにはgeneratorを指定する必要があることに注意が必要です。
もし
# 間違った例
default=CUID_GENERATOR()
とした場合、固定のCUIDがdefault値になり、Unique制約に違反してエラーになります。
マイグレーションファイルの修正
データベースにはアイテムが登録された状態で後からユニーク制約のあるカラムを追加するには、少しコツが必要でした。上記のドキュメントに詳しく書いてあります。
3つのマイグレーションファイルが必要になります。
- cuidフィールドをnull=Trueとして追加
- 既存のcuidフィールドにcuidを挿入する
- cuidフィールドをunique=Trueに変更する
1つ目
docker compose exec api python manage.py makemigrations
from django.db import migrations, models
from cuid2 import cuid_wrapper
CUID_GENERATOR = cuid_wrapper()
class Migration(migrations.Migration):
dependencies = [
("api", "0008"),
]
operations = [
migrations.AddField(
model_name="item",
name="cuid",
field=models.CharField(
max_length=24,
null=True,
editable=False,
default=CUID_GENERATOR,
),
),
]
2つ目
最初に空のMigrationファイルを生成します.
docker compose exec api python manage.py makemigrations api --empty
from django.db import migrations
from cuid2 import cuid_wrapper
CUID_GENERATOR = cuid_wrapper()
def gen_cuid(apps, schema_editor):
Item = apps.get_model("api", "Item")
for row in Item.objects.all():
row.cuid = CUID_GENERATOR()
row.save(update_fields=["cuid"])
class Migration(migrations.Migration):
dependencies = [
("api", "0009"),
]
operations = [
migrations.RunPython(gen_cuid, reverse_code=migrations.RunPython.noop),
]
3つ目
docker compose exec api python manage.py makemigrations api --empty
from django.db import migrations, models
from cuid2 import cuid_wrapper
CUID_GENERATOR = cuid_wrapper()
class Migration(migrations.Migration):
dependencies = [
("api", "0010"),
]
operations = [
migrations.AlterField(
model_name="item",
name="cuid",
field=models.CharField(
max_length=24,
unique=True,
editable=False,
default=CUID_GENERATOR,
),
),
]
マイグレートを実行して、既存のアイテムにcuidが割り当てられていることを確認しましょう。
python manage.py migrate
GraphQLクエリに反映
class Query(graphene.ObjectType):
item = graphene.Field(
SharedItemType,
cuid=graphene.String(),
)
def resolve_item(self, info, cuid):
return Item.objects.get(cuid=cuid)
Itemを作るMutationのテスト
cuidを追加した影響でItemを作れなくなる場合があるのでテストします。
ユニーク制約に違反しないことを確認するため、3回アイテムを作成できることを確認します。
import graphene
from graphene_django.utils.testing import GraphQLTestCase
from api.schema import Mutation, Query
from api.models import User
class TestItemMutation(GraphQLTestCase):
def setUp(self):
super().setUp()
self.schema = graphene.Schema(query=Query, mutation=Mutation)
self.me = User.objects.create(
username="me",
email="me@gmail.com",
password="password",
)
# 3回連続でアイテムが作成できることを確認する
def test_normal_1(self):
for idx in range(3):
# Act
query = """
mutation createItem {
createItem {
item {
id,
}
}
}
"""
response = self.schema.execute(
query,
context_value=graphene.Context(user=self.me),
)
# Assert
expected = {
"createItem": {
"item": {
"id": str(idx + 1),
}
}
}
self.assertDictEqual(response.data, expected)
参考
Discussion