7.リクエストパラメーター
学習目標
@RequestParamでクエリパラメーターやフォームの値を引数として受け取れる@PostMappingでフォームの POST を受け取り、PRG パターンで二重送信を防げる@ModelAttributeとフォームクラスで複数の入力値をまとめて受け取れる- Thymeleaf の自動エスケープが XSS を防ぐ仕組みを説明できる
1. クエリパラメーターと @RequestParam
Section titled “1. クエリパラメーターと @RequestParam”ブラウザーからサーバーに送られる値を リクエストパラメーター と呼びます。送り方には URL に載せる方法とフォームのリクエストボディで送る方法があり、どちらも @RequestParam で受け取れます。
URL の ? 以降に書いて送るリクエストパラメーターを クエリパラメーター と呼びます。/books/sample?title=こころ の title=こころ は、title というパラメーター名で値 こころ を送ります。検索条件のように URL に残してよい値の受け渡しに使います。
1-1. @RequestParam の基本
Section titled “1-1. @RequestParam の基本”@GetMapping("パス")public String メソッド名(@RequestParam 型 引数名, Model model) { ...}引数名を、パラメーター名と合わせます。本のサンプル でタイトルをハードコードしていた BookController の sample メソッドを、URL から受け取る形に変えます。
package com.example.bookstore;
import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;
@Controllerpublic class BookController { @GetMapping("/books/sample") public String sample(@RequestParam String title, Model model) { model.addAttribute("title", title); return "books/sample"; }}@RequestParam の import と、引数の @RequestParam String title が加わります。引数 title には、同じパラメーター名のリクエスト値が渡されます。受け取った値を Model に格納し、books/sample.html に渡します。
ブラウザーで http://localhost:8080/books/sample?title=吾輩は猫である を開くと、引数 title に 吾輩は猫である が入り、画面に次のように表示されます。
<h1>本のサンプル</h1><p>タイトル: <span>吾輩は猫である</span></p>URL の title を変えると、表示されるタイトルが変わります。
1-2. 省略可能なパラメーター
Section titled “1-2. 省略可能なパラメーター”@RequestParam で受け取るパラメーターが URL に含まれていないと、Spring Boot は 400 Bad Request を返します。検索条件のように省略してもよいパラメーターには、required と defaultValue の指定があります。
| 指定 | 動作 |
|---|---|
@RequestParam(required = false) String title | 省略可。省略時は null |
@RequestParam(defaultValue = "こころ") String title | 省略時は既定値 こころ |
defaultValue を指定すると、required = false も同時に有効になります。
sample メソッドの title を省略可能にし、既定値を こころ にします。
@GetMapping("/books/sample")public String sample(@RequestParam(defaultValue = "こころ") String title, Model model) { model.addAttribute("title", title); return "books/sample";}ブラウザーで /books/sample を title なしで開くと、既定値の こころ が使われ、画面に タイトル: こころ と表示されます。/books/sample?title=坊っちゃん のように指定したときは、指定した値が表示されます。
2. フォーム送信
Section titled “2. フォーム送信”クエリパラメーターは URL に値が現れるため、利用者の入力のように URL に残したくない値には適しません。フォームで入力した値は、POST メソッドで送信し、URL に現れないリクエストボディで送ります。
| 方法 | 値の場所 | 用途 |
|---|---|---|
| クエリパラメーター(GET) | URL の ? 以降 | 検索条件など、URL に残してよい値 |
| フォーム送信(POST) | リクエストボディ | 利用者の入力など、URL に残したくない値 |
どちらも @RequestParam で受け取れます。POST を担当するメソッドには、@GetMapping の代わりに @PostMapping を付けます。
本の登録には、/books/new のテンプレートをフォームに作り変えます。タイトルを入力する books/new.html です。
<!DOCTYPE html><html lang="ja"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>本の登録</title></head><body> <h1>本の登録</h1> <form action="/books" method="post"> <label for="title">タイトル:</label> <input type="text" id="title" name="title"> <button type="submit">登録</button> </form></body></html><form> の action は送信先のパス、method は送信に使う HTTP メソッドです。method="post" で /books へ POST で送ります。<input> の name 属性は、送信される値のパラメーター名で、サーバー側で値を識別するのに使います。
2-1. @PostMapping によるフォーム受信
Section titled “2-1. @PostMapping によるフォーム受信”POST リクエストを担当するメソッドには @PostMapping を付けます。送られた値は、クエリパラメーターと同じく @RequestParam で受け取ります。
BookController に、/books への POST を担当する create メソッドを追加します。あわせて、05章で HomeController に置いた /books/new の表示を BookController に移し(HomeController の newBook は削除)、本に関わる画面を BookController にまとめます。
package com.example.bookstore;
import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestParam;
@Controllerpublic class BookController { @GetMapping("/books/sample") public String sample(@RequestParam(defaultValue = "こころ") String title, Model model) { model.addAttribute("title", title); return "books/sample"; }
@GetMapping("/books/new") public String newBook() { return "books/new"; }
@PostMapping("/books") public String create(@RequestParam String title, Model model) { model.addAttribute("title", title); return "registered"; }}@PostMapping の import と、新しい create メソッドが加わります。@PostMapping("/books") は、一覧表示 の books メソッドに付いている @GetMapping("/books") と同じパスですが、HTTP メソッドが違うため別のメソッドにルーティングされます。/books への GET には books、POST には create が呼び出されます。
登録完了を表示する registered.html です。
<!DOCTYPE html><html lang="ja" xmlns:th="http://www.thymeleaf.org"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登録完了</title></head><body> <h1>登録完了</h1> <p>「<span th:text="${title}">こころ</span>」を登録しました。</p></body></html>ブラウザーで /books/new を開いて こころ と入力し、登録ボタンを押すと、/books への POST が create メソッドに渡り、registered.html が表示されます。
2-2. POST 後のリダイレクト(PRG パターン)
Section titled “2-2. POST 後のリダイレクト(PRG パターン)”registered.html の画面で、利用者がブラウザーの更新ボタンを押すと、ブラウザーは同じ POST リクエストを再送します。実際にデータベースへ書き込む処理が走っていると、登録が二重に実行されてしまいます。
この再送を防ぐパターンが PRG(Post-Redirect-Get)です。POST の処理後に、GET でアクセスできる別ページへリダイレクトを返します。リダイレクト先で更新ボタンを押しても、GET が再実行されるだけで、POST は再送されません。
Spring では、ビュー名の代わりに redirect: を前置きすると、その URL へリダイレクトを返します。
return "redirect:/パス";create メソッドを、登録後に 一覧(/books)へリダイレクトする形に書き直すと、次のようになります。
@PostMapping("/books")public String create(@RequestParam String title) { // 登録処理 return "redirect:/books";}本章では、登録された値を画面で確かめやすくするため、以降も registered.html を直接返す形で続けます。実際のアプリケーションでは PRG パターンを使います。
3. フォームクラスへのバインド
Section titled “3. フォームクラスへのバインド”本の登録には、タイトルだけでなく価格も必要です。books/new.html に、価格の入力欄を追加します。
<!DOCTYPE html><html lang="ja"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>本の登録</title></head><body> <h1>本の登録</h1> <form action="/books" method="post"> <label for="title">タイトル:</label> <input type="text" id="title" name="title"> <label for="price">価格:</label> <input type="number" id="price" name="price"> <button type="submit">登録</button> </form></body></html><input type="number"> は、数値の入力欄です。
@RequestParam を引数に並べて受け取る形では、入力欄が増えるたびにメソッドの引数リストが長くなります。複数のフィールドをまとめた フォームクラス を作り、Spring Boot がリクエストの値をフィールドに割り当てる形にします。
BookForm クラスを作り、入力欄に対応するフィールドとアクセサーを定義します。
package com.example.bookstore;
public class BookForm { private String title; private int price;
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public int getPrice() { return price; }
public void setPrice(int price) { this.price = price; }}メソッドの引数に @ModelAttribute を付けると、Spring Boot がリクエストの値をフォームクラスのフィールドに割り当てて渡します。
public String メソッド名(@ModelAttribute フォームクラス 引数名, ...) { ...}フォームの <input name="title"> の値がセッター setTitle を通じてフィールドに入ります。値の型変換も @RequestParam のときと同じく、フィールドの型に合わせて自動で行われます。title は String のまま、price は int に変換されます。
create メソッドを、BookForm で受け取る形に変えます。
package com.example.bookstore;
import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestParam;
@Controllerpublic class BookController { @GetMapping("/books/sample") public String sample(@RequestParam(defaultValue = "こころ") String title, Model model) { model.addAttribute("title", title); return "books/sample"; }
@GetMapping("/books/new") public String newBook() { return "books/new"; }
@PostMapping("/books") public String create(@ModelAttribute BookForm form, Model model) { model.addAttribute("title", form.getTitle()); model.addAttribute("price", form.getPrice()); return "registered"; }}@ModelAttribute の import と、create メソッドの引数の変更が加わります。form.getTitle() と form.getPrice() で、フォームに入力された値をフィールドから取り出せます。フォームの入力欄が増えても、BookForm にフィールドとアクセサーを足すだけで対応でき、コントローラーのメソッド側は変えなくて済みます。
受け取った値を registered.html に反映します。
<!DOCTYPE html><html lang="ja" xmlns:th="http://www.thymeleaf.org"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登録完了</title></head><body> <h1>登録完了</h1> <p>「<span th:text="${title}">こころ</span>」(<span th:text="${price}">473</span>円)を登録しました。</p></body></html>/books/new で こころ と 473 を入力して送信すると、次の HTML が表示されます。
<h1>登録完了</h1><p>「<span>こころ</span>」(<span>473</span>円)を登録しました。</p>4. 自動エスケープと XSS
Section titled “4. 自動エスケープと XSS”registered.html の th:text="${title}" は、title の値を <span> の中身に埋め込みます。利用者が入力した値には、文字列だけでなく < や > を含む文字列が混ざることがあります。
たとえば、タイトルに <b>こころ</b> と入力したとします。これをそのまま HTML に埋め込むと、<b> がタグとして解釈され、画面では太字で表示されます。利用者が意図しない HTML の挙動が、入力値によって引き起こされます。
th:text は、値を HTML に埋め込むときに、< や > などの記号を自動で別の表記に変換します。これを エスケープ と呼びます。<b>こころ</b> を埋め込むと、出力は次のようになります。
<p>「<span><b>こころ</b></span>」(...)を登録しました。</p>< が <、> が > に変換され、ブラウザーには文字としてそのまま表示されます。タグとしての効果は発生しません。
4-1. XSS 攻撃の防止
Section titled “4-1. XSS 攻撃の防止”入力値を HTML に直接埋め込むと、入力に含まれるタグがそのまま HTML に混ざります。悪意のある利用者が、入力欄に <script> を含む文字列を送ると、そのスクリプトが他の利用者のブラウザーで実行されます。攻撃者が用意したスクリプトで Cookie を盗まれたり、画面が書き換えられたりします。この攻撃を XSS(クロスサイトスクリプティング、cross-site scripting)と呼びます。
th:text は、利用者の入力を文字としてエスケープして埋め込むため、入力に含まれるスクリプトはタグとして解釈されません。th:text で表示する値は、HTML として実行されることなく、文字としてそのまま画面に出ます。