Używam Spring MVC @ControllerAdvice
i @ExceptionHandler
do obsługi wszystkich wyjątków interfejsu API REST. Działa dobrze w przypadku wyjątków generowanych przez kontrolery Web MVC, ale nie działa w przypadku wyjątków generowanych przez niestandardowe filtry zabezpieczeń Spring, ponieważ są one uruchamiane przed wywołaniem metod kontrolera.
Mam niestandardowy filtr sprężynowy, który wykonuje uwierzytelnianie na podstawie tokenów:
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);
}
}
}
Dzięki temu niestandardowemu punktowi wejścia:
@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());
}
}
A dzięki tej klasie do obsługi wyjątków globalnie:
@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;
}
}
Muszę zwrócić szczegółową treść JSON nawet w przypadku wyjątku AuthenticationException zabezpieczeń wiosny. Czy istnieje sposób, aby Spring Security AuthenticationEntryPoint i spring mvc @ExceptionHandler współpracowały ze sobą?
Używam Spring Security 3.1.4 i Spring MVC 3.2.4.
źródło
(@)ExceptionHandler
Będzie działać tylko wtedy, gdy żądanie jest obsługiwane przezDispatcherServlet
. Jednak ten wyjątek występuje wcześniej, ponieważ jest zgłaszany przez plikFilter
. Dlatego nigdy nie będziesz w stanie obsłużyć tego wyjątku z rozszerzeniem(@)ExceptionHandler
.EntryPoint
. Możesz chcieć skonstruować tam obiekt i wstawićMappingJackson2HttpMessageConverter
tam.Odpowiedzi:
Ok, próbowałem, zgodnie z sugestią, samodzielnie napisać plik json z AuthenticationEntryPoint i działa.
Tylko do testów zmieniłem AutenticationEntryPoint, usuwając 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() + "\" }"); } }
W ten sposób możesz wysyłać niestandardowe dane json wraz z nieautoryzowanym kodem 401, nawet jeśli używasz Spring Security AuthenticationEntryPoint.
Oczywiście nie zbudowałbyś json, tak jak ja do celów testowych, ale zrobiłbyś serializację jakiejś instancji klasy.
źródło
Jest to bardzo interesujący problem, ponieważ Spring Security i Spring Web framework nie są do końca spójne w sposobie obsługi odpowiedzi. Uważam, że musi natywnie obsługiwać komunikaty o błędach
MessageConverter
w wygodny sposób.Próbowałem znaleźć elegancki sposób na wstrzyknięcie
MessageConverter
do Spring Security, aby mogli złapać wyjątek i zwrócić je w odpowiednim formacie zgodnie z negocjacjami zawartości . Jednak moje rozwiązanie poniżej nie jest eleganckie, ale przynajmniej wykorzystuje kod Spring.Zakładam, że wiesz, jak dołączyć bibliotekę Jacksona i JAXB, w przeciwnym razie nie ma sensu kontynuować. W sumie są 3 kroki.
Krok 1 - Utwórz samodzielną klasę, przechowując MessageConverters
Ta klasa nie gra żadnej magii. Po prostu przechowuje konwertery wiadomości i procesor
RequestResponseBodyMethodProcessor
. Magia tkwi w tym procesorze, który wykona całą pracę, w tym negocjację treści i odpowiednią konwersję treści odpowiedzi.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; } }
Krok 2 - Utwórz AuthenticationEntryPoint
Podobnie jak w wielu samouczkach, ta klasa jest niezbędna do implementacji niestandardowej obsługi błędów.
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(); } } }
Krok 3 - Zarejestruj punkt wejścia
Jak wspomniano, robię to za pomocą Java Config. Po prostu pokazuję tutaj odpowiednią konfigurację, powinna istnieć inna konfiguracja, taka jak bezstanowa sesja itp.
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint()); } }
Spróbuj z niektórymi przypadkami niepowodzenia uwierzytelniania, pamiętaj, że nagłówek żądania powinien zawierać Accept: XXX i powinieneś otrzymać wyjątek w formacie JSON, XML lub innym.
źródło
InvalidGrantException
ale moja wersjaCustomEntryPoint
nie jest wywoływana. Masz jakiś pomysł, czego mógłbym przegapić?AuthenticationEntryPoint
iAccessDeniedHandler
tak jakUsernameNotFoundException
iInvalidGrantException
mogą być obsługiwane przezAuthenticationFailureHandler
jak wyjaśniono tutaj .Najlepszym sposobem, jaki znalazłem, jest delegowanie wyjątku do obiektu 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); } }
następnie możesz użyć @ExceptionHandler, aby sformatować odpowiedź tak, jak chcesz.
źródło
@ControllerAdvice
nie będzie działać, jeśli w adnotacji określono basePackages. Musiałem to całkowicie usunąć, aby umożliwić wywołanie programu obsługi.@Component("restAuthenticationEntryPoint")
? Dlaczego potrzebna jest nazwa taka jak restAuthenticationEntryPoint? Czy ma to na celu uniknięcie kolizji nazw Spring?W przypadku Spring Boot i
@EnableResourceServer
stosunkowo łatwo i wygodnie jest rozszerzyćResourceServerConfigurerAdapter
zamiastWebSecurityConfigurerAdapter
w konfiguracji Java i zarejestrować niestandardowyAuthenticationEntryPoint
, nadpisującconfigure(ResourceServerSecurityConfigurer resources)
i używającresources.authenticationEntryPoint(customAuthEntryPoint())
wewnątrz metody.Coś takiego:
@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(); } }
Jest też fajny,
OAuth2AuthenticationEntryPoint
który można rozszerzyć (ponieważ nie jest ostateczny) i częściowo ponownie wykorzystać podczas wdrażania niestandardowegoAuthenticationEntryPoint
. W szczególności dodaje nagłówki „WWW-Authenticate” ze szczegółami dotyczącymi błędów.Mam nadzieję, że to komuś pomoże.
źródło
commence()
moja funkcjaAuthenticationEntryPoint
nie jest wywoływana - czy coś mi brakuje?Biorąc odpowiedzi od @Nicola i @Victor Wing i dodając bardziej ustandaryzowany sposób:
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"); } } }
Teraz możesz wstrzyknąć skonfigurowane Jackson, Jaxb lub cokolwiek innego, czego używasz do konwersji treści odpowiedzi w adnotacji MVC lub konfiguracji opartej na XML z jego serializatorami, deserializatorami i tak dalej.
źródło
<property name="messageConverter" ref="myConverterBeanName"/>
tag. Kiedy używasz@Configuration
klasy, po prostu użyjsetMessageConverter()
metody.HandlerExceptionResolver
W takim przypadku musimy użyć .@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); } }
Musisz również dodać klasę obsługi wyjątków, aby zwrócić obiekt.
@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; } }
może pojawić się błąd w momencie uruchamiania projektu, ponieważ wielu implementacjach
HandlerExceptionResolver
, W takim przypadku trzeba dodać@Qualifier("handlerExceptionResolver")
naHandlerExceptionResolver
źródło
GenericResponseBean
jest po prostu java pojo, możesz stworzyć własneByłem w stanie sobie z tym poradzić, po prostu zastępując metodę „nieudane uwierzytelnianie” w moim filtrze. Tam wysyłam klientowi odpowiedź o błędzie z żądanym kodem statusu 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()); } }
źródło
Aktualizacja: Jeśli chcesz i wolisz zobaczyć kod bezpośrednio, mam dla Ciebie dwa przykłady, jeden wykorzystujący standardowe Spring Security, którego szukasz, a drugi używający odpowiednika Reactive Web i Reactive Security:
- Normalny Web + Jwt Security
- Reactive Jwt
Ten, którego zawsze używam dla moich punktów końcowych opartych na JSON, wygląda następująco:
@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()); } }
Program mapowania obiektów staje się fasolą po dodaniu Spring Web Starter, ale wolę go dostosować, więc oto moja implementacja dla 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; }
Domyślny punkt AuthenticationEntryPoint ustawiony w klasie WebSecurityConfigurerAdapter:
@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); } // .......... }
źródło
Dostosuj filtr i określ, jakiego rodzaju nieprawidłowości, powinna być lepsza metoda niż ta
public class ExceptionFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { String msg = ""; try { filterChain.doFilter(request, response); } catch (Exception e) { if (e instanceof JwtException) { msg = e.getMessage(); } response.setCharacterEncoding("UTF-8"); response.setContentType(MediaType.APPLICATION_JSON.getType()); response.getWriter().write(JSON.toJSONString(Resp.error(msg))); return; } }
}
źródło
Używam objectMapper. Każda usługa odpoczynku działa głównie z json, aw jednej z twoich konfiguracji skonfigurowałeś już mapowanie obiektów.
Kod jest napisany w Kotlinie, mam nadzieję, że będzie dobrze.
@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) }}
źródło
Na
ResourceServerConfigurerAdapter
zajęciach działał dla mnie poniższy fragment kodu.http.exceptionHandling().authenticationEntryPoint(new AuthFailureHandler()).and.csrf()..
nie działał. Dlatego napisałem to jako oddzielne wezwanie.public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.exceptionHandling().authenticationEntryPoint(new AuthFailureHandler()); http.csrf().disable() .anonymous().disable() .authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() .antMatchers("/subscribers/**").authenticated() .antMatchers("/requests/**").authenticated(); }
Implementacja AuthenticationEntryPoint do przechwytywania wygaśnięcia tokenu i brakującego nagłówka autoryzacji.
public class AuthFailureHandler implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json"); httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); if( e instanceof InsufficientAuthenticationException) { if( e.getCause() instanceof InvalidTokenException ){ httpServletResponse.getOutputStream().println( "{ " + "\"message\": \"Token has expired\"," + "\"type\": \"Unauthorized\"," + "\"status\": 401" + "}"); } } if( e instanceof AuthenticationCredentialsNotFoundException) { httpServletResponse.getOutputStream().println( "{ " + "\"message\": \"Missing Authorization Header\"," + "\"type\": \"Unauthorized\"," + "\"status\": 401" + "}"); } } }
źródło