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

2022-09-01 16:11:08

我有一个ReST API到一个应用程序,其中所有控制器都在下找到,这些都返回JSON响应,该响应处理所有异常以映射到JSON格式的结果。/api/@ControllerAdvice

从春季4.0开始,这非常有效,现在支持在注释上进行匹配。我无法弄清楚的是如何返回401 - 未经身份验证和400 - 错误请求响应的JSON结果。@ControllerAdvice

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

安全性.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());
    }
}

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

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,我曾经尝试过一个捕获所有控制器,它确实有效,但似乎有时它会与其他控制器有奇怪的冲突行为,我不想重新审视。

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

@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

实际上,几周前我发现自己问了同样的问题 - 正如Dirk在评论中指出的那样,@ControllerAdvice只有在控制器方法中抛出异常时才会启动,因此不可避免地不会捕获所有事情(我实际上试图解决404错误的JSON响应的情况)。

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

<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应用程序提供服务),那么它就不能很好地工作。但就像我说的,这是我为了动起来而决定的,希望有更好的方式!


答案 2

我通过实现我自己的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感兴趣,但同样的技术可以增强,以使用视图解析器来确定要呈现的内容等。

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


推荐