Django側、フロント側のコードを必要最低限にして、Django Rest Frameworkの使い方を理解するのが目的です。どのようにデータを送ったらDjangoがデータベースに書き込むのかを明らかにしたいと思います。Html、Javascriptの部分はAIに聞いて作ってます(私が苦手なので)。
Djangoでindex.htmlを配信すればフロント側でサーバーを立ち上げる必要がない。また入力フォームが来たところと、回答先が違うとCorsの問題が発生するが、その問題も発生せず扱いやすいと思います。
また非同期通信の必要性についても実感できる内容になっています。
注意:クロスサイト・スクリプティングの部分も読んで別途対策していただかないと危険です。そのままコードを本番で使うのは危険です。
Django 4.2.2
djangorestframework 3.14.0
完成品 初めてのDjango Rest Framework(Railwayにデプロイ)
初めてのDjango Rest Framework(Renderにデプロイ。立ち上がるまで数分かかることがあります。)
Django側
Django側は、こちらを使わせていただきました。シンプルで分かりやすいです。
【Python】Django REST Framework(DRF)を使ってWeb APIを自作してみる
そのままでは、動かない部分があるので補足します。
アプリケーションを作成
python3 manage.py drf_test_app
→python3 manage.py startapp drf_test_app
3.Router
from drf_test_app.api_views import UserInfoViewSet
→from drf_test_app.views import UserInfoViewSet
簡単にまとめると
Serializerは、どのモデルを使うか指定。
ViewSetsは、モデルのオブジェクトをquerysetに代入。serializer_classにSerializerで作ったクラスを代入。
Routerは、urlの定義。
この3つは、参考サイトの具体例に自分のmodels.pyで定義したモデルクラスとurlを当てはめればいい感じです。(今回、両方ともuserInfoになっている。)
そうすることでフロント側から下の各URLにHTTPメソッドでリクエストすれば、データベースへ作成(Create)、読み出し(Read)、更新(Update)、削除(Delete)の頭文字をとったCRUDできるわけです。
CRUD機能 | HTTP メソッド | URL |
---|---|---|
Read(全レコード) | GET | /api/userInfo |
Create | POST | /api/userInfo |
Read(単一レコード) | GET | /api/userInfo/(pk) |
Update(一部レコード) | PUT | /api/userInfo/(pk) |
UPDATE(全レコード) | PATCH | /api/userInfo/(pk) |
DELETE | DELETE | /api/userInfo/(pk) |
HTMLでformを作ってデータをPOSTしてみる
具体的に/api/userInfoにPOSTしてみます。
DjangoでHTMLを表示させる
フロント側の入力formを用意します。Djangoで入力form付きのindex.htmlを配信するようにします。
Djangoでindex.htmlを配信する基本は、こちらも参考にしてみてください。
以下のように修正、追加してください。
api_test/api_test/urls.py
urlpatterns = [
path('admin/',admin.site.urls),
# defaultRouter をinclude する
path('api/',include(defaultRouter.urls)),
path('',include("drf_test_app.urls")),#追加
]
アプリ(drf_test_app)のurls.pyは最初ないので
drf_test_appを右クリックして新規ファイル作成でurls.py作成
中身を以下にします。
from django.urls import path
from . import views
app_name = "drf_test_app"
urlpatterns = [
path('', views.HajimeteView.as_view(), name="index"),
]
views.pyに以下追加
from django.shortcuts import render
from django.views import View
class HajimeteView(View):
def get(self, request, *args, **kwargs):
return render(request,"drf_test_app/index.html")
drf_test_appを右クリック、新規ディレクトリでtemplatesディレクトリ作成
できたtemplatesを右クリック、drf_test_appディレクトリ作成
templates/drf_test_appを右クリックして新規ファイル作成でindex.html作成
中身を以下にします。
<!DOCTYPE html>
<html lang=”ja”>
<head>
<meta charset=”UTF-8″>
<title>初めてのDjango</title>
</head>
<body>
<h2>初めてのDjango Rest Framework</h2>
</body>
</html>
python manage.py runserverで実行してhttp://localhost:8000/にいくとindex.htmlが表示されます。
目次へ
入力FormをAIに聞いて作る
入力formをAIに聞いて作ります。(こういうよくあるパターンはAIが得意なので。他はあてにならないというか自分が間違っているか判断できるレベルの質問でないと、かえってはまると私は思ってます。)
Bingチャットに質問:htmlで’user_name’, ‘birth_day’,’age’を入力しhttp://localhost:8000/api/userInfo/にpostするformを作って
Bingチャットの回答コード:
<form action="http://localhost:8000/api/userInfo/" method="post">
{% csrf_token %}
<label for="user_name">名前:</label>
<input type="text" id="user_name" name="user_name"><br><br>
<label for="birth_day">生年月日:</label>
<input type="date" id="birth_day" name="birth_day"><br><br>
<label for="age">年齢:</label>
<input type="number" id="age" name="age"><br><br>
<input type="submit" value="送信">
</form>
この質問をする前にDjangoの質問をしていたのでDjangoのformで必要なCSRF対策のコード
{% csrf_token %}も入ってました。
CSRFについては以下参照
これで実行してformにデータを入力し送信ボタンを押すとデータ登録はできるのですがDjango REST frameworkの画面になってしまいます。
Tips:<form action=”http://localhost:8000/api/userInfo/”と絶対パスで書いていますが/api/userInfo/の相対パスでも大丈夫です。
目次へ
非同期通信の役目
上の管理画面が表示される原因はhttp://localhost:8000/api/userInfo/にデータをPOSTできるのですがレスポンスが管理画面のhtmlだからです。下の画面に留まってくれないとどうにもなりません。
ここで何となく使っていたフロント側の非同期通信(XHR、Fetch、Ajax)を使う理由がわかりました。<form action=でPOSTした場合、そのレスポンスに対して処理を記述できないので、ただレスポンスを表示してしまうのです。htmlではなくjsonでレスポンスが来ても
[
{
"id": 1,
"user_name": "山田太郎",
"birth_day": "2022-06-01",
"age": 1,
"created_at": "2023-06-25T07:56:35.183842Z"
}
]と表示されるだけです。
非同期通信は
fetch or Ajax(リクエスト処理).then(レスポンスに対する処理)
xhrはif (xhr.status === 200) {
のようにレスポンスを受け止めて何をするか決めることができます。これにより画面は、留まったまま処理ができるわけです。
POSTとGetでfetch(非同期通信)を使う
入力したデータを/api/userInfo/にfetchでPostする処理と
/api/userInfoにfetchでGetしてデータを取得して表示する処理に変更しました。(Getの場合、methodや中身は書かなくてもアクセスすればGetになる。)
Fetchは、JavaScriptでHTTPリクエストを発行するためのAPIです。fetch関数を呼び出して、実行するだけです。第1引数に呼び出し先のURLを記載して、第2引数に渡したいパラメータをオブジェクト形式で記載します。GETの場合第1引数だけでもOK。
<!DOCTYPE html>
<html lang=”ja”>
<head>
<meta charset=”UTF-8″>
<title>初めてのDjango Rest Framework</title>
</head>
<body>
<h2>初めてのDjango Rest Framework</h2>
<form id="myForm">
{% csrf_token %}
<label for="user_name">名前:</label>
<input type="text" id="user_name" name="user_name"><br><br>
<label for="birth_day">生年月日:</label>
<input type="date" id="birth_day" name="birth_day"><br><br>
<label for="age">年齢:</label>
<input type="number" id="age" name="age"><br><br>
<input type="submit" value="送信">
</form>
<h3>ユーザーリスト</h3>
<ul id="productList"></ul>
</body>
</html>
<script>
//Get: 毎回表示するとき/api/userInfoにGETメソッドで全レコード(ユーザー情報)をReadし、表示する
fetch('/api/userInfo')
.then(response => response.json())
.then(userInfo => {
let products = JSON.stringify(userInfo);
let jsObj = JSON.parse(products);//jsObjにデータ格納
let productList = document.getElementById("productList");//htmlのid="productList"の場所を取得
for (let i = 0; i < jsObj.length; i++) {//jsObjから1件ずつ取り出してリストに変換
let li = document.createElement("li");
li.innerHTML = "ID: " + jsObj[i].id + ", 名前: " + jsObj[i].user_name + ", 生年月日: " + jsObj[i].birth_day + ", 年齢: " + jsObj[i].age + ", 登録日: " + jsObj[i].created_at;
productList.appendChild(li);//1件表示
}
})
.catch(error => console.error(error));//エラーが発生したらエラーをコンソールに表示
//POST: 送信ボタンを監視し、押されたらformのデータを/api/userInfoにPostする。
const form = document.getElementById('myForm');
form.addEventListener('submit', (event) => {//送信ボタンが押されたか監視
event.preventDefault();//ボタンが押されたときのデフォルトの処理をキャンセル
const formData = new FormData(form);//入力フォームに書いたデータを送信できる形に変換
fetch('/api/userInfo/', {//最後の/は必要
method: 'POST',
body: formData
})
.then(location.reload())//レスポンスが帰ってきたらリロード
.catch(error => console.error(error));//エラーが発生したらエラーをコンソールに表示
});
</script>
Jvascriptはpythonに比べ直感的にわかりにくい。なのでJSON.のJSONに何も代入してないのに何が入ってるの?userInfoってどっからでてきた?という疑問があるが、その辺はわからなくても実際動いているので良しとしている。時間かけて掘り下げても、すぐ忘れるし🤣。こんな感じでAIを活用してます。(全面的には信用してませんが)
目次へ
クロスサイト・スクリプティング(XSS)に注意
名前欄に<HR>と入力して送信するとユーザーリストに線が引かれます。これはHtmlのコードが実行されています。Javascriptのコードも同様に実行できます。線を引かれるだけならいいですが。Javascriptの危険なコードを名前欄に入力されたら実行されてしまい危険です。
原因はindex.htmlのユーザーリストを表示する処理でinnerHTMLを使っているため、HtmlやJavascriptを実行してしまいます。別途対策が必要です。(デプロイしたあるものは対策不十分ですが8文字以内で制限しました。)
こちらのセキュリティの考慮事項にinnerHTMLについて書いてあります。HTML5 では innerHTML
で挿入された <script>
タグは実行するべきではないと定義されているようですが、<imgを使うと実行される例が載っています。
下記記事も参照してください。Django側でindex.htmlに書き込んで表示する場合の注意点を載せています。
Deleteで削除する
idを指定してメソッドを実行する例としてDELETEをやってみます。
DELETEの場合/api/userInfo/にid番号を加えでDELETEメソッドで送れば削除されます。Bingチャットに聞いて作った削除の処理は以下です。
index.html追加部分
<h3>削除</h3>
<form>
<label for="id">ID番号:</label>
<input type="text" id="id" name="id">
<button type="button" onclick="deleteData()">削除</button>
</form>
//script追加部分
// DELETE:削除ボタンが押されたらidを/api/userInfo/の末尾に加えDELETEメソッドでアクセス
const deleteData = async () => {
const id = document.getElementById('id').value;
const url = `/api/userInfo/${id}/`;
const options = {
method: 'DELETE',
};
try {
const response = await fetch(url, options);
if (response.ok) {
location.reload();
} else {
console.error(`HTTPエラーが発生しました: ${response.status}`);
}
} catch (error) {
console.error(`ネットワークエラーが発生しました: ${error}`);
}
};
削除ボタンが押されるとdeleteDataが実行されます。
今回はasync/awaitの非同期処理が使われています。私の認識では、asyncをつけた関数の中でawaitがある場合、そこの部分でレスポンスを待ちます。待っている間もasyncの外は実行されています。
fetchも非同期処理なのでasync/awaitが必要なさそうですが、なくすとエラーになります。これはfetchがレスポンスを待っている間にlocation.reload()が実行されてしまうためです。async/awaitを使わない場合はfetchのレスポンスを待ってからlocation.reload()するように以下のようにします。
const deleteData = () => {
const id = document.getElementById('id').value;
const url = `/api/userInfo/${id}/`;
const options = {
method: 'DELETE',
};
fetch(url, options)
.then(location.reload())//レスポンスが帰ってきたらリロード
.catch(error => console.error(error));//エラーが発生したらエラーをコンソールに表示
};
Deleteでエラー403がでるとき
Postのときは{% csrf_token %}がフォームに入っているので、それを送っていてエラーにはならないようである。DELETEのときにエラーが出る時があった。
上のプログラムをDocker(Django3.2.19や4.2.2)でやってるときはうまくいった。デプロイしようと思いAnaconda(Django4.1)でやっているRailway用プロジェクトに今回のアプリを追加してエラーになった。RenderはOK。アプリの構成が違うので、その辺が原因だと思いますが原因特定はしていません。単独でうまくいくほうは、上で書いてきた通り。などの違いはあるが原因は不明。とりあえず直ったので一応書いておくと。
クロスサイトリクエストフォージェリ (CSRF) 対策(注意、ドキュメント4.0で古いが、その内容で書いています。2023/06/26現在4.2が最新です。)によるとCSRF_USE_SESSIONS と CSRF_COOKIE_HTTPONLY が False に設定されている場合にトークンを取得するとあるので、まずsettings.pyで
from django.conf import settings
print('CSRF_USE_SESSIONSは'+str(settings.CSRF_USE_SESSIONS))
print('CSRF_COOKIE_HTTPONLYは'+str(settings.CSRF_COOKIE_HTTPONLY))
を実行してCSRF_USE_SESSIONS、CSRF_COOKIE_HTTPONLYが両方ともFalseであることを確認。
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
};
const deleteData = () => {
const id = document.getElementById('id').value;
const url = `/api/userInfo/${id}`;
const csrftoken = getCookie('csrftoken');
const options = {
method: 'DELETE',
headers: {'X-CSRFToken': csrftoken},
};
fetch(url, options)
.then(location.reload())//レスポンスが帰ってきたらリロード
.catch(error => console.error(error));//エラーが発生したらエラーをコンソールに表示
};
まとめるとクッキーからcsrftokenを取り出しX-CSRFTokenに代入しヘッダーにつけて送るとエラーがなくまりました。しかし、これをやらなくてもうまくいく場合がある点や、最新のドキュメント4.2は読んでいないので、その辺は承知ください。
あとがき
フロント側でVue.jsなどのDjangoと別のサーバーを立ててDjango RestFrameworkをいじるよりはわかりやすいと思います。
ただ、やはりよくはまるCSRFではまった。また、今回関係ないがCORDSもやっかいな問題です。
目次へ
イチゲをOFUSEで応援する(御質問でもOKです)Vプリカでのお支払いがおすすめです。
Vプリカでのお支払いがおすすめです。
コメント