Comment faire pour que Spring-Security renvoie une réponse 401 en format JSON?

j'ai une API ReST pour une application avec tous les controllers trouvés sous /api/, ceux-ci renvoient tous des réponses JSON avec un @ControllerAdvice qui gère toutes les exceptions à la mise en correspondance des résultats au format JSON.

cela fonctionne très bien à partir du printemps 4.0 @ControllerAdvice supporte maintenant la correspondance sur les annotations. Ce que je ne peux pas trouver, c'est comment retourner un résultat JSON pour une réponse 401 - non authentifiée et 400 - mauvaise requête.

au lieu de cela, le ressort renvoie simplement la réponse au conteneur (tomcat) qui le rend HTML. Comment puis-je intercepter ceci et rendre un résultat JSON en utilisant la même technique que mon @ControllerAdvice.

sécurité.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());
    }
}

je peux résoudre 401 en utilisant le BasicAuthenticationEntryPoint pour écrire directement au flux de sortie, mais je ne suis pas sûr que ce soit la meilleure approche.

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

}

je suis encore à comprendre comment gérer les 400, cependant, je l' une fois, j'ai essayé un contrôleur catch all qui fonctionnait, mais il semblait que parfois il y avait un comportement étrange et conflictuel avec d'autres contrôleurs que je ne voulais pas revoir.

Mon ControllerAdvice mise en place a un fourre-tout qui si le printemps lance une exception pour la bad request (400), il devrait, en théorie, le capturer, mais il ne fonctionne pas:

@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());
    }
}
23
demandé sur Brett Ryan 2014-04-01 14:46:11

4 réponses

la solution que j'ai choisie, bien que pas tout à fait agréable (j'espère que si vous obtenez une meilleure réponse, je vais changer mon approche) est de gérer l'erreur cartographie du Web.xml-j'ai ajouté ce qui suit, qui va remplacer les pages d'erreur tomcat par défaut avec des mappages D'URL spécifiques:

<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>

maintenant, si une page renvoie un 404, Il est géré par mon contrôleur d'erreur quelque chose comme ceci:

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

tout semble encore assez sombre - et bien sûr est un traitement global des erreurs - donc si vous ne vouliez pas toujours une réponse 404 pour être json (si vous serviez une webapp normale de la même application) alors il ne fonctionne pas si bien. Mais comme je l'ai dit, c'est ce que j'ai décidé d'aller de l'avant, en espérant qu'il y ait un meilleur moyen!

16
répondu rhinds 2014-04-01 12:11:22

Vous devriez regarder dehors pour mettre le code d'erreur dans ResponseEntity, comme ci-dessous:

new ResponseEntity<String>(err, HttpStatus.UNAUTHORIZED);
3
répondu Shishir Kumar 2014-04-01 11:13:36

j'ai résolu cela en implémentant ma propre version de HandlerExceptionResolver et sous-classement DefaultHandlerExceptionResolver. Il a fallu un peu pour résoudre ce problème et vous devez outrepasser la plupart des méthodes, bien que ce qui suit a fait exactement ce que je suis après.

tout d'abord, l'essentiel est de créer une implémentation.

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

et maintenant enregistrez-le dans le contexte de votre servlet.

cela ne fait maintenant rien de différent, mais qui sera le premier HandlerExceptionResolver pour être jugé et correspondent à toute demande de départ avec un chemin de contexte de /api (note: vous pouvez configurer ce paramètre).

ensuite, nous pouvons maintenant remplacer n'importe quelle méthode qui rencontre des erreurs de spring sur, il y en a 15 sur mon compte.

ce que j'ai trouvé c'est que je peux écrire directement à la réponse et retourner un empty ModelAndView objet ou de retourner la valeur null si ma méthode n'a pas finalement gérer la faille qui provoque l'essai du résolveur d'exception suivant.

à titre d'exemple pour traiter les situations où un défaut de reliure de requête est survenu, j'ai fait ce qui suit:

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

le seul inconvénient ici est que parce que j'écris au flux moi-même, je n'utilise pas de convertisseurs de messages pour gérer l'écriture de la réponse, donc si je voulais prendre en charge XML et les API JSON en même temps, ce ne serait pas possible, heureusement, je suis seulement intéressé à soutenir JSON, mais cette même technique pourrait être améliorée pour utiliser des résolveurs de vue pour déterminer ce qu'il faut rendre dans etc.

si quelqu'un a une meilleure approche, je serais toujours intéressé à savoir comment faire face à cela.

3
répondu Brett Ryan 2014-04-01 13:20:42

Simplement attraper l'exception dans votre contrôleur et de retourner la réponse que vous souhaitez envoyer.

   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);
}
0
répondu LNT 2017-11-27 05:50:33