Skip to content
Playground

7.リクエストパラメーター

学習目標

  • @RequestParam でクエリパラメーターやフォームの値を引数として受け取れる
  • @PostMapping でフォームの POST を受け取り、PRG パターンで二重送信を防げる
  • @ModelAttribute とフォームクラスで複数の入力値をまとめて受け取れる
  • Thymeleaf の自動エスケープが XSS を防ぐ仕組みを説明できる

1. クエリパラメーターと @RequestParam

Section titled “1. クエリパラメーターと @RequestParam”

ブラウザーからサーバーに送られる値を リクエストパラメーター と呼びます。送り方には URL に載せる方法とフォームのリクエストボディで送る方法があり、どちらも @RequestParam で受け取れます。

URL の ? 以降に書いて送るリクエストパラメーターを クエリパラメーター と呼びます。/books/sample?title=こころtitle=こころ は、title というパラメーター名で値 こころ を送ります。検索条件のように URL に残してよい値の受け渡しに使います。

@GetMapping("パス")
public String メソッド名(@RequestParam 型 引数名, Model model) {
...
}

引数名を、パラメーター名と合わせます。本のサンプル でタイトルをハードコードしていた BookControllersample メソッドを、URL から受け取る形に変えます。

src/main/java/com/example/bookstore/BookController.java
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;
@Controller
public class BookController {
@GetMapping("/books/sample")
public String sample(@RequestParam String title, Model model) {
model.addAttribute("title", title);
return "books/sample";
}
}

@RequestParamimport と、引数の @RequestParam String title が加わります。引数 title には、同じパラメーター名のリクエスト値が渡されます。受け取った値を Model に格納し、books/sample.html に渡します。

ブラウザーで http://localhost:8080/books/sample?title=吾輩は猫である を開くと、引数 title吾輩は猫である が入り、画面に次のように表示されます。

<h1>本のサンプル</h1>
<p>タイトル: <span>吾輩は猫である</span></p>

URL の title を変えると、表示されるタイトルが変わります。

@RequestParam で受け取るパラメーターが URL に含まれていないと、Spring Boot は 400 Bad Request を返します。検索条件のように省略してもよいパラメーターには、requireddefaultValue の指定があります。

指定動作
@RequestParam(required = false) String title省略可。省略時は null
@RequestParam(defaultValue = "こころ") String title省略時は既定値 こころ

defaultValue を指定すると、required = false も同時に有効になります。

sample メソッドの title を省略可能にし、既定値を こころ にします。

BookController.java
@GetMapping("/books/sample")
public String sample(@RequestParam(defaultValue = "こころ") String title, Model model) {
model.addAttribute("title", title);
return "books/sample";
}

ブラウザーで /books/sampletitle なしで開くと、既定値の こころ が使われ、画面に タイトル: こころ と表示されます。/books/sample?title=坊っちゃん のように指定したときは、指定した値が表示されます。

クエリパラメーターは URL に値が現れるため、利用者の入力のように URL に残したくない値には適しません。フォームで入力した値は、POST メソッドで送信し、URL に現れないリクエストボディで送ります。

方法値の場所用途
クエリパラメーター(GET)URL の ? 以降検索条件など、URL に残してよい値
フォーム送信(POST)リクエストボディ利用者の入力など、URL に残したくない値

どちらも @RequestParam で受け取れます。POST を担当するメソッドには、@GetMapping の代わりに @PostMapping を付けます。

本の登録には、/books/new のテンプレートをフォームに作り変えます。タイトルを入力する books/new.html です。

templates/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"/booksPOST で送ります。<input>name 属性は、送信される値のパラメーター名で、サーバー側で値を識別するのに使います。

2-1. @PostMapping によるフォーム受信

Section titled “2-1. @PostMapping によるフォーム受信”

POST リクエストを担当するメソッドには @PostMapping を付けます。送られた値は、クエリパラメーターと同じく @RequestParam で受け取ります。

BookController に、/books への POST を担当する create メソッドを追加します。あわせて、05章で HomeController に置いた /books/new の表示を BookController に移し(HomeControllernewBook は削除)、本に関わる画面を BookController にまとめます。

src/main/java/com/example/bookstore/BookController.java
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;
@Controller
public 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";
}
}

@PostMappingimport と、新しい create メソッドが加わります。@PostMapping("/books") は、一覧表示books メソッドに付いている @GetMapping("/books") と同じパスですが、HTTP メソッドが違うため別のメソッドにルーティングされます。/books への GET には booksPOST には create が呼び出されます。

登録完了を表示する registered.html です。

templates/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 への POSTcreate メソッドに渡り、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 に、価格の入力欄を追加します。

templates/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 クラスを作り、入力欄に対応するフィールドとアクセサーを定義します。

src/main/java/com/example/bookstore/BookForm.java
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 のときと同じく、フィールドの型に合わせて自動で行われます。titleString のまま、priceint に変換されます。

create メソッドを、BookForm で受け取る形に変えます。

src/main/java/com/example/bookstore/BookController.java
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;
@Controller
public 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";
}
}

@ModelAttributeimport と、create メソッドの引数の変更が加わります。form.getTitle()form.getPrice() で、フォームに入力された値をフィールドから取り出せます。フォームの入力欄が増えても、BookForm にフィールドとアクセサーを足すだけで対応でき、コントローラーのメソッド側は変えなくて済みます。

受け取った値を registered.html に反映します。

templates/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>

registered.htmlth:text="${title}" は、title の値を <span> の中身に埋め込みます。利用者が入力した値には、文字列だけでなく <> を含む文字列が混ざることがあります。

たとえば、タイトルに <b>こころ</b> と入力したとします。これをそのまま HTML に埋め込むと、<b> がタグとして解釈され、画面では太字で表示されます。利用者が意図しない HTML の挙動が、入力値によって引き起こされます。

th:text は、値を HTML に埋め込むときに、<> などの記号を自動で別の表記に変換します。これを エスケープ と呼びます。<b>こころ</b> を埋め込むと、出力は次のようになります。

<p><span>&lt;b&gt;こころ&lt;/b&gt;</span>」(...)を登録しました。</p>

<&lt;>&gt; に変換され、ブラウザーには文字としてそのまま表示されます。タグとしての効果は発生しません。

入力値を HTML に直接埋め込むと、入力に含まれるタグがそのまま HTML に混ざります。悪意のある利用者が、入力欄に <script> を含む文字列を送ると、そのスクリプトが他の利用者のブラウザーで実行されます。攻撃者が用意したスクリプトで Cookie を盗まれたり、画面が書き換えられたりします。この攻撃を XSS(クロスサイトスクリプティング、cross-site scripting)と呼びます。

th:text は、利用者の入力を文字としてエスケープして埋め込むため、入力に含まれるスクリプトはタグとして解釈されません。th:text で表示する値は、HTML として実行されることなく、文字としてそのまま画面に出ます。