如何让Spring-Security以401格式返回401响应?

我有一个带有/api/下所有控制器的应用程序的ReST API,这些都返回带有@ControllerAdvice JSON响应,它处理映射到JSON格式结果的所有exception。

从Spring 4.0开始,它的效果很好@ControllerAdvice现在支持在注释上进行匹配。 我无法解决的是如何为401返回JSON结果 – 未经validation和400 – 错误请求响应。

相反,Spring只是将响应返回给容器(tomcat),将其呈现为HTML。 我如何使用与@ControllerAdvice使用相同的技术来拦截它并呈现JSON结果。

security.xml文件

         

XBasicAuthenticationEntryPoint

 public class XBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); } } 

我可以通过使用BasicAuthenticationEntryPoint直接写入输出流来解决401,但我不确定这是最好的方法。

 public class XBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint { private final ObjectMapper om; @Autowired public XBasicAuthenticationEntryPoint(ObjectMapper om) { this.om = om; } @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); om.writeValue(httpResponse.getOutputStream(), new ApiError(request.getRequestURI(), HttpStatus.SC_UNAUTHORIZED, "You must sign in or provide basic authentication.")); } } 

我还没弄明白如何处理400,我曾经尝试过捕获所有可行的控制器,但似乎有时它会与其他控制器产生奇怪的冲突行为,我不想重新审视。

我的ControllerAdvice实现有一个捕获所有如果spring抛出任何exception的错误请求(400)它理论上应该捕获它,但它不会:

 @ControllerAdvice(annotations = {RestController.class}) public class ApiControllerAdvisor { @ExceptionHandler(Throwable.class) public ResponseEntity exception(Throwable exception, WebRequest request, HttpServletRequest req) { ApiError err = new ApiError(req.getRequestURI(), exception); return new ResponseEntity(err, err.getStatus()); } } 

几周前我发现自己问了同样的问题 – 正如Dirk在评论中指出的那样,@ ControllerAdvice只会在控制器方法中抛出exception时启动,所以不可避免地会抓住所有东西(我实际上是试图解决404错误的JSON响应的情况)。

我确定的解决方案,虽然不是很愉快(希望如果你得到一个更好的答案,我将改变我的方法)是在Web.xml中处理错误映射 – 我添加了以下内容,它将覆盖特定于tomcat的默认错误页面URL映射:

  404 /errors/resourcenotfound   403 /errors/unauthorised   401 /errors/unauthorised  

现在,如果任何页面返回404,它由我的错误控制器处理,如下所示:

 @Controller @RequestMapping("/errors") public class ApplicationExceptionHandler { @ResponseStatus(HttpStatus.NOT_FOUND) @RequestMapping("resourcenotfound") @ResponseBody public JsonResponse resourceNotFound(HttpServletRequest request, Device device) throws Exception { return new JsonResponse("ERROR", 404, "Resource Not Found"); } @ResponseStatus(HttpStatus.UNAUTHORIZED) @RequestMapping("unauthorised") @ResponseBody public JsonResponse unAuthorised(HttpServletRequest request, Device device) throws Exception { return new JsonResponse("ERROR", 401, "Unauthorised Request"); } } 

这一切仍然感觉非常严峻 – 当然是全局处理错误 – 所以如果你不总是希望404响应是json(如果你正在服务同一个应用程序的普通webapp)那么它不能很好地工作。 但就像我说的那样,我决定采取行动,这里希望有一个更好的方式!

你应该注意在ResponseEntity中设置错误代码,如下所示:

 new ResponseEntity(err, HttpStatus.UNAUTHORIZED); 

我通过实现自己的HandlerExceptionResolver版本和子类化DefaultHandlerExceptionResolver解决了这个问题。 花了一些时间来解决这个问题,你必须覆盖大多数方法,尽管以下内容完全符合我的要求。

首先,基础是创建一个实现。

 public class CustomHandlerExceptionResolver extends DefaultHandlerExceptionResolver { private final ObjectMapper om; @Autowired public CustomHandlerExceptionResolver(ObjectMapper om) { this.om = om; setOrder(HIGHEST_PRECEDENCE); } @Override protected boolean shouldApplyTo(HttpServletRequest request, Object handler) { return request.getServletPath().startsWith("/api"); } } 

现在在servlet上下文中注册它。

这现在没有什么不同,但是将是第一个要尝试的HandlerExceptionResolver ,它将匹配以/api的上下文路径开头的任何请求(注意:您可以将其设置为可配置参数)。

接下来,我们现在可以覆盖spring遇到错误的任何方法,我的数量有15个。

我发现我可以直接写入响应并返回一个空的ModelAndView对象,如果我的方法最终没有处理导致下一个exception解析器被尝试的错误,则返回null。

作为处理发生请求绑定错误的情况的示例,我已完成以下操作:

 @Override protected ModelAndView handleServletRequestBindingException(ServletRequestBindingException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { ApiError ae = new ApiError(request.getRequestURI(), ex, HttpServletResponse.SC_BAD_REQUEST); response.setStatus(ae.getStatusCode()); om.writeValue(response.getOutputStream(), ae); return new ModelAndView(); } 

这里唯一的缺点是,因为我自己写入流我没有使用任何消息转换器来处理写响应,所以如果我想同时支持XML和JSON API,那就不可能了,幸运的是我只对支持JSON感兴趣,但是可以增强相同的技术来使用视图解析器来确定要呈现的内容等。

如果有人有更好的方法,我仍然有兴趣知道如何处理这个问题。

只需在控制器中捕获exception并返回要发送的响应即可。

  try { // Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password); token.setDetails(new WebAuthenticationDetails(request)); Authentication authentication = this.authenticationProvider.authenticate(token); logger.debug("Logging in with [{}]", authentication.getPrincipal()); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception e) { SecurityContextHolder.getContext().setAuthentication(null); logger.error("Failure in autoLogin", e); }