Spring MVC の @ControllerAdvice
と @ExceptionHandler
を使って、REST Api のすべての例外を処理しています。Web MVCのコントローラから投げられる例外については問題なく動作しますが、Spring Securityのカスタムフィルタから投げられる例外については、コントローラのメソッドが呼び出される前に実行されるので動作しません。
私は、トークンベースの認証を行うカスタムのスプリングセキュリティフィルタを持っています:
public class AegisAuthenticationFilter extends GenericFilterBean {
...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
try {
...
} catch(AuthenticationException authenticationException) {
SecurityContextHolder.clearContext();
authenticationEntryPoint.commence(request, response, authenticationException);
}
}
}
このカスタムエントリーポイントで
@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
}
}
そして、このクラスで例外をグローバルに処理することができます:
@ControllerAdvice
public class RestEntityResponseExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({ InvalidTokenException.class, AuthenticationException.class })
@ResponseStatus(value = HttpStatus.UNAUTHORIZED)
@ResponseBody
public RestError handleAuthenticationException(Exception ex) {
int errorCode = AegisErrorCode.GenericAuthenticationError;
if(ex instanceof AegisException) {
errorCode = ((AegisException)ex).getCode();
}
RestError re = new RestError(
HttpStatus.UNAUTHORIZED,
errorCode,
"...",
ex.getMessage());
return re;
}
}
私が行う必要があるのは、春のセキュリティAuthenticationExceptionの場合でも、詳細なJSONボディを返すことです。春セキュリティAuthenticationEntryPointと春mvcの@ExceptionHandlerを一緒に動作させる方法はありますか?
私は、spring security 3.1.4とspring mvc 3.2.4を使っています。
OK、提案通りAuthenticationEntryPointから自分でjsonを書き込んでみたらうまくいったよ。
テストのために、AutenticationEntryPointを変更し、response.sendErrorを削除しました。
@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().println("{ \"error\": \"" + authenticationException.getMessage() + "\" }");
}
}
この方法で、Spring Security AuthenticationEntryPointを使用している場合でも、401の無許可と一緒にカスタムjsonデータを送信することができます。
もちろん、テスト用に私がやったようにjsonをビルドするのではなく、クラスのインスタンスをシリアライズすることになります。
この問題は、Spring SecurityとSpring Webのフレームワークが、レスポンスの処理方法に一貫性がないことが非常に興味深いです。私は、MessageConverter
を使ったエラーメッセージの処理を便利な方法でネイティブにサポートしなければならないと考えています。
私は、Spring SecurityにMessageConverter
を注入して、例外をキャッチし、コンテンツ交渉に従って正しいフォーマットで返すことができるようにするエレガントな方法を見つけようとしました。それでも、以下の私の解決策はエレガントではないが、少なくともSpringのコードを利用することができる。
JacksonとJAXBライブラリのインクルード方法を知っていると仮定します。全部で3つのステップがあります。
このクラスは何の魔法も使いません。単にメッセージコンバータとプロセッサ RequestResponseBodyMethodProcessor
を格納するだけです。このプロセッサは、コンテンツネゴシエーションやレスポンスボディの変換など、すべての作業を行います。
public class MessageProcessor { // Any name you like
// List of HttpMessageConverter
private List<HttpMessageConverter<?>> messageConverters;
// under org.springframework.web.servlet.mvc.method.annotation
private RequestResponseBodyMethodProcessor processor;
/**
* Below class name are copied from the framework.
* (And yes, they are hard-coded, too)
*/
private static final boolean jaxb2Present =
ClassUtils.isPresent("javax.xml.bind.Binder", MessageProcessor.class.getClassLoader());
private static final boolean jackson2Present =
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", MessageProcessor.class.getClassLoader()) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", MessageProcessor.class.getClassLoader());
private static final boolean gsonPresent =
ClassUtils.isPresent("com.google.gson.Gson", MessageProcessor.class.getClassLoader());
public MessageProcessor() {
this.messageConverters = new ArrayList<HttpMessageConverter<?>>();
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
this.messageConverters.add(new ResourceHttpMessageConverter());
this.messageConverters.add(new SourceHttpMessageConverter<Source>());
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (jaxb2Present) {
this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
this.messageConverters.add(new GsonHttpMessageConverter());
}
processor = new RequestResponseBodyMethodProcessor(this.messageConverters);
}
/**
* This method will convert the response body to the desire format.
*/
public void handle(Object returnValue, HttpServletRequest request,
HttpServletResponse response) throws Exception {
ServletWebRequest nativeRequest = new ServletWebRequest(request, response);
processor.handleReturnValue(returnValue, null, new ModelAndViewContainer(), nativeRequest);
}
/**
* @return list of message converters
*/
public List<HttpMessageConverter<?>> getMessageConverters() {
return messageConverters;
}
}
多くのチュートリアルでそうですが、このクラスはカスタムエラー処理を実装するのに必須です。
public class CustomEntryPoint implements AuthenticationEntryPoint {
// The class from Step 1
private MessageProcessor processor;
public CustomEntryPoint() {
// It is up to you to decide when to instantiate
processor = new MessageProcessor();
}
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
// This object is just like the model class,
// the processor will convert it to appropriate format in response body
CustomExceptionObject returnValue = new CustomExceptionObject();
try {
processor.handle(returnValue, request, response);
} catch (Exception e) {
throw new ServletException();
}
}
}
前述の通り、私はJava Configで行っています。ここでは関連する設定を示しているだけで、他にもセッション ステートレス などの設定があるはずです。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint());
}
}
リクエストヘッダには Accept : XXX が含まれていて、JSONやXMLなどのフォーマットで例外を取得する必要があることを思い出して、認証失敗のケースをいくつか試してみてください。
私が見つけた最良の方法は、例外をHandlerExceptionResolverに委任することです。
@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Autowired
private HandlerExceptionResolver resolver;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
resolver.resolveException(request, response, null, exception);
}
}
次に、@ ExceptionHandlerを使用して、応答を好きなようにフォーマットできます。
Spring Bootと @ EnableResourceServer
の場合、Java構成で WebSecurityConfigurerAdapter
の代わりに ResourceServerConfigurerAdapter
を拡張し、 configure(ResourceServerSecurityConfigurerリソース)
をオーバーライドしてカスタム AuthPointticationEntryPoint
Enter.
このような何か:
@Configuration
@EnableResourceServer
public class CommonSecurityConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(customAuthEntryPoint());
}
@Bean
public AuthenticationEntryPoint customAuthEntryPoint(){
return new AuthFailureHandler();
}
}
カスタム「AuthenticationEntryPoint」の実装中に拡張(最終ではないため)して部分的に再利用できる素敵な「OAuth2AuthenticationEntryPoint」もあります。 特に、エラー関連の詳細を含む「WWW-Authenticate」ヘッダーを追加します。
これが誰かを助けることを願っています。
NicolaとVictor Wingから回答を得て、より標準的な方法を追加しました:
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class UnauthorizedErrorAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {
private HttpMessageConverter messageConverter;
@SuppressWarnings("unchecked")
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
MyGenericError error = new MyGenericError();
error.setDescription(exception.getMessage());
ServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
outputMessage.setStatusCode(HttpStatus.UNAUTHORIZED);
messageConverter.write(error, null, outputMessage);
}
public void setMessageConverter(HttpMessageConverter messageConverter) {
this.messageConverter = messageConverter;
}
@Override
public void afterPropertiesSet() throws Exception {
if (messageConverter == null) {
throw new IllegalArgumentException("Property 'messageConverter' is required");
}
}
}
これで、MVCアノテーションやXMLベースの設定に、設定されたJacksonやJaxbなどのレスポンスボディを変換するために使用するものを、そのシリアライザーやデシリアライザーなどとともに注入できるようになりました。
その場合は「HandlerExceptionResolver」を使用する必要があります。
@Component
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Autowired
//@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
resolver.resolveException(request, response, null, authException);
}
}
また、オブジェクトを返すには、例外ハンドラクラスを追加する必要があります。
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(AuthenticationException.class)
public GenericResponseBean handleAuthenticationException(AuthenticationException ex, HttpServletResponse response){
GenericResponseBean genericResponseBean = GenericResponseBean.build(MessageKeys.UNAUTHORIZED);
genericResponseBean.setError(true);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return genericResponseBean;
}
}
HandlerExceptionResolver
の複数の実装が原因で、プロジェクトの実行時にエラーが発生する可能性があります。その場合、 HandlerExceptionResolver
に @ Qualifier( "handlerExceptionResolver")
を追加する必要があります。
フィルターでメソッド「unsuccessfulAuthentication」をオーバーライドするだけで、それを処理できました。 そこで、目的のHTTPステータスコードを使用してエラー応答をクライアントに送信します。
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
if (failed.getCause() instanceof RecordNotFoundException) {
response.sendError((HttpServletResponse.SC_NOT_FOUND), failed.getMessage());
}
}
objectMapperを使用しています。 すべてのRest Serviceは主にjsonで動作しており、構成の1つではすでにオブジェクトマッパーを構成しています。
コードはコトリンで書かれていますが、うまくいけば大丈夫です。
@Bean
fun objectMapper(): ObjectMapper {
val objectMapper = ObjectMapper()
objectMapper.registerModule(JodaModule())
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
return objectMapper
}
class UnauthorizedAuthenticationEntryPoint : BasicAuthenticationEntryPoint() {
@Autowired
lateinit var objectMapper: ObjectMapper
@Throws(IOException::class, ServletException::class)
override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) {
response.addHeader("Content-Type", "application/json")
response.status = HttpServletResponse.SC_UNAUTHORIZED
val responseError = ResponseError(
message = "${authException.message}",
)
objectMapper.writeValue(response.writer, responseError)
}}
更新:コードを直接表示したい場合は、2つの例があります。1つは探している標準のSpring Securityを使用しており、もう1つはReactive WebとReactiveに相当するものを使用しています。セキュリティ:
。
-通常のWeb + Jwt Security
。
-リアクティブJwt
。
。
JSONベースのエンドポイントに常に使用するものは次のようになります。
@コンポーネント。
公開クラスJwtAuthEntryPointはAuthenticationEntryPoint {を実装しています。
@Autowired。
ObjectMapper mapper ;
プライベートスタティック最終ロガーロガー= LoggerFactory.ge tLogger(JwtAuthEntryPoint.cl ass);。
@オーバーライド。
public void commence(HttpServletRequest request、。
HttpServletResponse応答、。
AuthenticationException e)。
IOException、ServletException {を投げます。
//ユーザーが認証が必要なエンドポイントにアクセスしようとしたときに呼び出されます。
//無許可で返送します。
logger.er ror( "無許可のエラー。 メッセージ-{{{}}} "、e.ge tMessage());。
ServletServerHttpResponse res = new ServletServerHttpResponse(response);。
res.se tStatusCode(HttpStatus.UN AUTHORIZED);。
res.ge tServletResponse().setHeader(HttpHeaders.CO NTENT_TYPE、MediaType.AP PLICATION_JSON_VALUE);。
res.ge tBody().write(mapper.wr iteValueAsString(new ErrorResponse( "認証する必要があります"))。getBytes());。
}。
}。
``。
春のウェブスターターを追加すると、オブジェクトマッパーは豆になりますが、カスタマイズしたいので、ObjectMapperの実装は次のとおりです。
```ジャワ。
@Bean。
public Jackson2ObjectMapperBuilder objectMapperBuilder(){。
Jackson2ObjectMapperBuilderビルダー=新しいJackson2ObjectMapperBuilder();
builder.mo dules(new JavaTimeModule());。
//例:createdAtの代わりにcreated_atを使用します。
builder.pr opertyNamingStrategy(PropertyNamingStrategy.SN AKE_CASE);。
// nullフィールドをスキップします。
builder.se rializationInclusion(JsonInclude.Include.NO N_NULL);。
builder.fe aturesToDisable(SerializationFeature.WR ITE_DATES_AS_TIMESTAMPS);。
ビルダーを返す;。
}。
``。
WebSecurityConfigurerAdapterクラスで設定したデフォルトのAuthenticationEntryPoint:
```ジャワ。
@構成。
@EnableWebSecurity。
@EnableGlobalMethodSecurity(prePostEnabled = true)。
public class SecurityConfigはWebSecurityConfigurerAdapter {を拡張します。
//。 ............
@Autowired。
プライベートJwtAuthEntryPoint unrovalizedHandler ;
@オーバーライド。
protected void configure(HttpSecurity http)はException {。
http.co rs()。and().csrf().disable()。
.authorizeRequests()。
// .antMatchers( "/ api / auth **"、 "/ api / login **"、 "**").permitAll()。
.anyRequest().permitAll()。
。および()。
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)。
。および()。
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ST ATELESS);。
http.he aders().frameOptions().disable(); //それ以外の場合、H2コンソールは使用できません。
//フィルターをチェーン内の位置に配置する方法はたくさんあります。
//デバッグを可能にするエラーをトラブルシューティングできます(以下を参照)。フィルターのチェーンが印刷されます。
http.ad dFilterBefore(authenticationJwtTokenFilter()、UsernamePasswordAuthenticationFilter.cl ass);。
}。
//。 ..........
}。
``。