Подтвердить что ты не робот

Как я могу получить Spring -Security, чтобы вернуть ответ 401 в формате JSON?

У меня есть API ReST для приложения со всеми контроллерами, найденными в /api/, все они возвращают ответы JSON с помощью @ControllerAdvice, который обрабатывает все исключения для сопоставления с форматированными результатами JSON.

Это отлично работает с spring 4.0 @ControllerAdvice теперь поддерживает сопоставление по аннотации. Я не могу решить, как вернуть результат JSON для ответов 401 - Unauthenticated и 400 - Bad Request.

Вместо этого spring просто возвращает ответ на контейнер (tomcat), который отображает это как HTML. Как я могу перехватить это и отобразить результат JSON, используя тот же метод, что и мой @ControllerAdvice.

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

Я могу решить 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, хотя однажды я попробовал поймать все контроллеры, которые действительно работали, но казалось, что иногда это будет иметь странное противоречивое поведение с другими контроллерами, которые я не хочу пересматривать.

В моей реализации ControllerAdvice есть catch, который, если 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());
    }
}
4b9b3361

Ответ 1

Я действительно обнаружил, что задаю один и тот же вопрос несколько недель назад. Как заметил Дирк в комментариях, @ControllerAdvice только ударит, если исключение выбрано из метода контроллера, поэтому неизбежно не поймать все вещи ( Я на самом деле пытался решить случай для ответа 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 (если вы служили обычным webapp того же приложения), то это не делает работайте так хорошо. Но, как я уже сказал, это то, на что я остановился, чтобы двигаться, здесь надеясь, что есть более хороший способ!

Ответ 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");
    }
}

И теперь зарегистрируйте его в контексте сервлета.

Теперь это ничего не изменит, но будет первым HandlerExceptionResolver, который будет проверяться и будет соответствовать любому запросу, начинающемуся с пути контекста /api (обратите внимание: вы можете сделать это настраиваемым параметром).

Затем мы теперь можем переопределить любой метод, с которым spring встречается с ошибками, на мой счет 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, но эта же техника может быть улучшена, чтобы использовать разрешающие средства для определения того, что делать в и т.д.

Если у кого-то есть лучший подход, мне все равно будет интересно узнать, как с этим справиться.

Ответ 3

Вы должны посмотреть, как установить код ошибки в ResponseEntity, как показано ниже:

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

Ответ 4

Просто поймайте исключение в своем контроллере и ответьте на ответ, который вы хотите отправить.

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