2014-04-01 56 views
23

我有一个应用程序的ReST API,所有控制器在/api/下找到,这些都返回JSON响应与@ControllerAdvice处理所有异常映射到JSON格式的结果。如何让Spring-Security以JSON格式返回401响应?

此功能在春季4.0时效果很好@ControllerAdvice现在支持在注释上进行匹配。我无法解决的是如何为401返回JSON结果 - 未认证和400 - 错误请求响应。

取而代之的是,Spring只是简单地将容器(tomcat)的响应返回给HTML。我如何拦截这个并使用我的@ControllerAdvice正在使用的相同技术来呈现JSON结果。

的security.xml

<bean id="xBasicAuthenticationEntryPoint" 
     class="com.example.security.XBasicAuthenticationEntryPoint"> 
    <property name="realmName" value="com.example.unite"/> 
</bean> 
<security:http pattern="/api/**" 
       create-session="never" 
       use-expressions="true"> 
    <security:http-basic entry-point-ref="xBasicAuthenticationEntryPoint"/> 
    <security:session-management /> 
    <security:intercept-url pattern="/api/**" access="isAuthenticated()"/> 
</security:http> 

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实现有一个捕获所有这些仿佛春天抛出任何异常的不良请求(400),它在理论上应该抓住它,但它并不:

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

对于401你的解决方案似乎是合适的:如果它是一个未经验证的请求,它永远不会打你的控制器,所以建议是没有帮助的。对于400我认为这取决于。如果因为控制器中没有匹配的签名而被返回,我很不确定这个通知是否会启动(显然不是)。 –

+0

谢谢德克,只是不完全确定'400',它确实工作正常。对于'400',我已经测试了映射期望的请求参数的位置,如果省略了参数,我会得到普通的'HTML'响应。它实际上是有意义的思考它,因为控制器方法不会被调用,这是春季之前发现的故障,因此再次不会像401用户那样调用顾问。 –

回答

2

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

new ResponseEntity<String>(err, HttpStatus.UNAUTHORIZED); 
+0

你认为这应该做什么?我在正确返回的入口点设置此项。当直接写入流时,虽然我不确定它是否是最合适的方式或者是否存在更好的方法,但它也可以正常工作。 –

16

我居然发现自己问了同样的问题在几个星期前 - 正如德克在评论中指出,如果异常是从控制器中抛出的@ControllerAdvice只会踢方法,所以将inevi显然没有抓住所有的东西(我实际上正在试图解决一个JSON响应404错误的情况)。

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

<error-page> 
    <error-code>404</error-code> 
    <location>/errors/resourcenotfound</location> 
</error-page> 
<error-page> 
    <error-code>403</error-code> 
    <location>/errors/unauthorised</location> 
</error-page> 
<error-page> 
    <error-code>401</error-code> 
    <location>/errors/unauthorised</location> 
</error-page> 

现在,如果有任何页返回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(如果你正在提供同一个应用程序的普通web应用程序),那么它的效果并不好。但就像我说的那样,我决定采取行动,希望有一个更好的方法!

+0

我也在考虑这样做,我刚刚尝试创建[DefaultHandlerExceptionResolver]的新实现(http://docs.spring.io/spring/docs/4.0.x/javadoc-api/org/springframework /web/servlet/mvc/support/DefaultHandlerExceptionResolver.html)。我花了整晚的时间来解决这个问题,但我会尽快回复。 –

+0

实施我的方法后,我不确定实际上我更喜欢哪种方式。我只是稍微等一下,看看我的感受,然后才接受:) –

+0

我忘了这个问题,直到它碰到了。我刚刚阅读了我所谈论的内容,最后我仍然更喜欢我的解决方案,尽管很多人都投了赞成票,所以我仍然感到矛盾,并且宁愿接受别人的答案,而不是我自己的答案。我更喜欢我的方法的原因是它处理没有控制器映射的情况。有人可能只是访问您网站上的'/ unauthorised'并获取错误,我相信这不应该暴露。 –

3

我已经通过实施我自己的版本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上下文路径的任何要求(注:你可以把这个配置参数)。

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

我发现的是,我可以直接写入响应并返回一个空的ModelAndView对象,或者如果我的方法没有最终处理导致下一个异常解析器被尝试的错误,则返回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感兴趣,但是可以使用这种相同的技术来增强使用视图解析器确定要渲染什么等。

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

0

只需在您的控制器中捕获异常并返回您要发送的响应。

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