Spring MVCの@ControllerAdvice
と@ExceptionHandler
を使用して、REST APIのすべての例外を処理しています。 Web MVCコントローラーによってスローされる例外に対しては正常に機能しますが、コントローラーメソッドが呼び出される前に実行されるため、スプリングセキュリティカスタムフィルターによってスローされる例外に対しては機能しません。
私はトークンベースの認証を行うカスタムスプリングセキュリティフィルターを持っています:
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ボディを返すことです。 Spring Security AuthenticationEntryPointとSpring mvc @ExceptionHandlerを連携させる方法はありますか?
Spring Security 3.1.4とSpring MVC 3.2.4を使用しています。
OK、私はAuthenticationEntryPointからjsonを自分で書くことを提案されたように試みましたが、それは動作します。
テストのために、response.sendErrorを削除してAutenticationEntryPointを変更しました
@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を使用している場合でも、カスタムのjsonデータと401を無許可で送信できます。
明らかに、テスト目的で行ったようにjsonをビルドするのではなく、クラスインスタンスをシリアル化します。
これは非常に興味深い問題です。Spring SecurityおよびSpring Webフレームワークは応答の処理方法に一貫性がありません。 MessageConverter
によるエラーメッセージ処理を手軽にネイティブでサポートする必要があると思います。
MessageConverter
をSpring Securityに注入して、例外をキャッチしてコンテンツネゴシエーションに従って正しい形式で返すにするためのエレガントな方法を見つけようとしました。それでも、以下の私のソリューションはエレガントではありませんが、少なくとも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でそれを行います。ここでは関連する構成を示していますが、セッションstatelessなどの他の構成が必要です。
@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構成でResourceServerConfigurerAdapter
の代わりにWebSecurityConfigurerAdapter
を拡張し、メソッド内でconfigure(ResourceServerSecurityConfigurer resources)
をオーバーライドしてresources.authenticationEntryPoint(customAuthEntryPoint())
を使用してカスタムAuthenticationEntryPoint
を登録するのは比較的簡単で便利です。
このようなもの:
@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();
}
}
また、ナイスOAuth2AuthenticationEntryPoint
があり、これは(最終ではないため)拡張して、カスタムAuthenticationEntryPoint
の実装中に部分的に再利用できます。特に、エラー関連の詳細を含む「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");
}
}
}
これで、構成されたジャクソン、Jaxb、またはMVCアノテーションまたはXMLベースの構成の応答本文をシリアライザー、デシリアライザーなどで変換するために使用するものを注入できます。
フィルターでメソッド '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());
}
}
その場合、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")
を追加する必要があります
私はobjectMapperを使用しています。すべてのRESTサービスはほとんどjsonで動作しており、設定の1つでオブジェクトマッパーを既に設定しています。
コードはKotlinで書かれています。うまくいけばうまくいきます。
@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を使用しています。 、もう一方は、Reactive WebとReactive Securityで同等のものを使用しています。
- 通常のWeb + Jwtセキュリティ
- リアクティブJwt
JSONベースのエンドポイントに常に使用するものは次のようになります。
@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
@Autowired
ObjectMapper mapper;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthEntryPoint.class);
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e)
throws IOException, ServletException {
// Called when the user tries to access an endpoint which requires to be authenticated
// we just return unauthorizaed
logger.error("Unauthorized error. Message - {}", e.getMessage());
ServletServerHttpResponse res = new ServletServerHttpResponse(response);
res.setStatusCode(HttpStatus.UNAUTHORIZED);
res.getServletResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
res.getBody().write(mapper.writeValueAsString(new ErrorResponse("You must authenticated")).getBytes());
}
}
Spring Web Starterを追加すると、オブジェクトマッパーは既にBeanになっていますが、ObjectMapperで行うことは次のとおりです。
@Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.modules(new JavaTimeModule());
// for example: Use created_at instead of createdAt
builder.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
// skip null fields
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return builder;
}
WebSecurityConfigurerAdapterクラスでデフォルトとして設定したAuthenticationEntryPoint:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ............
@Autowired
private JwtAuthEntryPoint unauthorizedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
// .antMatchers("/api/auth**", "/api/login**", "**").permitAll()
.anyRequest().permitAll()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.headers().frameOptions().disable(); // otherwise H2 console is not available
// There are many ways to ways of placing our Filter in a position in the chain
// You can troubleshoot any error enabling debug(see below), it will print the chain of Filters
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
// ..........
}