Я использую Spring MVC @ControllerAdvice
и @ExceptionHandler
обрабатываю все исключения REST Api. Он отлично работает для исключений, создаваемых веб-контроллерами mvc, но не работает для исключений, создаваемых настраиваемыми фильтрами безопасности Spring, поскольку они запускаются до вызова методов контроллера.
У меня есть настраиваемый фильтр безопасности Spring, который выполняет аутентификацию на основе токенов:
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;
}
}
Что мне нужно сделать, так это вернуть подробное тело JSON даже для Spring Security AuthenticationException. Есть ли способ заставить Spring Security AuthenticationEntryPoint и spring mvc @ExceptionHandler работать вместе?
Я использую Spring Security 3.1.4 и Spring MVC 3.2.4.
источник
(@)ExceptionHandler
Будет работать, только если запрос обрабатываетсяDispatcherServlet
. Однако это исключение возникает раньше, поскольку оно вызывается объектомFilter
. Таким образом, вы никогда не сможете обработать это исключение с помощью(@)ExceptionHandler
.EntryPoint
. Возможно, вы захотите построить там объект и вставитьMappingJackson2HttpMessageConverter
туда.Ответы:
Хорошо, я попробовал, как предлагалось, написать json сам из AuthenticationEntryPoint, и он работает.
Просто для тестирования я изменил 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() + "\" }"); } }
Таким образом, вы можете отправлять пользовательские данные json вместе с неавторизованным 401, даже если вы используете Spring Security AuthenticationEntryPoint.
Очевидно, вы не станете создавать json, как я, для целей тестирования, но вы бы сериализовали некоторый экземпляр класса.
источник
Это очень интересная проблема, заключающаяся в том, что Spring Security и Spring Web framework не совсем согласованы в способе обработки ответа. Я считаю, что он должен иметь удобную встроенную поддержку обработки сообщений об ошибках
MessageConverter
.Я попытался найти элегантный способ внедрения
MessageConverter
в Spring Security, чтобы они могли перехватить исключение и вернуть их в правильном формате в соответствии с согласованием содержимого . Тем не менее, мое решение ниже не изящно, но, по крайней мере, использует код Spring.Я предполагаю, что вы знаете, как включить библиотеку Jackson и JAXB, иначе нет смысла продолжать. Всего есть 3 ступени.
Шаг 1. Создайте автономный класс, хранящий MessageConverters.
Этот класс не играет никакой магии. Он просто хранит преобразователи сообщений и процессор
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; } }
Шаг 2 - Создайте AuthenticationEntryPoint
Как и во многих руководствах, этот класс необходим для реализации настраиваемой обработки ошибок.
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(); } } }
Шаг 3 - Зарегистрируйте точку входа
Как уже упоминалось, я делаю это с помощью 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 или некоторых других форматах.
источник
InvalidGrantException
но ваша версияCustomEntryPoint
не вызывается. Есть идеи, что мне может не хватать?AuthenticationEntryPoint
иAccessDeniedHandler
такие , какUsernameNotFoundException
иInvalidGrantException
могут быть обработаны ,AuthenticationFailureHandler
как описано здесь .Лучший способ, который я нашел, - это делегировать исключение в 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, чтобы отформатировать ответ так, как вы хотите.
источник
@ControllerAdvice
не будете работать, если вы указали basePackages в аннотации. Мне пришлось полностью удалить это, чтобы можно было вызвать обработчик.@Component("restAuthenticationEntryPoint")
? Зачем нужно такое имя, как restAuthenticationEntryPoint? Чтобы избежать конфликтов имен Spring?В случае Spring Boot и
@EnableResourceServer
относительно легко и удобно расширить егоResourceServerConfigurerAdapter
вместоWebSecurityConfigurerAdapter
конфигурации Java и зарегистрировать пользовательский объектAuthenticationEntryPoint
путем переопределенияconfigure(ResourceServerSecurityConfigurer resources)
и использованияresources.authenticationEntryPoint(customAuthEntryPoint())
внутри метода.Что-то вроде этого:
@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» со сведениями об ошибках.Надеюсь, это кому-то поможет.
источник
commence()
функция myAuthenticationEntryPoint
не запускается - я что-то упустил?Взяв ответы от @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"); } } }
Теперь вы можете ввести настроенный Jackson, Jaxb или что-то еще, что вы используете для преобразования тел ответа в аннотацию MVC или конфигурацию на основе XML с ее сериализаторами, десериализаторами и т. Д.
источник
<property name="messageConverter" ref="myConverterBeanName"/>
тег. Когда вы используете@Configuration
класс, просто используйтеsetMessageConverter()
метод.Нам нужно использовать
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
, в этом случае вы должны добавить@Qualifier("handlerExceptionResolver")
наHandlerExceptionResolver
источник
GenericResponseBean
это просто java pojo, вы можете создать свое собственноеЯ смог справиться с этим, просто переопределив метод 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()); } }
источник
Обновление: если вам нравится и вы предпочитаете видеть код напрямую, у меня есть два примера для вас, один с использованием стандартной Spring Security, которую вы ищете, а другой с использованием эквивалента Reactive Web и Reactive Security:
- Нормальный Web + Jwt Security
- Reactive 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()); } }
Сопоставитель объектов становится bean-компонентом после добавления веб-стартера Spring, но я предпочитаю его настраивать, поэтому вот моя реализация для 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; }
AuthenticationEntryPoint по умолчанию, установленный в классе 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); } // .......... }
источник
Настройте фильтр и определите, что за ненормальность, должен быть лучший метод, чем этот
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; } }
}
источник
Я использую objectMapper. Каждая служба отдыха в основном работает с json, и в одной из ваших конфигураций вы уже настроили сопоставитель объектов.
Код написан на Котлине, надеюсь, все будет хорошо.
@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) }}
источник
В
ResourceServerConfigurerAdapter
классе у меня сработал фрагмент кода ниже.http.exceptionHandling().authenticationEntryPoint(new AuthFailureHandler()).and.csrf()..
не работает. Поэтому я написал это как отдельный звонок.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(); }
Реализация AuthenticationEntryPoint для обнаружения истечения срока действия токена и отсутствия заголовка авторизации.
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" + "}"); } } }
источник