アイキャッチ

実装・エラー解決

Spring MVCでヘッダー不足とフォームの入力エラーを区別する|@RequestHeader・@Valid

このページに書いてあること。

Spring Bootのリクエストチェックを・・・・

  • メソッド引数として単項目(@RequestHeader)で実施する。
  • オブジェクトとBeanValidationにより複数項目まとめて実施する。
  • この時の例外ハンドリングの違いについての覚書(←これが重要)

Springの機能として、リクエストチェックをメソッドの引数とアノテーションで実施することができます。

やり方は他の記事で大量に出てくるので省略して簡単に済ませましょう。

1)メソッド引数として単項目(@RequestHeader)で実施する。

最初の例は @RequestHeader の必須ヘッダー欠落です。以下の旧コード・ログは2018年の記録として読み、現行で実行したコードは結論を参照してください。

public class UserInfoEndpoint {

	@Autowired
	LogUtil logUtil;

	@RequestMapping("userinfo")
	public String index(@RequestHeader(name = "Authorization") String authorization) {

		return null;
	}
}


必須ヘッダーが届かない場合は、Controllerを呼ぶ前に引数の解決が失敗します。Controllerメソッドに対するAOPだけでは、この失敗を捕まえられません。

HeaderErrorAdvice.java の全文

package info.syachiku.maintenance;

import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice(assignableTypes = HeaderAndFormController.class)
class HeaderErrorAdvice {
    @ExceptionHandler(MissingRequestHeaderException.class)
    ResponseEntity<Map<String, String>> missingHeader() {
        return ResponseEntity.badRequest().body(Map.of("code", "MISSING_HEADER"));
    }
}

現行の例外処理は欠落だけを対象にします。リクエストヘッダーの値やスタックトレースをレスポンスへ出す必要はありません。

この時に発生した例外は、以下の通り。

2018-11-19 21:05:41.108 [http-nio-8080-exec-3] [WARN] - WA000001 [ServletWebRequest: uri=/userinfo;client=0:0:0:0:0:0:0:1]. {}.
org.springframework.web.bind.ServletRequestBindingException: Missing request header 'Authorization' for method parameter of type String

旧ログでは ServletRequestBindingException が出ています。現在版では、そのサブクラスの MissingRequestHeaderException として検出します。

ここで、バリデーション対象のリクエスト項目が増えたので、メソッドの引数に夜バリデーションをやめてオブジェクトに詰めて一気にバリデーションを実施します。

2)オブジェクトとBeanValidationにより複数項目まとめて実施する。

簡単に言えば、DTOに変数を定義して、そこにオートバインドしてもらった上で、@notnullとかを使ってバリデーションしてしまおうというお話。
こんなDTOクラスを用意。

import javax.validation.constraints.NotBlank;

import lombok.Getter;
import lombok.Setter;

/**
 * AuthorizationEndpointのリクエストを格納するDTO
 *
 */
@Getter
@Setter
public class AuthorizationRequestDto {

	@NotBlank
	private String response_type;

	private String client_id;
}


そして、Controllerを修正。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * OIDCに於ける、OPのAuthorizationEndpointを提供するクラス<br>
 *
 */
@RestController
public class AuthorizationEndpoint {

	@Autowired
	LogUtil logUtil;

	/**
	 * Authorizationリクエストを受診して、認証を実施するメソッド<br>
	 * 
         */
	@RequestMapping(value = "authorize", method = RequestMethod.GET)
	public String index(@Validated AuthorizationRequestDto authorizationRequestDto) {
		logUtil.appl(ApplicationLogEnum.DE000001,authorizationRequestDto.getResponse_type(),authorizationRequestDto.getClient_id());
		return null;
	}
}

すると、先ほどのExceptionHandlerに以下のログが出ました、

2018-11-19 21:05:33.277 [http-nio-8080-exec-1] [WARN] - WA000001 [ServletWebRequest: uri=/authorize;client=0:0:0:0:0:0:0:1]. {}.
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'authorizationRequestDto' on field 'response_type': rejected value [null]; codes [NotBlank.authorizationRequestDto.response_type,NotBlank.response_type,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [authorizationRequestDto.response_type,response_type]; arguments []; default message [response_type]]; default message [must not be blank]

旧環境でフォームに必須項目がなければ BindException が発生していました。配布サンプルはSpring MVC 6.2.19で、同じ場面の MethodArgumentNotValidException を確認します。

結論

実行用サンプル

必須ヘッダーの欠落、空白、フォームDTOの項目欠落を、実際のMVC例外と400応答で比較します。 検証コードを見る。 JDK 17とMaven 3.9系で試せます。ソース・テスト・pom.xml・READMEをZIPに収録しています。

HeaderAndFormController.java の全文

package info.syachiku.maintenance;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
class HeaderAndFormController {
    @GetMapping("/userinfo")
    ResponseEntity<Void> userInfo(@RequestHeader("Authorization") @NotBlank String authorization) {
        return ResponseEntity.noContent().build();
    }

    @GetMapping("/authorize")
    ResponseEntity<Void> authorize(@Valid @ModelAttribute AuthorizationInput input) {
        return ResponseEntity.noContent().build();
    }
}

record AuthorizationInput(@BindParam("response_type") @NotBlank String responseType) {
}

テストでは、/userinfo のヘッダー欠落を MissingRequestHeaderException、空白を HandlerMethodValidationException、/authorize のフォーム必須項目欠落を MethodArgumentNotValidException として確認します。必要な値がある場合は204です。このサンプルは認証処理を行いません。

関連記事Spring Bootで@RequestParamのバリデーションが効かない原因|@Validatedは必要? クエリパラメーターの欠落、空白、型変換を区別し、Spring MVC 6.1以降で発生する例外を確認できます。 記事を読む

考えれば当たり前で、なんてことはない話ですが・・・・

ヘッダーがない場合と、値はあるが空白の場合は別の失敗です。前者は引数の解決、後者はメソッド引数の制約検証で失敗します。

フォームDTOの項目が不足した場合は、バインド先オブジェクトの制約検証で失敗します。

HTTPステータスはどれも400です。応答をカスタマイズする場合は、対象の例外だけにハンドラーを付けます。

-実装・エラー解決
-,