気ままなタンス*プログラミングなどのノートブック

プログラミングやRPGツクール、DTM等について、学んだことや備忘録をアウトプットとして残し、情報を必要としている誰かにとって「かゆいところに手が届く」ブログとなることを願いながら記事を書いています。

【Django】Djangoアプリのフロント側からAjax実行時にCSRFトークンを一緒に送信する方法

スポンサーリンク

DjangoアプリでデータをPOSTする際、CSRFトークンは必須*1になります。

通常のリクエストだったら、条件反射的に {% csrf_token %} をFormタグの中に入れるのに、Ajaxの時だけ、なぜか忘れて「動かない」と悩んでしまう・・・なんてことありませんか。

「CSRFトークン問題で時間を溶かしてしまった・・・」
なんてことを起こさないようにするため、備忘録を残します。

では、具体的に何をすれば良いか。
その答えは、公式のドキュメントにあります。

CSRFトークンを一緒に送信する方法

ヘッダー[X-CSRFToken]をセットするためのフック機能を追加する

DjangoドキュメントのAJAXの部分を引用

クロスサイトリクエストフォージェリ (CSRF) 対策 | Django documentation | Django

すべての POST リクエストで CSRF トークンを POST するデータに含めることを 覚えておかなければなりません。なので、別の方法が用意されています。それは、各 XMLHttpRequest に対して、X-CSRFToken という独自ヘッダーに CSRF トークンの 値を設定することです。多くの JavaScript のフレームワークはすべてのリクエストについて、ヘッダーを設定するようなフック機能を提供しているので、この操作は多くの 場合、簡単に行うことができます。 jQuery の場合、 ajaxSend イベントを以下の ように記述します

jQuery(document).ajaxSend(function(event, xhr, settings) {
    function getCookie(name) {
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                // 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;
    }
    function sameOrigin(url) {
        // url could be relative or scheme relative or absolute
        var host = document.location.host; // host + port
        var protocol = document.location.protocol;
        var sr_origin = '//' + host;
        var origin = protocol + sr_origin;
        // Allow absolute or scheme relative URLs to same origin
        return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
            (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
            // or any other URL that isn't scheme relative or absolute i.e relative.
            !(/^(\/\/|http:|https:).*/.test(url));
    }
    function safeMethod(method) {
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }

    if (!safeMethod(settings.type) && sameOrigin(settings.url)) {
        xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
    }
});


カスタムヘッダー「X-CSRFToken」に対して、CSRFトークンを設定するスクリプトを組み込んでおき、Ajaxを実行することによって、CSRF検証に失敗したというエラーを回避することができます。

スクリプトを導入するにあたっては、下記のような方法が良いかもしれません。

  • 上記、ajaxSendスクリプトが記載されたDjangoテンプレートファイルを別途用意しておき、Ajaxを実行するテンプレート画面でincludeする
    • メリット:Ajaxが必要な画面のみに、X-CSRFTokenヘッダを付与する処理が実行される
    • デメリット:複数画面でAjaxを実行する場合、その都度テンプレートをincludeする必要がある
  • 基本となるベーステンプレートを用意し、その中にajaxSendスクリプトを記述。他の画面を作成する際にはベーステンプレートを継承extendする
    • メリット:ベーステンプレートさえ継承していれば、X-CSRFTokenを含めてリクエストを送信してくれるので、CSRF検証エラーを意識しなくて良い
    • デメリット:Ajaxを実行しない画面でも、X-CSRFTokenを含めるスクリプトが読み込まれる

FullCalendarを利用した例

<!-- Djangoテンプレートの構造は割愛 -->
<link href="{% static 'css/fullcalendar.min.css' %}" rel="stylesheet">
<link href="{% static 'css/fullcalendar.print.css' %}" rel="stylesheet" media="print">
<script src="{% static 'js/fullcalendar.min.js' %}"></script>

<script type="text/javascript">
// X-CSRFTokenにセットするフックコード(引用元のコード)を書いていることを前提
// なお先頭は$(document).ajaxSend(function(event, xhr, settings) {に変更した
$(document).ready(function() {
    $('#calendar').fullCalendar({
        header: {
            left: 'prev, next, today',
            center: 'title',
            right: 'month, basicWeek, basicDay'
        },
        defaultDate: '2015-03-18',
        editable: true,
        eventLimit: true,

        events: {

            url: '{% url 'api:any_view' %}',
            type: 'POST',
            error: function() {
                console.log("error");
            },
            success: function() {
                console.log("ok");
            }
        }
    });
});
</script>

<div id="calendar" class="fc fc-ltr fc-unthemed"></div>

views.py

# coding: utf-8
def any_view(request):
    user = request.user
    start = request.POST["start"] # FullCalendarのパラメータ
    end = request.POST["end"] # FullCalendarのパラメータ
    
    any_list = Any.objects.filter(user__username=user.username,
                                   any_ymd__range=(start, end) )

    #... jsonを返却 [{'title':'any_event', 'start': '2015-03-20'}...]

urls.py(project)

    # 追記
    url(r'^api/', include('api.urls', namespace='api')),

urls.py(app)

from django.conf.urls import patterns, url
from api import any
urlpatterns = patterns('',
    url('^any/$', views.any_view, name='any_view'),
)

結果のコード

(fullcalendarに渡すjsonレスポンスの部分は割愛したため、上記コードをそのまま書いてもこの結果にはならないのですが、適切にAjax送信できたことがわかります)
f:id:rinne_grid2_1:20150320071809p:plain


それでは、皆さんも楽しいDjangoコーディングを!



おすすめのPython本

Pythonプロフェッショナルプログラミング 第2版

Pythonプロフェッショナルプログラミング 第2版

初めてのPython 第3版

初めてのPython 第3版

*1:ミドルウェアを有効にしているか、アノテーションをつけていない限り