Skip to main content

Spring Bootでの入力検証ガイド

著者
Headshot of Lucien Chemaly

Lucien Chemaly

feature bean validation

2023年9月12日

0 分で読めます
{"id":1,"name":"John Doe Updated","email":"john.doe.updated@example.com","password":"newP@ssW0rd!"}

Javaを使う開発者なら、スタンドアロンの本番環境対応Springベースアプリケーションの開発を効率化する堅牢なフレームワーク、Spring Bootをご存じでしょう。その多くの機能の1つが、データの整合性を確保し、ユーザー体験を向上させるうえで重要なBean Validationです。

Spring Bootアプリケーションでは、入力やフォームの検証、データベースへの保存前のデータ検証、さまざまなセキュリティポリシーの適用にBean Validationを利用できます。Bean Validationを使うことで、開発者はエラーを防ぎ、アプリケーション全体の品質を高め、アプリケーション全体でデータの一貫性を保てます。

この記事では、Bean Validationのさまざまな用途と、Spring Bootでの実装方法について解説します。自分のプロジェクトで効果的に活用する方法を学びましょう。

Bean Validationとは

Bean Validationは、アプリケーションのデータモデルに制約を適用し、データが処理または保存される前に特定のルールに準拠していることを確認できる機能です。これは、制約の定義と検証に使用するアノテーションとインターフェースを提供するJakarta Bean Validationとの統合によって実現されます。

Bean Validationを適用すると、開発プロセスの早い段階でエラーや不整合を検出できるため、開発者は時間とリソースを節約できます。また、誤ったデータが入力されるのを減らし、ユーザー体験全体の向上にも役立ちます。

アプリケーション開発におけるBean Validationの一般的な用途をいくつか見てみましょう。

入力とフォームの検証

Bean Validationの最も一般的な用途の1つは、入力やフォームの検証です。これにより、ユーザーが正しい形式でデータを入力していることを確認できます(例:メールアドレス、ユーザー名、パスワードを使う登録フォームの検証)。

データベースへの保存前のデータ検証

Bean Validationは、データベースに保存する前のデータ検証にも利用できます。この手法により、入力されたデータの一貫性と正確性を確保できます。その結果、エラーを防ぎ、データ破損のリスクを軽減できます。

セキュリティポリシーの適用

Bean Validationは、ユーザー入力やデータベースからマッピングされたデータを検証し、パスワード強度の要件など、特定の基準を満たしていることを確認することで、セキュリティポリシーの適用にも利用できます。検証はインジェクション攻撃の防止に重要な役割を果たし、保存型クロスサイトスクリプティング(XSS)などのセキュリティ脆弱性の防止にも役立ちます。

ビジネスロジックの検証

Bean Validationを使ってビジネスロジックを検証し、特定の要件や標準を満たしていることを確認できます。パスワードの複雑さや、注文品目の数量・価格の妥当性などの制約を定義すると、Bean Validationがデータを自動的にチェックし、必要なルールを適用します。これにより手作業での確認をなくし、アプリケーションが特定の標準や要件に準拠していることを確保できます。

Spring BootでBean Validationを実装する

このチュートリアルでは、インメモリデータベースを使ったシンプルな作成・読み取り・更新・削除(CRUD)アプリケーションにBean Validationを実装します。ユーザーが名前、メールアドレス、パスワードを入力し、特定の条件を満たした場合に受け付けます。ここでは、RESTful APIを備えたバックエンドサーバーとインメモリH2データベースで構成される、次の高レベルアーキテクチャを使用します。

クライアントアプリケーションがWebサービス(このチュートリアルで作成するアプリケーション)を呼び出すと、リクエストはUserController.に届きます。次に、コントローラーがリクエストデータを検証します。データが有効であれば、リクエストはUserRepositoryに渡され、インメモリデータベースと通信して該当する応答を返します。無効な場合は、エラーメッセージとともにリクエストがクライアントに返されます。

クライアントのリクエストがUserControllerのBean Validationを通ってUserRepositoryに渡される流れと、正常時および検証エラー時の経路を示すフロー図。

前提条件

実装を始める前に、次のツールとテクノロジーがインストールされていることを確認してください。

新しいSpring Bootプロジェクトを作成する

新しいSpring Bootプロジェクトを作成するには、Spring Initializrにアクセスし、次のオプションを選択します。

  • プロジェクトの種類: Maven Project

  • 言語: Java

  • パッケージ形式: Jar

  • Javaのバージョン: 17

  • Spring Boot: 3.1.0 (SNAPSHOT)

Project Metadataセクションに次の詳細を入力します。

  • Group: com.example

  • Artifact: simple-crud-bean-validation

  • Name: simple-crud-bean-validation

  • Description: Spring Bootを使ったシンプルなCRUDアプリケーション

  • Package name: com.example.simplecrud

次の依存関係も追加します。

  • Web: Spring Web

  • Validation: Bean Validation

  • H2 Database: H2 Database

  • Spring Data JPA: Spring Data and Hibernate

Bean Validation、H2 Database、Spring Data JPA、Spring Webの依存関係を含む、Java Spring Boot CRUDプロジェクトのSpring Initializr設定

Generateをクリックして、プロジェクトをZIPファイルとしてダウンロードします。ZIPファイルを展開し、お好みのIDEにプロジェクトをインポートします。

アプリケーションを実装する

Spring Bootアプリケーションの標準的なプロジェクト構成に従います。プロジェクト全体の構成は次のとおりです。

シンプルなCRUD Bean Validationアプリケーションのプロジェクトディレクトリツリー。Javaのソース、モデル、リポジトリ、コントローラー、テスト、Mavenの各ファイルを表示。

Userモデル

アプリケーションを実装するには、まずsrc/main/java/com/example/simplecrudbeanvalidation内にmodelディレクトリを作成します。次に、modelディレクトリ内にUser.javaというファイルを作成し、次のフィールドとBean ValidationアノテーションをUserクラスに追加します。

import jakarta.persistence.*;
import jakarta.validation.constraints.*;

@Entity
@Table(name="users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @NotEmpty
    @Pattern(regexp = "[a-zA-Z0-9 ]")
    private String name;

    @NotNull
    @NotEmpty
    @Email
    @Pattern(regexp=".+@.+\\..+")
    private String email;

    @NotNull
    @NotEmpty
    @Size(min = 8, max = 64)
    private String password;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public User(Long id, String name, String email, String password){
        this.id = id;
        this.name = name;
        this.email = email;
        this.password = password;
    }

    //default constructor
    public User(){}
}

このコードは、Jakarta Persistence API(JPA)のアノテーションを使い、データベース内のusersテーブルを表すUserというJavaクラスを定義します。インスタンス変数はid、name、email、passwordの4つで、それぞれに対応するgetterとsetterがあります。各フィールドには、@NotNull、@NotEmpty、@Email、@Size、@Patternなどの検証制約が設定されています。@Patternは、パスワードが8~64文字で、数字、小文字、大文字、特殊文字をそれぞれ少なくとも1つ含む必要があることを指定します。また、@Patternはnameプロパティにも使われ、スクリプトインジェクションを防ぎます。このクラスは、データベースにユーザーデータを保存したり、取得したりするためのモデルクラスとして使用できます。

Userリポジトリ

インメモリデータベースでCRUD操作を処理するには、src/main/java/com/example/simplecrudbeanvalidation内にrepositoryディレクトリを作成します。次に、repositoryのdirectory内にUserRepository.javaというファイルを作成し、次のようにUserRepositoryインターフェースを定義します。

import com.example.simplecrudbeanvalidation.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends CrudRepository<User, Long> {
}

このコードは、Spring Data JPAが提供するCrudRepositoryインターフェースを拡張する、UserRepositoryというJavaインターフェースを定義します。UserRepositoryインターフェースでは、CrudRepositoryに2つの型パラメーターを指定します。1つはこのリポジトリが管理するエンティティ型のUser、もう1つはエンティティの主キー型のLongです。

CrudRepositoryを拡張することで、UserRepositoryはUserエンティティに対するCRUD操作用の複数のメソッドを継承します。これらのメソッドを使うと、複雑な設定や本格的なデータベースサーバーを用意せずに、インメモリデータベースと連携してUserオブジェクトを管理できます。

Userコントローラー

RESTful APIのエンドポイントと検証ロジックを定義するUserControllerクラスを作成するには、まずsrc/main/java/com/example/simplecrudbeanvalidationディレクトリ内にcontrollerというディレクトリを作成します。次にcontrollerディレクトリ内にUserController.javaというファイルを作成し、次の内容を記述します。

import com.example.simplecrudbeanvalidation.model.User;
import com.example.simplecrudbeanvalidation.repository.UserRepository;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/users")
@Validated
public class UserController
{
   @Autowired
   private UserRepository userRepository;

   @GetMapping
   public List<User> getAllUsers() {
       return (List<User>) userRepository.findAll();
   }

   @GetMapping("/{id}")
   public ResponseEntity<User> getUserById(@PathVariable Long id) {
       Optional<User> userOptional = userRepository.findById(id);
       if (userOptional.isPresent()) {
           return ResponseEntity.ok(userOptional.get());
       } else {
           return ResponseEntity.notFound().build();
       }
   }

   @PostMapping
   public User createUser(@Valid @RequestBody User user) {
       return userRepository.save(user);
   }

   @PutMapping("/{id}")
   public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User updatedUser) {
       Optional<User> userOptional = userRepository.findById(id);
       if (userOptional.isPresent()) {
           User user = userOptional.get();
           user.setName(updatedUser.getName());
           user.setEmail(updatedUser.getEmail());
           user.setPassword(updatedUser.getPassword());
           userRepository.save(user);
           return ResponseEntity.ok(user);
       } else {
           return ResponseEntity.notFound().build();
       }
   }

   @DeleteMapping("/{id}")
   public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
       if (userRepository.existsById(id)) {
           userRepository.deleteById(id);
           return ResponseEntity.ok().build();
       } else {
           return ResponseEntity.notFound().build();
       }
   }
}

このコードは、UserControllerというSpring BootのRestControllerクラスを定義し、Userオブジェクトを管理するHTTPリクエストを処理します。このクラスには、すべてのユーザーの取得、IDによる単一ユーザーの取得、新規ユーザーの作成、既存ユーザーの更新、ユーザーの削除という5種類のHTTPリクエストを処理するメソッドがあります。

@Autowiredアノテーションは、UserRepositoryの依存関係をUserControllerクラスに注入するために使われます。UserControllerクラスは@RequestMappingアノテーションによって/api/usersエンドポイントに関連付けられ、受信したHTTPリクエストがこのコントローラーに割り当てられます。また、@Validatedアノテーションを使うと、リクエストパラメーターの検証が有効になります。このコードは全体として、Spring Dataとインメモリデータベースを使い、Userオブジェクトに対するCRUD操作を行うRESTful APIを提供します。

インメモリデータベースを設定する

インメモリデータベースを設定するには、src/main/resources/内のapplication.propertiesを開き、次の設定を追加します。

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

このSpring Bootの設定では、testdbという名前のH2インメモリデータベースをセットアップし、ドライバー、ユーザー名、パスワードを設定します。また、Hibernateベースのデータベース操作で使用するH2の方言も指定します。

本番環境でのH2の使用は推奨されません

本番データベースとしてH2を使用することは推奨されません。また、本番環境ではデフォルトのユーザー名とパスワードを使用しないでください。この設定はデモ用途には適していますが、本番環境では使用しないでください。

Bean Validationの単体テスト

インメモリデータベースの設定が完了したら、Bean Validationの単体テストを始められます。src/test/java/com/example/simplecrudbeanvalidation内にcontrollerディレクトリを作成します。次にcontrollerディレクトリ内にUserControllerTest.javaというファイルを作成し、Bean Validationの制約が想定どおりに機能することを確認するテストを記述します。

UserControllerTestクラスに次のコードを追加します。

import com.example.simplecrudbeanvalidation.model.User;
import com.example.simplecrudbeanvalidation.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Arrays;
import java.util.Optional;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserRepository userRepository;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void createUser_validData_success() throws Exception {
        User newUser = new User(null, "John Doe", "john.doe@example.com", "p@ssW0rd!");
        User savedUser = new User(1L, newUser.getName(), newUser.getEmail(), newUser.getPassword());

        when(userRepository.save(any(User.class))).thenReturn(savedUser);

        mockMvc.perform(post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(newUser)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(savedUser.getId()))
                .andExpect(jsonPath("$.name").value(savedUser.getName()))
                .andExpect(jsonPath("$.email").value(savedUser.getEmail()))
                .andExpect(jsonPath("$.password").value(savedUser.getPassword()));
    }

    @Test
    public void createUser_invalidData_failure() throws Exception {
        User invalidUser = new User(null, "", "invalid-email", "123");

        mockMvc.perform(post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(invalidUser)))
                .andExpect(status().isBadRequest());
    }

    @Test
    public void getAllUsers() throws Exception {
        User user1 = new User(1L, "John Doe", "john.doe@example.com", "p@ssW0rd!");
        User user2 = new User(2L, "Jane Doe", "jane.doe@example.com", "p@ssW0rd!");
        when(userRepository.findAll()).thenReturn(Arrays.asList(user1, user2));
        mockMvc.perform(get("/api/users"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(2)))
                .andExpect(jsonPath("$[0].id").value(user1.getId()))
                .andExpect(jsonPath("$[0].name").value(user1.getName()))
                .andExpect(jsonPath("$[0].email").value(user1.getEmail()))
                .andExpect(jsonPath("$[0].password").value(user1.getPassword()))
                .andExpect(jsonPath("$[1].id").value(user2.getId()))
                .andExpect(jsonPath("$[1].name").value(user2.getName()))
                .andExpect(jsonPath("$[1].email").value(user2.getEmail()))
                .andExpect(jsonPath("$[1].password").value(user2.getPassword()));
    }

    @Test
    public void getUserById_found() throws Exception {
        User user = new User(1L, "John Doe", "john.doe@example.com", "p@ssW0rd!");
        when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
        mockMvc.perform(get("/api/users/{id}", user.getId()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(user.getId()))
                .andExpect(jsonPath("$.name").value(user.getName()))
                .andExpect(jsonPath("$.email").value(user.getEmail()))
                .andExpect(jsonPath("$.password").value(user.getPassword()));
    }

    @Test
    public void getUserById_notFound() throws Exception {
        Long userId = 1L;
        when(userRepository.findById(userId)).thenReturn(Optional.empty());
        mockMvc.perform(get("/api/users/{id}", userId))
                .andExpect(status().isNotFound());
    }
}

このコードはUserControllerクラスに対するJUnitテストのセットです。テストではMockMvcクラスを使って/api/usersエンドポイントへのHTTPリクエストをシミュレートし、UserControllerクラス内の対応するメソッドが正しく処理することを確認します。テストでは、有効なデータと無効なデータを使った新規ユーザーの作成、すべてのユーザーの取得、ユーザーが存在する場合と存在しない場合のIDによるユーザー取得などを検証します。

@SpringBootTestアノテーションはアプリケーションコンテキストの読み込みに使われ、@MockBeanアノテーションはUserControllerクラスの依存関係であるUserRepositoryをモック化するために使われます。これらのテストにより、UserControllerクラスが正しく機能し、さまざまなHTTPリクエストに適切に応答することを確認できます。

テストを実行するには、ターミナルまたはシェルを開き、プロジェクトのルートディレクトリに移動して、次のコマンドを実行します。

mvn test

すべてのテストが成功したことを確認できます。

…output omitted…
[INFO] Results:
[INFO] 
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
…output omitted…

APIを実行してテストする

IntelliJでSimpleCrudBeanValidationApplication.javaファイルを右クリックし、Run 'SimpleCrudBeanValidationApplication.main()'を選択します。

IntelliJ IDEAに、Bean Validationアノテーションが付いたJavaのUserクラスと、アプリケーションの実行オプションが強調表示されたコンテキストメニューが表示されています。

または、次のコマンドを使ってターミナルやシェルからアプリケーションを実行することもできます。

mvn clean install
java -jar target/simple-crud-bean-validation-0.0.1-SNAPSHOT.jar

Spring Bootアプリケーションを起動したら、ターミナルやシェルからcurlコマンドを使ってCRUD操作をテストできます。これは、フロントエンドアプリケーションやPostmanからこれらのAPIを呼び出す方法と同様です。

CRUD操作をテストするには、まずユーザーを作成します。

curl -X POST -H "Content-Type: application/json" -d '{"name":"John Doe","email":"john.doe@example.com","password":"p@ssW0rd!"}' http://localhost:8080/api/users

次のように出力されます。

{"id":1,"name":"John Doe","email":"john.doe@example.com","password":"p@ssW0rd!"} 

次に、すべてのユーザーを取得します。

curl -X GET http://localhost:8080/api/users

次のように出力されます。

{"id":1,"name":"John Doe","email":"john.doe@example.com","password":"p@ssW0rd!"}

次に、IDを指定してユーザーを取得します。取得したいユーザーのIDを<id>に置き換えてください。

curl -X GET http://localhost:8080/api/users/<id>

出力は次のとおりです。

{"id":1,"name":"John Doe","email":"john.doe@example.com","password":"p@ssW0rd!"}

ユーザーを更新するには、更新したいユーザーのIDを<id>に置き換えます。

curl -X PUT -H "Content-Type: application/json" -d '{"name":"John Doe Updated","email":"john.doe.updated@example.com","password":"newP@ssW0rd!"}' http://localhost:8080/api/users/<id>

次のように出力されます。

{"id":1,"name":"John Doe Updated","email":"john.doe.updated@example.com","password":"newP@ssW0rd!"}

次に、削除したいユーザーのIDを<id>に置き換えて、ユーザーを削除します。

curl -X DELETE http://localhost:8080/api/users/<id>

このコマンドの出力は空です。

これらのcurlコマンドを使って、User RESTful APIのCRUD操作をテストできます。各コマンドの<id>は、適切なユーザーIDに置き換えてください。以下の手順に沿ってSpring Bootアプリケーションを操作し、実行時にBean Validationの制約が想定どおりに機能することを確認できます。

無効なメールアドレスやパスワード、空の値など、不正なデータを使ってこれらのcurlリクエストを再度実行すると、APIから不正なリクエストのエラーが返されます。このエラーメッセージは、コントローラーレベルで行われる検証(Bean Validationとも呼ばれます)が失敗したことを示しています。

不正なリクエストの例を見てみましょう。ここでは、無効なメールアドレスでユーザーを作成します。

curl -X POST -H "Content-Type: application/json" -d '{"name":"John Doe","email":"johexample","password":"p@ssW0rd!"}' http://localhost:8080/api/users

出力は次のようになります。

{"timestamp":"2023-03-28T10:27:21.884+00:00","status":400,"error":"Bad Request","path":"/api/users"}

次に、無効なパスワードでユーザーを作成します。

curl -X POST -H "Content-Type: application/json" -d '{"name":"John Doe","email":"joh@example.com","password":"test"}' http://localhost:8080/api/users

出力は次のとおりです。

{"timestamp":"2023-03-28T10:29:30.793+00:00","status":400,"error":"Bad Request","path":"/api/users"}

Bean Validationのまとめ

この記事では、シンプルなCRUDアプリケーションにSpring BootのBean Validationを実装する方法を学びました。Spring Initializrを使ってプロジェクトを作成し、必要な依存関係を追加したうえで、Bean Validationのアノテーションを使ったユーザーモデルを実装しました。また、UserRepositoryと、Bean Validationを利用するRESTful APIエンドポイントを備えたUserControllerを実装し、Bean Validationの制約が想定どおりに機能することを確認するテストを作成しました。

アプリケーションのセキュリティをさらに強化するには、インジェクション攻撃につながる安全でないコードパターンを検出するSASTのSnyk Codeを活用しましょう。IntelliJ IDE向けSnyk Securityプラグイン拡張機能を使うと、この分析を開発ワークフローに組み込めます。

Spring BootのBean Validationの仕組みを理解したら、自分のプロジェクトにも実装して、データの整合性を確保し、ユーザーエクスペリエンスを向上させましょう。完成したコードは、こちらのGitHubリポジトリでご確認ください。

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。