Spring将浏览器访问者与API调用区分为端点

时间:2017-12-10 01:55:13

标签: java spring spring-boot

在我的Spring启动应用程序中,我在/api/**处有许多端点。以下是我的App配置:

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

检查/api/**后面的端点的访问权限是否经过身份验证,因此当我在浏览器中输入/api/my_endpoint时,我得到 401 错误,这不是我想要的是。我希望用户可以使用index.html

3 个答案:

答案 0 :(得分:2)

您可以检查X-Requested-With标题:

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

更新:也许这是检查Accept标头的更好方法。我认为浏览器包含Accept: text/html标题而非脚本等的概率要高得多,包括X-Requested-With标题。

如果Accept: text/html标题存在,您可以创建自定义身份验证入口点并重定向用户:

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

}

注意:

如果使用自定义身份验证筛选器(继承自AbstractAuthenticationProcessingFilter),则不会调用身份验证入口点。您可以使用unsuccessfulAuthentication()的{​​{1}}方法处理重定向。

<强>备选方案:

  • 覆盖Spring Boot的标准AbstractAuthenticationProcessingFilter并处理BasicErrorController错误的重定向。
  • 为什么不在所有401 Unauthorized调用上返回JSON,否则返回html?

答案 1 :(得分:2)

所以,我最终通过修复我的安全配置解决了这个问题:

我有一个自定义JWTAuthenticationFilter,其中我覆盖了unsuccessfulAuthentication方法:

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
    logger.debug("failed authentication while attempting to access "+ URL_PATH_HELPER.getPathWithinApplication((HttpServletRequest) request));
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    response.sendRedirect("/");
}

如您所见,如果身份验证失败,我会将用户重定向到&#34; /&#34;作为回报,将由资源解析器捕获,index.html将被提供!

答案 2 :(得分:1)

浏览器通常设置&#34; User-Agent&#34; http请求的标头。

因此,您可以使用以下命令区分这些调用:request.getHeader(&#34; User-Agent&#34;);

另见: