Skip to content

Latest commit

 

History

History
264 lines (196 loc) · 9.78 KB

11--跨域.md

File metadata and controls

264 lines (196 loc) · 9.78 KB

源码:https://github.com/langyastudio/langya-tech/tree/springboot/cors

说到跨域访问,必须先解释一个名词:同源策略。所谓同源策略就是在浏览器端出于安全考量,向服务端发起请求必须满足:协议相同、Host相同、端口相同的条件,否则访问将被禁止,该访问也就被称为跨域访问

虽然跨域访问被禁止之后,可以在一定程度上提高了应用的安全性,但也为开发带来了一定的麻烦。比如:我们开发一个前后端分离的易用,页面及 js 部署在一个主机的 nginx 服务中,后端接口部署在一个 tomcat 应用容器中,当前端向后端发起请求的时候一定是不符合同源策略的,也就无法访问。

解决方案

来自网络,找不到原文了

目前比较常用的是:corsjsonp

前端

虽然浏览器对于不符合同源策略的访问是禁止的,但是仍然存在例外的情况,如以下资源引用的标签不受同源策略的限制:

  • html 的 script 标签
  • html 的 link 标签
  • html 的 img 标签
  • html 的 iframe 标签

除了基于 HTML 本身的特性实现跨域访问,我们还可以使用如下方式实现跨域访问:

  • jsonp
  • postMessage
  • flash request

CORS

CORS — 跨域资源共享,通过修改 Http 协议 header 的方式实现跨域。说的简单点就是通过设置 HTTP 的响应头信息,告知浏览器哪些情况在不符合同源策略的条件下也可以跨域访问,浏览器通过解析 Http 协议中的 Header 执行具体判断。具体的 Header 如下:

  • Access-Control-Allow-Origin

    允许哪些 ip 或 域名可以跨域访问

  • Access-Control-Max-Age

    表示在多少秒之内不需要重复校验该请求的跨域访问权限

  • Access-Control-Allow-Methods

    表示允许跨域请求的 HTTP 方法,如:GET,POST,PUT,DELETE

  • Access-Control-Allow-Headers

    表示访问请求中允许携带哪些 Header 信息,如:AcceptAccept-LanguageContent-LanguageContent-Type

代理

实际上对跨域访问的支持在服务端实现起来更加容易,最常用的方法就是通过代理的方式,如:nginx 或 haproxy 代理跨域。

其实实现代理跨域的逻辑非常简单:就是在不同的资源服务:js 资源、html 资源、css 资源、接口数据资源服务的前端搭建一个中间层,所有的浏览器及客户端访问都通过代理转发。所以在浏览器、客户端看来,它们访问的都是同一个 ip、同一个端口的资源,从而符合同源策略实现跨域访问。 img

实战

cors 跨域

cors 跨域对于 getpost等都支持。

可以基于 Filter 实现,全局有效。下图的虚线框就是 Filter2 的拦截范围,这里关于Filter不做详细介绍:

         │   ▲
         ▼   │
       ┌───────┐
       │Filter1│
       └───────┘
         │   ▲
         ▼   │
       ┌───────┐
┌ ─ ─ ─│Filter2│─ ─ ─ ─ ─ ─ ─ ─ ┐
       └───────┘
│        │   ▲                  │
         ▼   │
│ ┌─────────────────┐           │
  │DispatcherServlet│<───┐
│ └─────────────────┘    │      │
   │              ┌────────────┐
│  │              │ModelAndView││
   │              └────────────┘
│  │                     ▲      │
   │    ┌───────────┐    │
│  ├───>│Controller1│────┤      │
   │    └───────────┘    │
│  │                     │      │
   │    ┌───────────┐    │
│  └───>│Controller2│────┘      │
        └───────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘

直接继承 FilterRegistrationBean<Filter> 可以快速的定义 filter 并自动生效,如下:

@Order(1)
@Component
public class CorsFilterBean extends FilterRegistrationBean<Filter>
{
    @PostConstruct
    public void init()
    {
        setFilter(new CorsFilter());
        
        //设置复合条件的URL地址
        setUrlPatterns(List.of("/admin/*", "/portal/*"));
    }

    class CorsFilter implements Filter
    {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException
        {
            HttpServletRequest  req  = (HttpServletRequest) request;
            HttpServletResponse resp = (HttpServletResponse) response;

            // 跨域
            String uiDomain = null;
            String origin   = req.getHeader("origin");
            if (StrUtil.isNotBlank(origin))
            {
                uiDomain = origin;
            }

            if (StrUtil.isNotBlank(uiDomain))
            {
                String method = req.getMethod();

                if ("OPTIONS".equals(method) || "POST".equals(method))
                {
                    resp.addHeader("Access-Control-Allow-Origin", uiDomain);
                    resp.addHeader("Access-Control-Allow-Headers", "access-control-allow-origin," +
                            "Authorization," + "school_id," +
                            "token,Content-Type,Cookie,Origin, Referer,If-Match, If-Modified-Since, If-None-Match," +
                            "If-Unmodified-Since, X-Requested-With,X_Requested_With");
                    resp.addHeader("Access-Control-Allow-Method", "GET, POST");
                    resp.addHeader("Access-Control-Allow-Credentials", "true");

                    // 在这个时间范围内,所有同类型的请求都将不再发送预检请求而是直接使用此次返回的头作为判断依据
                    resp.addHeader("Access-Control-Max-Age", "1440");
                }

                //P3P
                //跨域向IE写入cookie
                resp.addHeader("P3P", "CP=\"CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI " +
                        "DSP COR\"");

                // OPTIONS请求
                if ("OPTIONS".equals(method))
                {
                    //204 empty
                    resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
                    return;
                }
            }
            resp.addHeader("Keep-Alive", "timeout=5, max=60");

            chain.doFilter(request, response);
        }
    }
}

还可以使用如下方式实现 cors 跨域:

  • 重写 WebMvcConfigurer 的 addCorsMappings 方法

  • 使用 CrossOrigin 注解

jsonp 跨域

jsonp 是一个非官方的协议,它允许在服务器端集成 Script tags 返回至客户端,通过 Javascript callback 的形式实现跨域访问 — jsonp 只适合 GET 类型的请求

使用 @ControllerAdvice 注解 + 继承 ResponseBodyAdvice ,即可统一处理返回值的方式完成 jsonp 的实现,如下:

@ControllerAdvice
public class JsonpResponseAdvice extends FastJsonHttpMessageConverter implements ResponseBodyAdvice
{
    private static final Pattern CALLBACK_PARAM_PATTERN = Pattern.compile("[0-9A-Za-z_\\.]*");
    public static final  Charset UTF8                   = StandardCharsets.UTF_8;

    private final Charset             charset;
    private final SerializerFeature[] features;

    @Override
    public Object beforeBodyWrite(Object obj,
                                  @NotNull MethodParameter methodParameter,
                                  @NotNull MediaType mediaType,
                                  @NotNull Class aClass,
                                  @NotNull ServerHttpRequest serverHttpRequest,
                                  @NotNull ServerHttpResponse serverHttpResponse)
    {

        HttpServletRequest  servletRequest = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();
        HttpServletResponse response       = ((ServletServerHttpResponse) serverHttpResponse).getServletResponse();
        String              functionName   = servletRequest.getParameter("callback");

        if (functionName != null)
        {
            if (this.isValidJsonpQueryParam(functionName))
            {
                String text      = JSON.toJSONString(obj, this.features);
                String jsonpText = functionName + "(" + text + ")";

                byte[]       bytes = jsonpText.getBytes(this.charset);
                OutputStream out   = null;
                try
                {
                    //jsonp content
                    response.setContentType("application/javascript; charset=utf-8");

                    out = response.getOutputStream();
                    out.write(bytes);
                    out.flush();
                    out.close();
                }
                catch (IOException e)
                {

                }
            }
        }

        return obj;
    }


    @Override
    public boolean supports(@NotNull MethodParameter methodParameter,
                            @NotNull Class aClass)
    {
        return true;
    }

    protected boolean isValidJsonpQueryParam(String value)
    {
        return CALLBACK_PARAM_PATTERN.matcher(value).matches();
    }

    public JsonpResponseAdvice()
    {
        super();
        this.charset = UTF8;
        this.features = new SerializerFeature[0];
    }
}

JSONP works by creating a <script> tag that executes Javascript from a different domain;it is not possible to send a POST request using a <script> tag.