Gestire le eccezioni di autenticazione di sicurezza a molla con @ExceptionHandler

Sto usando @ControllerAdvice ‘s @ControllerAdvice e @ExceptionHandler per gestire tutte le eccezioni di @ExceptionHandler REST. Funziona bene per le eccezioni generate dai controller Web mvc, ma non funziona per le eccezioni generate dai filtri personalizzati di sicurezza primaverile perché vengono eseguite prima che i metodi del controller vengano richiamati.

Dispongo di un filtro di sicurezza personalizzato che esegue un’authorization basata su token:

 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); } } } 

Con questo punto di ingresso personalizzato:

 @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()); } } 

E con questa class per gestire le eccezioni a livello globale:

 @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; } } 

Quello che devo fare è restituire un corpo JSON dettagliato anche per la sicurezza di spring AuthenticationException. C’è un modo per far funzionare la sicurezza di spring AuthenticationEntryPoint e spring mvc @ExceptionHandler insieme?

Sto usando spring security 3.1.4 e spring mvc 3.2.4.

Ok, ho provato come suggerito a scrivere il jSON personalmente da AuthenticationEntryPoint e funziona.

Solo per il test ho cambiato l’AutenticationEntryPoint rimuovendo 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() + "\" }"); } } 

In questo modo è ansible inviare dati personali JSON insieme a 401 non autorizzati anche se si utilizza Spring Security AuthenticationEntryPoint.

Ovviamente non si costruisce il json come ho fatto a scopo di test ma si serializza qualche istanza di class.

Questo è un problema molto interessante che il framework Spring Security e Spring Web non è abbastanza coerente nel modo in cui gestiscono la risposta. Credo che debba supportare in modo nativo la gestione dei messaggi di errore con MessageConverter in modo pratico.

Ho cercato di trovare un modo elegante per inserire MessageConverter in Spring Security in modo che potessero rilevare l’eccezione e restituirli in un formato corretto in base alla negoziazione del contenuto . Tuttavia, la mia soluzione qui sotto non è elegante ma almeno fa uso del codice Spring.

Presumo che tu sappia come includere la libreria di Jackson e JAXB, altrimenti non ha senso procedere. Ci sono 3 passaggi in totale.

Passaggio 1: crea una class autonoma, memorizzando MessageConverters

Questa class non ha magia. Memorizza semplicemente i convertitori di messaggi e un processore RequestResponseBodyMethodProcessor . La magia è all’interno di quel processore che farà tutto il lavoro compresa la negoziazione del contenuto e la conversione del corpo della risposta di conseguenza.

 public class MessageProcessor { // Any name you like // List of HttpMessageConverter private List> 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>(); this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(new StringHttpMessageConverter()); this.messageConverters.add(new ResourceHttpMessageConverter()); this.messageConverters.add(new SourceHttpMessageConverter()); 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> getMessageConverters() { return messageConverters; } } 

Passaggio 2: crea AuthenticationEntryPoint

Come in molte esercitazioni, questa class è essenziale per implementare la gestione degli errori personalizzata.

 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(); } } } 

Passaggio 3: registrare il punto di ingresso

Come accennato, lo faccio con Java Config. Ho appena mostrato la configurazione rilevante qui, ci dovrebbero essere altre configurazioni come la sessione senza stato , ecc.

 @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint()); } } 

Prova con alcuni casi di errore di autenticazione, ricorda che l’intestazione della richiesta dovrebbe includere Accetta: XXX e dovresti ottenere l’eccezione in JSON, XML o altri formati.

Il modo migliore che ho trovato è debind l’eccezione a 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); } } 

quindi puoi usare @ExceptionHandler per formattare la risposta nel modo desiderato.

Prendendo le risposte da @Nicola e @Victor Wing e aggiungendo un modo più standardizzato:

 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"); } } } 

Ora, puoi iniettare Jackson, Jaxb o qualsiasi altra cosa tu usi per convertire i corpi di risposta sulla tua annotazione MVC o sulla configurazione basata su XML con i suoi serializzatori, deserializzatori e così via.

In caso di Spring Boot e @EnableResourceServer , è relativamente semplice e conveniente estendere ResourceServerConfigurerAdapter invece di WebSecurityConfigurerAdapter nella configurazione Java e registrare un object AuthenticationEntryPoint personalizzato sovrascrivendo configure(ResourceServerSecurityConfigurer resources) e utilizzando resources.authenticationEntryPoint(customAuthEntryPoint()) all’interno del metodo .

Qualcosa come questo:

 @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(); } } 

C’è anche un bel OAuth2AuthenticationEntryPoint che può essere esteso (dal momento che non è definitivo) e parzialmente riutilizzato durante l’implementazione di un AuthenticationEntryPoint personalizzato. In particolare, aggiunge intestazioni “WWW-Authenticate” con dettagli relativi agli errori.

Spero che questo aiuti qualcuno.

Sto usando objectMapper. Ogni servizio di ripristino funziona principalmente con JSON e in una delle tue configurazioni hai già configurato un object mapper.

Il codice è scritto in Kotlin, si spera che sia ok.

 @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) }}