J'ai une API ReST vers une application avec tous les contrôleurs trouvés sous /api/
; ceux-ci renvoient tous des réponses JSON avec un @ControllerAdvice
qui gère toutes les exceptions à associer aux résultats formatés JSON.
Cela fonctionne très bien à partir du printemps 4.0 @ControllerAdvice
prend désormais en charge l'appariement sur les annotations. Ce que je ne peux pas résoudre, c'est comment renvoyer un résultat JSON pour une réponse 401 - Unauthenticated et 400 - Bad Request.
Au lieu de cela, Spring renvoie simplement la réponse au conteneur (Tomcat), qui la restitue au format HTML. Comment puis-je intercepter ceci et restituer un résultat JSON en utilisant la même technique que celle utilisée par mon @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());
}
}
Je peux résoudre 401 en utilisant la variable BasicAuthenticationEntryPoint
pour écrire directement dans le 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 n'ai pas encore compris comment gérer 400, j'ai déjà essayé un contrôleur catch all qui fonctionnait, mais il semblait que parfois il pouvait avoir un comportement étrange en conflit avec d'autres contrôleurs que je ne voulais pas revisiter.
Ma mise en oeuvre ControllerAdvice
a un problème qui, si printemps déclenche une exception pour requête incorrecte (400), devrait normalement la capturer, mais ce n'est pas le cas:
@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());
}
}
En fait, je me suis posé la même question il y a quelques semaines. Comme Dirk l'a souligné dans les commentaires, @ControllerAdvice n'interviendra que si une exception est renvoyée depuis une méthode de contrôleur. essayant de résoudre le cas d’une réponse JSON pour une erreur 404).
La solution sur laquelle j’ai opté, bien que pas tout à fait agréable (espérons que si vous obtenez une meilleure réponse, je changerai d’approche) consiste à gérer le mappage des erreurs dans le fichier Web.xml - j’ai ajouté ce qui suit, qui remplacera les pages d’erreur de Tomcat avec des Mappages d'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>
Maintenant, si une page renvoie un 404, elle est gérée par mon contrôleur d’erreur, 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 sinistre - et bien sûr, il s’agit d’un traitement global des erreurs - donc si vous ne voulez pas toujours une réponse 404 au format json (si vous serviez une application Web normale de la même application), cela ne fonctionnerait pas aussi bien. . Mais comme je l'ai dit, c'est ce sur quoi j'ai décidé de bouger. En espérant qu'il existe un moyen plus agréable!
J'ai résolu ce problème en implémentant ma propre version de HandlerExceptionResolver et de la sous-classe DefaultHandlerExceptionResolver . Il a fallu un peu de temps pour résoudre ce problème et vous devez redéfinir la plupart des méthodes, même si ce qui suit a fait exactement ce que je recherche.
Premièrement, les bases sont 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 votre contexte de servlet.
Cela ne fait maintenant plus rien de différent, mais ce sera le premier HandlerExceptionResolver à essayer et correspondra à toute requête commençant par un chemin de contexte de /api
(remarque: vous pouvez en faire un paramètre configurable).
Ensuite, nous pouvons maintenant remplacer toute méthode sur laquelle Spring rencontre des erreurs, il y en a 15 sur mon compte.
Ce que j’ai trouvé, c’est que je peux écrire directement dans la réponse et renvoyer un objet ModelAndView ou null si la méthode ne gère pas l’erreur qui entraîne la tentative du résolveur d’exceptions suivant.
Comme exemple pour gérer les situations dans lesquelles une erreur de liaison de requête s'est produite, j'ai procédé comme 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 moi-même dans le flux, je n'utilise aucun convertisseur de message pour gérer l'écriture de la réponse. Par conséquent, si je voulais prendre en charge les API XML et JSON en même temps, ce ne serait pas possible, heureusement Je suis uniquement intéressé par la prise en charge de JSON, mais cette même technique pourrait être améliorée pour utiliser des résolveurs de vue afin de déterminer le rendu, etc.
Si quelqu'un a une meilleure approche, j'aimerais quand même savoir comment gérer cela.
Vous devez veiller à définir le code d'erreur dans ResponseEntity, comme ci-dessous:
new ResponseEntity<String>(err, HttpStatus.UNAUTHORIZED);
Attrapez simplement l'exception dans votre contrôleur et renvoyez 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);
}