Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring to distinguish browser visitors from API calls to the endpoints

In my Spring boot app, I have bunch of endpoints at /api/**. The following is my App configuration:

@Configuration
public class AppConfig extends WebMvcConfigurerAdapter {

    private class PushStateResourceResolver implements ResourceResolver {
        private Resource index = new ClassPathResource("/public/index.html");
        private List<String> handledExtensions = Arrays.asList("html", "js",
                "json", "csv", "css", "png", "svg", "eot", "ttf", "woff",
                "appcache", "jpg", "jpeg", "gif", "ico");

        private List<String> ignoredPaths = Arrays.asList("^api\\/.*$");

        @Override
        public Resource resolveResource(HttpServletRequest request,
                String requestPath, List<? extends Resource> locations,
                ResourceResolverChain chain) {
            return resolve(requestPath, locations);
        }

        @Override
        public String resolveUrlPath(String resourcePath,
                List<? extends Resource> locations, ResourceResolverChain chain) {
            Resource resolvedResource = resolve(resourcePath, locations);
            if (resolvedResource == null) {
                return null;
            }
            try {
                return resolvedResource.getURL().toString();
            } catch (IOException e) {
                return resolvedResource.getFilename();
            }
        }

        private Resource resolve(String requestPath,
                List<? extends Resource> locations) {
            if (isIgnored(requestPath)) {
                return null;
            }
            if (isHandled(requestPath)) {
                return locations
                        .stream()
                        .map(loc -> createRelative(loc, requestPath))
                        .filter(resource -> resource != null
                                && resource.exists()).findFirst()
                        .orElseGet(null);
            }
            return index;
        }

        private Resource createRelative(Resource resource, String relativePath) {
            try {
                return resource.createRelative(relativePath);
            } catch (IOException e) {
                return null;
            }
        }

        private boolean isIgnored(String path) {
            return false;
            //          return !ignoredPaths.stream().noneMatch(rgx -> Pattern.matches(rgx, path));
            //deliberately made this change for examining the code
        }

        private boolean isHandled(String path) {
            String extension = StringUtils.getFilenameExtension(path);
            return handledExtensions.stream().anyMatch(
                    ext -> ext.equals(extension));
        }
    }
}

The access to the endpoints behind /api/** is checked to be authenticated, therefore when I type in /api/my_endpoint in the browser, I get 401 error back, which is not what I want. I want users to be served with index.html.

like image 412
Arian Avatar asked Dec 10 '17 01:12

Arian


People also ask

How do you expose API endpoints in spring boot?

In a Spring Boot application, we expose a REST API endpoint by using the @RequestMapping annotation in the controller class. For getting these endpoints, there are three options: an event listener, Spring Boot Actuator, or the Swagger library.

What is @API annotation in spring boot?

The @API annotations as per the documentation states “The annotation @Api is used to configure the whole API, and apply to all public methods of a class unless overridden by @APIMethod”. Note the words unless overridden. Often you find that you casually go ahead and mark a class with @API.

What is the use of @RestController in spring boot?

Spring RestController annotation is used to create RESTful web services using Spring MVC. Spring RestController takes care of mapping request data to the defined request handler method. Once response body is generated from the handler method, it converts it to JSON or XML response.


1 Answers

You can check for the X-Requested-With header:

private boolean isAjax(HttpServletRequest request) {
    String requestedWithHeader = request.getHeader("X-Requested-With");
    return "XMLHttpRequest".equals(requestedWithHeader);
}

UPDATE: Maybe it's a better approach to check for the Accept header. I think the probability is much higher that browsers include a Accept: text/html header than scripts etc. include a X-Requested-With header.

You could create a custom authentication entry point and redirect the user if the Accept: text/html header is present:

public class CustomEntryPoint implements AuthenticationEntryPoint {

    private static final String ACCEPT_HEADER = "Accept";

    private final RedirectStrategy redirect = new DefaultRedirectStrategy();

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        if (isHtmlRequest(request)) {
            redirect.sendRedirect(request, response, "/");
        } else {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized access is not allowed");
        }
    }

    private boolean isHtmlRequest(HttpServletRequest request) {
        String acceptHeader = request.getHeader(ACCEPT_HEADER);
        List<MediaType> acceptedMediaTypes = MediaType.parseMediaTypes(acceptHeader);
        return acceptedMediaTypes.contains(MediaType.TEXT_HTML);
    }

}

Note:

If you use a custom authentication filter (inherited from AbstractAuthenticationProcessingFilter) then the authentication entry point won't be called. You can handle the redirect in the unsuccessfulAuthentication() method of AbstractAuthenticationProcessingFilter.

Alternatives:

  • Override standard BasicErrorController of Spring Boot and handle redirect of 401 Unauthorized errors there.
  • Why not just return JSON on all /api calls and html otherwise?
like image 169
Christian Avatar answered Sep 28 '22 02:09

Christian