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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/22783583/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 17:51:52  来源:igfitidea点击:

How can I get Spring-Security to return a 401 response as a JSON format?

javaspring-mvcspring-security

提问by Brett Ryan

I have a ReST API to an application with all controllers found under /api/, these all return JSON responses with a @ControllerAdvicewhich handles all exceptions to map to JSON formatted results.

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

This works great as of spring 4.0 @ControllerAdvicenow supports matching on annotations. What I can't work out is how to return a JSON result for a 401 - Unauthenticated and 400 - Bad Request responses.

从 spring 4.0 开始,这很有效,@ControllerAdvice现在支持注释匹配。我无法解决的是如何为 401 - Unauthenticated 和 400 - Bad Request 响应返回 JSON 结果。

Instead Spring is simply returning the response to the container (tomcat) which renders this as HTML. How can I intercept this and render a JSON result using the same technique that my @ControllerAdviceis using.

相反,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

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

I can solve 401 by using the BasicAuthenticationEntryPointto write directly to the output stream, but I'm not sure it's the best approach.

我可以通过使用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."));
    }

}

I am yet to figure out how to handle 400 though, I once tried a catch all controller which did work, but it seemed that sometimes it would have odd conflicting behaviour with other controllers that I don't want to revisit.

我还没有弄清楚如何处理 400,我曾经尝试过一个有效的 catch all 控制器,但似乎有时它会与我不想重新访问的其他控制器有奇怪的冲突行为。

My ControllerAdviceimplementation has a catch all which if spring throws any exception for bad request (400) it should in theory capture it, but it does not:

我的ControllerAdvice实现有一个 catch all,如果 spring 为错误的请求(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());
    }
}

回答by Shishir Kumar

You should look out to set error code in ResponseEntity, as below:

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

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

回答by rhinds

I actually found myself asking the very same question a few weeks ago - As Dirk pointed out in the comments, @ControllerAdvice will only kick in if an exception is thrown from within a controller method, so will inevitably not catch all things (I was actually trying to solve the case for a JSON response for a 404 error).

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

The solution I settled on, although not entirely pleasant (hopefully if you get a better answer I will change my approach) is handling the error mapping in the Web.xml - I added the following, which will override the tomcat default error pages with specific URL mappings:

我确定的解决方案,虽然并不完全令人愉快(希望如果你得到更好的答案,我会改变我的方法)正在处理 Web.xml 中的错误映射 - 我添加了以下内容,它将用特定的内容覆盖 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>

Now, if any page returns a 404, it is handled by my error controller something like this:

现在,如果任何页面返回 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");
    }
}

It all still feels pretty grim - and of course is a global handling of errors - so if you didn't always want a 404 response to be json (if you were serving a normal webapp of the same application) then it doesnt work so well. But like I said, its what I settled on to get moving, here's hoping there is a nicer way!

这一切仍然感觉很糟糕 - 当然是对错误的全局处理 - 所以如果你并不总是希望 404 响应是 json(如果你正在为同一应用程序的普通 webapp 提供服务)那么它就不会那么好用. 但就像我说的,这是我决定搬家的方式,希望有更好的方法!

回答by Brett Ryan

I have solved this by implementing my own version of HandlerExceptionResolverand subclassing DefaultHandlerExceptionResolver. It took a bit to work this out and you must override most methods, though the following has done exactly what I'm after.

我已经通过实现我自己的HandlerExceptionResolver版本和子类化DefaultHandlerExceptionResolver解决了这个问题。解决这个问题需要一些时间,您必须覆盖大多数方法,尽管以下内容正是我所追求的。

Firstly the basics are to create an implementation.

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

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

And now register it in your servlet context.

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

This now does nothing different, but will be the first HandlerExceptionResolverto be tried and will match any request starting with a context path of /api(note: you could make this a configurable parameter).

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

Next, we now can override any method that spring encounters errors on, there are 15 on my count.

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

What I have found is that I can write to the response directly and return an empty ModelAndViewobject or return null if my method did not eventually handle the fault which causes the next exception resolver to be tried.

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

As an example to handle situations where a request binding fault occurred I have done the following:

作为处理发生请求绑定错误的情况的示例,我执行了以下操作:

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

The only disadvantage here is that because I'm writing to the stream myself I'm not using any message converters to handle writing the response, so if I wanted to support XML and JSON API's at the same time it would not be possible, fortunately I'm only interested in supporting JSON, but this same technique could be enhanced to use view resolvers to determine what to render in etc.

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

If anyone has a better approach I'd still be interested in knowing how to deal with this.

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

回答by LNT

Simply catch the exception in your controller and return response you want to send.

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

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