38
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

開発チームで毎週徳丸本を読んで、内容を発表する取り組みをしています。
その際に、ちょこちょこJSONPというキーワードが出てきており、
お恥ずかしながら、JSONPってなんやねーんという状態だったので、
JSONPについて調べた内容や、実際にJSONPを作って実行してみたりした
知見などを共有できればと思い、本記事を書いてみました。

JSONPってなんやねーん!?

JSONP(JSON with Padding) とは、クロスドメイン通信を可能にするため、JSONデータをJavaScript関数でラップして scriptタグ経由で取得する手法です。クライアントはcallbackパラメータで関数名を指定し、サーバーはその関数を呼び出す形式でレスポンスを返します。これにより、同一オリジンポリシーの制約を回避してデータを受け取れます。ただし、今回解説はしませんが、セキュリティ的な観点から、XSSの対策としてコールバック関数名の検証や、JSONの安全なエンコードが必要となる点は押さえておきましょう。 また、近年ではより安全なCORSを用いた方法が主流となっています。

同一オリジンポリシーとは
Same-Origin Policyとか、同一生成元ポリシーとも呼ばれます。
Webブラウザのセキュリティポリシーで、同一オリジン(※1)ではない場合、基本的にはjavascriptから異なるオリジンへの通信はできません。

このため、上述のような同一オリジンポリシーの制約を回避するには、CORS(クロスオリジンリソース共有)という機能を利用して、異なるオリジンからのリソースアクセス制限を緩和させてあげるか、JSONPを使う必要があります。

※1: スキーム(httpsとかhttp)と、ホスト(example.com)と、ポート(80とか443)が同一

JSONPの具体的な実装方法

1. クライアント側で「javascriptの関数」が実行できるよう事前に定義を済ませる
2. scriptタグのsrc属性に、レスポンスとして関数名(JSONデータ);を受け取れるエンドポイントを指定しておき、ブラウザから当該エンドポイントへGETリクエストさせる(scriptタグのsrc属性に記述したエンドポイントへのGETリクエストは同一オリジンポリシーの制約を受けません)
3. サーバー側にて関数名(JSONデータ);といった呼び出し形式でレスポンスを返す
4. クライアント側で関数名(JSONデータ);を受け取ることで、事前定義しておいた「javascriptの関数」を実行させる

これにより、同一オリジンポリシーを回避しつつ、JSONデータをサーバーから受け取り、クライアント側のjavascriptで処理させることが可能です。

実際に作って実行してみよう!

説明だけだと中々イメージが掴みにくいと思いますので、実際に作って実行してみましょう。以下はLaravel(バージョンは11系です)で実際に作成したサンプルになります。

まずは、ルーティングを定義しておきます。

routes/web.php
Route::group([
    'prefix' => 'sample',
    'middleware' => 'guest:web'
], function () {
    Route::get('render', [JsonpController::class, 'render']);
    Route::get('jsonp', [JsonpController::class, 'jsonp']);
});

次にコントローラーの処理を書きます。

app/Http/Controllers/JsonpController.php
<?php

namespace App\Http\Controllers;
use Illuminate\Http\Request;

class JsonpController extends Controller
{
    public function render(Request $request) {
        return view('sample.render');
    }
    
    public function jsonp(Request $request)
    {
        $data = [
            'message' => 'This is a JSONP response',
            'status' => 'success',
        ];

        $callback = $request->query('callback');

        if ($callback && preg_match('/^[a-zA-Z0-9_.]+$/', $callback)) {
            $json = json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
            return response("$callback($json);", 200)
                ->header('Content-Type', 'application/javascript');
        } else {
            return response()->json($data);
        }
    }
}

最後にビュー側の処理を書いていきます。

resources/views/sample/render.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>JSONPを試して理解</title>
    </head>
    <body style="background-color: #222; color: greenyellow">
        <div id="result">レスポンス待ち</div>
        <script>
            // 1. クライアント側で「javascriptの関数」が実行できるよう事前に定義を済ませる
            function handleJsonpResponse(data) {
                const resultDiv = document.getElementById('result');
                resultDiv.innerHTML = `
                <p><strong>Status:</strong> ${data.status}</p>
                <p><strong>Message:</strong> ${data.message}</p>
            `;
                console.log('JSONP Data:', data);
            }
        </script>
        <!-- 2. scriptタグのsrc属性にレスポンスとして`関数名(JSONデータ);`
        受け取れるエンドポイントを指定しておきブラウザから当該エンドポイントへGETリクエストさせる -->
        <script src="http://localhost:8320/sample/jsonp/?callback=handleJsonpResponse"></script>
    </body>
</html>

ビューファイルのfunction handleJsonpResponse(data)の部分で、以下を行っています。

1. クライアント側で「javascriptの関数」が実行できるよう事前に定義を済ませる

そして、ビューファイルの<script src="http://localhost:8320/sample/jsonp/?callback=handleJsonpResponse"></script>の部分で、以下を行っています。

2. scriptタグのsrc属性に、レスポンスとして関数名(JSONデータ);を受け取れるエンドポイントを指定しておき、ブラウザから当該エンドポイントへGETリクエストさせる

resources/views/sample/render.blade.phpがブラウザによりロードされると、<script src="http://localhost:8320/sample/jsonp/?callback=handleJsonpResponse"></script>の部分により、同一オリジンポリシーの制約を受けずに、http://localhost:8320/sample/jsonp/?callback=handleJsonpResponseへGETリクエストが行われます。

このGETリクエストを受けたサーバー側は、以下を行います。

3. サーバー側にて関数名(JSONデータ);といった呼び出し形式でレスポンスを返す

その結果、クライアント側では、レスポンスとしてhandleJsonpResponse({"message":"This is a JSONP response","status":"success"});を受け取ります。

レスポンスで受け取ったhandleJsonpResponseという関数自体は事前定義されているため、サーバーから受け取ったJSONデータを渡しつつ、事前定義されているhandleJsonpResponseが実行されます。これが以下に該当します。

4. クライアント側で関数名(JSONデータ);を受け取ることで、事前定義しておいた「javascriptの関数」を実行させる

以上により、同一オリジンポリシーの制約を回避しつつ、サーバーからJSONデータをJavascriptに渡して実行させることができました。これがJSONPという手法です。

image.png

JSONPの由来

さらに理解を深めるためにJSONPの由来を紐解いていきましょう。

JSONP(JSON with Padding)の、”Padding(パディング)”は直訳すると「詰め物」や「余白」などの意味があります。ただし、プログラミングでは何かの前後にデータを追加して包むような操作を指すことがよくあるようです。
そのため、JSON with Paddingとは、JSONデータの周りに追加で包むようなものをつけ加えることを示します。

JSONデータの周りに追加で包むようなものをつけ加えるとは、具体的に何かというと
サンプルコードで試した通り、関数名と呼び出し用のカッコをJSONデータの周りに付け加えることです。
つまり、Paddingの正体は、関数名と呼び出し用のカッコになります。

通常のJSONレスポンスはこうです。

{ "name": "taro", "country": "Japan" }

JSONPでは、このJSONをJavaScript関数で包んで返す、つまり「JSONにパディング(包み)を加える」わけです。

callbackFn({ "name": "taro", "country": "Japan" });

この callbackFn(...) の形式にすることで、同一オリジンポリシーに適合せずとも
javascriptとして読み込んで即座に関数を実行させつつ、JSONデータも渡せるというのがポイントです。

ただし、JSONPは公式な規格があるわけではなく慣習的な実装方法で、誰かがRFCで定めたわけではありません。そのため、Paddingという言葉の選定もシンプルで説明的なアイディアとして広まったもののようです。

最後に

記事は以上となります、ここまで読んでいただきありがとうございます。
この記事が、JSONPってなんやねーんとなっている方の一助になれば幸いです。

38
15
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?