使用@ExceptionHandler处理spring安全认证异常
我使用Spring MVC的@ControllerAdvice
和@ExceptionHandler
来处理REST API的所有异常。 它适用于web mvc控制器抛出的异常,但对于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安全性AuthenticationException。 有没有办法使Spring安全AuthenticationEntryPoint和spring mvc @ExceptionHandler一起工作?
我使用的是spring 3.1.4和spring mvc 3.2.4。
好吧,我试着建议从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代码。
我假设你知道如何包括杰克逊和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()); } }
尝试一些身份验证失败的情况下,记住请求头应该包括接受:XXX ,你应该得到的JSON,XML或其他格式的异常。
从@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的配置上的响应体转换。
在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”标题和错误相关的细节。
希望这会帮助别人。
我发现的最好的方法是将异常委托给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以你想要的方式格式化响应。
我正在使用objectMapper。 Every Rest Service主要使用json,在你的一个配置中你已经配置了一个对象映射器。
代码是写在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) }}
- 如何让黄瓜让Spring注入一步定义类?
- Kotlin + SpringBoot 2.0.0-M4失败tu加载应用程序上下文(BeanCreationException)
- Gradle脚本Kotlin和dependencyManagement
- 我不能在Springboot-Kotlin中排除MongoAutoConfiguration(MongoSocketOpenException)
- Spring Boot将文本/ JavaScript序列化为JSON
- Spring Data JPA + Guice错误:使用@EnableTransactionManagement注解时,没有类型的主要bean定义
- 选择在休眠
- 无法使用Gradle在IntelliJ Community Edition中添加应用程序服务器
- kotlin中的数字是不可序列化的