CORS policyが原因でアクセスできない??

Django
StockSnapによるPixabayからの画像

今回AjaxとDjangoを使って非同期通信をやってみました。
しかし、”CORS policy”によってアクセスができないとのエラーが発生。
その原因と解決をまとめてみました。

開発環境

  • OS: Windows10 Home
  • エディター: Visual studio code

API側

  • 言語: Python 3.9
  • Pythonフレームワーク: Django 3.1.2
  • web api作成フレームワーク: Django REST framework 3.12.2
  • DB: SQLite3

クライアント側

  • 言語: javascript, html, python3.9
  • Pythonフレームワーク: flask 1.1.2
  • Javascriptフレームワーク: jquery-3.5.1.min

API側の作成

まずはAPI側を作成していきます。
こちらは公式の”Django REST framework”のチュートリアルを参考に作成していきます。

ディレクトリ構成

rest_api/
  ├ .vscode/
  │   └ settings.json
  └ api_project/
     ├ friends/
     │   ├ admin.py
     │   ├ models.py
     │   ├ serializers.py
     │   ├ urls.py
     │   └ views.py
     └ api_project/
         ├ urls.py
         └ views.py

※今回編集するファイルのみ表示しています。

API機能をさっさと実装

Djangoのプロジェクト作成、アプリの作成の基本的なところはDjango公式のチュートリアルを見れば分かるのでここでは省きます。

データベーステーブルの元となるmodelの作成

まずはmodelを作成します。
これはこのAPI側で所持するデータベースの元になるものです。
このmodelがあることでデータをPython内でオブジェクトとして扱うことができます。
いわゆる”ORM“というやつですかね。

# models.py
from django.db import models

class Friend(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField(default=0)

上記のmodels.pyでは’Friend‘というテーブルにカラムをそれぞれ’name‘, ‘age‘を作成しました。

作成したmodelをデータベースに反映

ではデータベースに反映していきます。
makemigrationsをする前にapi_project/api_project/settings.pyの’INSTALLED_APPS‘にfriendsアプリが追加されているか確認

# settings.py
~~~~~~~~~~~~~省略~~~~~~~~~~~~~~~~~
INSTALLED_APPS = [
~~~~~~~~~~~~~省略~~~~~~~~~~~~~~~~~
    'friends.apps.FriendsConfig',
]
~~~~~~~~~~~~~省略~~~~~~~~~~~~~~~~~

確認後、以下のコマンドをapi_project上で実行します。

rest_api\api_project>python manage.py makemigrations friends

以下の項目が表示されたら成功です。

Migrations for 'friends':
  friends\migrations\0001_initial.py
    - Create model Friend

また、これだけだとmodelのmigrationが作られただけなので次にsqlite3にテーブルを作成していきます。
次のコマンドを実行することで作成したmigrationを元にテーブルが作られます。

rest_api\api_project>python manage.py migrate

実行後、いろいろ表示されると思いますが”Applying friends.0001_initial… OK“の文字が表示されていればOKです。

serializerの作成

次はserializerの処理コードを作成していきます。

# api_project/friends/serializers.py
from rest_framework import serializers

from friends.models import Friend

class FriendSerializer(serializers.ModelSerializer):
    class Meta:
        model = Friend  # ...1
        fields = ['id', 'name', 'age']  # ...2
  • 1: ここで先ほど作成したFriendモデルを指定します。
    このことでモデル内で用意した”name”, “age”カラムをそのまま引き継ぐことができます。
  • 2: ここでは実際に処理をするフィールドを指定します。
    また、モデル側ではidが自動で用意されているのでfields変数のリスト内に追加します。

serializerを扱うviewを実装する

ではシリアライズしたデータを処理してくれるAPIのview機能を実装します。

# api_project/friends/views.py
from rest_framework import generics

from friends.models import Friend
from friends.serializers import FriendSerializer


class FriendList(generics.ListCreateAPIView):  # ...1
    queryset = Friend.objects.all()  # ...2
    serializer_class = FriendSerializer  # ...3
  • 1: ここで”ListCreateAPIView”を継承したクラスを作ることで特定のURLにアクセスした際に、
    任意のデータの取得または新規データ作成が可能になる
  • 2, 3: この設定により、Friendデータの取得、新規作成に関して自動的にシリアライズ、
    デシリアライズをしてくれるようになります。

URLの設定

friendsにアクセスできるようにするためURLを設定します。

# api_project/api_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('friends.urls')),  # ...1
    path('admin/', admin.site.urls),
]
# api_project/friends/urls.py
from django.urls import path

from friends.views import FriendList

urlpatterns = [
    path('friends/', FriendList.as_view()),  ...2
]

1, 2により”http://127.0.0.1:8000/friends/“にアクセスするとGET, POSTのそれぞれによってデータの取得、作成のAPI操作が可能になります。

adminの設定(必須ではないよ)

本来ならurlの設定までで十分なのですが、データの管理をしやすくするためにadminを設定していきます。

adminユーザーをpython manage.py createsuperuserのコマンドで作成します。
作成した後、デフォルトだとadminの画面にはfriendのデータを管理するための項目が表示されないためadminファイルを以下のようにします。

# api_project/friends/admin.py
from django.contrib import admin

from friends.models import Friend 

admin.site.register(Friend)  ...1
  • 1: たったこれだけでadminページにfriendデータの項目が追加されます。

後は”http://127.0.0.1:8000/admin/“にアクセスしてfriendデータを作成します。

下画像の赤枠のようにFriends項目が表示されています。

ホーム画面よりFriends項目をクリックするとした画面に遷移するので下画像の赤枠からデータを追加していきます。

追加すると下画像のように表示されます。

これにてapi側の作成は終了しました。
次はこのapiを呼び出すクライアント側を作成していきます。

クライアント側の作成

次はjqueryでAjaxを使って先ほど作ったDjangoAPIからデータを取得していきます。
フレームワークはFlaskです。
なんでわざわざFlaskかという部分は気にしないでください。

Flask自体は”pip install flask“で簡単に導入できます。

ディレクトリ構成

jquery_study/
  ├ static/
  │   └ js/
  │      └ ajax.js
  ├ templates/
  │  └ index.html
  └ app.py

※今回編集するファイルのみ表示しています。

htmlのメインページを呼び出す処理

まずは”http://localhost:5000/”にアクセスしたときにhtmlページをレンダリングしてくれる処理を作っていきます。

# app.py
from flask import Flask, render_template, request

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html")

# run the app.
if __name__ == "__main__":
    app.run(host='localhost', debug=False)

ここは特に説明することないですね。
一応、templatesフォルダに表示したいhtmlファイルを追加し、”render_template()”の引数に追加したhtmlファイル名を指定することで”http://127.0.0.1:5000/”の画面に表示することができます。

その表示予定のhtmlファイルが次になります。

<!-- templates/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Ajax</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.min.js"></script>  <!-- 1 -->
  </head>
  <body>
    <p class="title">Ajax通信</p>
    <form>
      <input type='text' id="bb"/>  <!-- 2 -->
    </form>
    <script type="text/javascript" src="static/js/ajax.js"></script>  <!-- 3 -->
  </body>
</html>
  • 1: 今回使用するjqueryはダウンロードはせず、CDNより読み込んで使用することにしました。
  • 2: ここのidと後に説明するjavascriptファイル内で使用するajaxの処理と紐づけていきます。
  • 3: 一応htmlファイルとjavascriptファイルを分けて用意しておきます。

次がこのアプリ内本命となるjavascriptファイルです。

// static/js/ajax.js
$(function(){
    $("#bb").on("change", function() {  // ...1
        $.ajax({
            url:'http://127.0.0.1:8000/friends/',  // ...2
        })
        .done((data) => {
            console.log('done')
            console.log('data: ', data);
        })
        .fail((data) => {
            console.log('fail')
            console.log(data);
        })
    });
});
  • 1: ここで先ほど設定したidのフォームが変化したときにここのコードが処理されます。
    変化というのが”change”という部分が表しています。
  • 2: ここからajax処理のためのコードでアクセス先を指定しています。
    データを取得するurlが’http://127.0.0.1:8000/friends/‘なので設定
    以降はデータの取得が成功したときが”done“、失敗したときが”fail“の処理を実行します。

いざ実行!!

ではいざ実行していきます。

API側、クライアント側でそれぞれ以下のようにサーバーを立ち上げます。

〇API側

api_study\api_project>python manage.py runserver

〇クライアント側

jquery_study>python .\app.py

それぞれ実行したらブラウザでクライアント側のURL(デフォルトならhttp://127.0.0.1:5000/)にアクセスします。
すると下の画面が表示されます。
一度、赤枠の入力フォームに何か文字を入れてからこの入力フォーム以外の場所をクリックしてカーソルを外すとデベロッパーツールのコンソール画面にajaxの結果が表示されます。

おや?なにやらエラーが発生してしまいました。

エラーの部分を読んでみると”CORS ポリシー“とやらでapi側へのアクセスが許可されていないようだ。

MDN web docsによると”CORS“というのは「オリジン間リソース共有」の略称らしい。
オリジンというのはスキーム(http)、ホスト(~.com)、ポート(5000 または8000)で定義される部分のことですね。
そのオリジンが違うもの同士のリソースへのアクセス許可をブラウザ側に指示する仕組みとのこと。
確かに違う場所からのアクセスをなんでもかんでも許可していたらセキュリティ的にまずそうというのは感覚として分かる。
今のエラーはそのポリシーに反して許可されていないオリジンからアクセスしようとしたから怒られたみたいです。

どうにかして”http://127.0.0.1:5000″をDjango側で許可していきます。

今回apiを実装する上で使用しているパッケージのDjango Rest Framework公式ドキュメントから調べていきます。

公式ホームページ内でCORSで調べた結果、以下の記述が出てきました。

Otto Yiu maintains the django-cors-headers package, which is known to work correctly with REST framework APIs.

https://www.django-rest-framework.org/topics/ajax-csrf-cors/

django-cors-headersの導入

pipでインストールできるのでインストールします。

その後、設定ファイルに以下のように記述していきます。

# settings.py
~~~~~~~~~~~ 省略 ~~~~~~~~~~~~
INSTALLED_APPS = [
~~~~~~~~~ 省略 ~~~~~~~~~~~~
    'corsheaders',  # ...1
]

~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~
MIDDLEWARE = [
~~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~~~~~~~~~~
    'corsheaders.middleware.CorsMiddleware',  # ...2
    'django.middleware.common.CommonMiddleware',
~~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~~~~~~~~~~
]

~~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~~~~~~~~~~
CORS_ALLOWED_ORIGINS = [
    "http://127.0.0.1:5000",  # ...3
]
  • 1: これを追加することでDjangoプロジェクトに存在を知らせます。
  • 2: この部分に関してはパッケージのREADMEにも記載がありますが、”CommonMiddleware”の上に追加する必要があります。
    レスポンスを生成する前にCorsMiddlewareを動かす必要があるみたいです。
  • 3: ここが何のオリジンに対して許可するのかを指定します。
    許可の仕方は三つあります。
    それぞれ以下のような内容になっています。
種類内容
CORS_ALLOWED_ORIGINS許可したいオリジンを直接追加する
CORS_ALLOWED_ORIGIN_REGEXES許可したいオリジンを正規表現を使って追加する
CORS_ALLOW_ALL_ORIGINSすべてのオリジンに対して許可する

改めて実行

これでもうアクセスできるはずです。
では改めてもう一度API側とクライアント側のローカルサーバーを起動し、ブラウザ側から”http://127.0.0.1:5000“にアクセスします。

※一度ブラウザのキャッシュを消してからじゃないとクライアント側の変更が画面に反映しないかもしれません。

先ほどと同じようにajax画面の入力フォームに何か値を入力し、フォーム以外の部分をクリックすると下の画像のようになります。

これでajaxを使った非同期通信ができるようになりました。

最後に

今回はjqueryとDjangoでデータのやり取りをしてみました。
この記事がなにかしら参考になれば幸いです。
ここまで読んでいただきありがとう!

コメント

タイトルとURLをコピーしました