跨域访问和防盗链基本原理(二)

2015/10/18 · HTML5 ·
跨域,
防盗链

原文出处: 童燕群
(@童燕群)   

跨域访问和防盗链基本原理(一)

2015/10/18 · HTML5 ·
跨域,
防盗链

原文出处: 童燕群
(@童燕群)   

关于防盗链与跨域访问

最近用阿里云的时候发现一些防盗链与跨域访问的一些坑,填完坑之后稍微整理一下。

1.  我的实现防盗链的做法,也是参考该位前辈的文章。基本原理就是就是一句话:通过判断request请求头的refer是否来源于本站。(当然请求头是来自于客户端的,是可伪造的,暂不在本文讨论范围内)。

二、跨域访问基本原理

在上一篇,介绍了盗链的基本原理和防盗链的解决方案。这里更深入分析一下跨域访问。先看看跨域访问的相关原理:跨网站指令码。维基上面给出了跨站访问的危害性。从这里可以整理出跨站访问的定义:JS脚本在浏览器端发起的请求其他域(名)下的网站数据的HTTP请求。

这里要与referer区分开,referer是浏览器的行为,所有浏览器发出的请求都不会存在安全风险。而由网页加载的脚本发起请求则会不可控,甚至可以截获用户数据传输到其他站点。referer方式拉取其他网站的数据也是跨域,但是这个是由浏览器请求整个资源,资源请求到后,客户端的脚本并不能操纵这份数据,只能用来呈现。但是很多时候,我们都需要发起请求到其他站点动态获取数据,并将获取到底数据进行进一步的处理,这也就是跨域访问的需求。

 

现在从技术上有几个方案去解决这个问题。

一、什么是防盗链

网站资源都有域的概念,浏览器加载一个站点时,首先加载这个站点的首页,一般是index.html或者index.php等。页面加载,如果仅仅是加载一个index.html页面,那么该页面里面只有文本,最终浏览器只能呈现一个文本页面。丰富的多媒体信息无法在站点上面展现。

那么我们看到的各类元素丰富的网页是如何在浏览器端生成并呈现的?其实,index.html在被解析时,浏览器会识别页面源码中的img,script等标签,标签内部一般会有src属性,src属性一般是一个绝对的URL地址或者相对本域的地址。浏览器会识别各种情况,并最终得到该资源的唯一地址,加载该资源。具体的加载过程就是对该资源的URL发起一个获取数据的请求,也就是GET请求。各种丰富的资源组成整个页面,浏览器按照html语法指定的格式排列获取到各类资源,最终呈现一个完整的页面。因此一个网页是由很多次请求,获取众多资源形成的,整个浏览器在一次网页呈现中会有很多次GET请求获取各个标签下的src资源。

图片 1

上图是一篇本站的博客网页呈现过程中的抓包截图。可以看到,大量的加载css、js和图片类资源的get请求。

观察其中的请求目的地址,可以发现有两类,一个是本站的43.242段的IP地址,这是本站的空间地址,即向本站自身请求资源,一般来说这个是必须的,访问资源由自身托管。另外一类是访问182的网段拉取数据。这类数据不是托管站内的,是在其他站点的。浏览器在页面呈现的过程,拉取非本站的资源,这就称“盗链”。

准确的说,只有某些时候,这种跨站访问资源,才被称为盗链。假设B站点作为一个商业网站,有很多自主版权的图片,自身展示用于商业目的。而A站点,希望在自己的网站上面也展示这些图片,直接使用:

<img src=”;

1
<img src="http://b.com/photo.jpg"/>

这样,大量的客户端在访问A站点时,实际上消耗了B站点的流量,而A站点却从中达成商业目的。从而不劳而获。这样的A站点着实令B站点不快的。如何禁止此类问题呢?

HTTP协议和标准的浏览器对于解决这个问题提供便利,浏览器在加载非本站的资源时,会增加一个头域,头域名字固定为:

Referer:

1
Referer:

而在直接粘贴地址到浏览器地址栏访问时,请求的是本站的该url的页面,是不会有这个referer这个http头域的。使用Chrome浏览器的调试台,打开network标签可以看到每一个资源的加载过程,下面两个图分别是主页面和一个页面内资源的加载请求截图:

图片 2

图片 3

这个referer标签正是为了告诉请求响应者(被拉取资源的服务端),本次请求的引用页是谁,资源提供端可以分析这个引用者是否“友好”,是否允许其“引用”,对于不允许访问的引用者,可以不提供图片,这样访问者在页面上就只能看到一个图片无法加载的浏览器默认占位的警告图片,甚至服务端可以返回一个默认的提醒勿盗链的提示图片。

一般的站点或者静态资源托管站点都提供防盗链的设置,也就是让服务端识别指定的Referer,在服务端接收到请求时,通过匹配referer头域与配置,对于指定放行,对于其他referer视为盗链。

1 赞 1 收藏
评论

图片 4

防盗链

防盗链是利用浏览器Http请求头Referer,告诉服务器谁访问资源,由服务器作判断,如果符合一定规则则返回数据,否则返回403。

2.  首先我们去了解下什么是HTTP Referer。简言之,HTTP
Referer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。比如从我主页上链接到一个朋友那里,他的服务器就能够从HTTP
Referer中统计出每天有多少用户点击我主页上的链接访问他的网站。(注:该文所有用的站点均假设以 http://blog.csdn.net为例)

1、JSONP跨域访问

利用浏览器的Referer方式加载脚本到客户端的方式。以:

<script type=”text/javascript”
src=”;

1
<script type="text/javascript" src="http://api.com/jsexample.js"></script>

这种方式获取并加载其他站点的JS脚本是被允许的,加载过来的脚本中如果有定义的函数或者接口,可以在本地使用,这也是我们用得最多的脚本加载方式。但是这个加载到本地脚本是不能被修改和处理的,只能是引用。

而跨域访问需要正是访问远端抓取到的数据。那么能否反过来,本地写好一个数据处理函数,让请求服务端帮助完成调用过程?JS脚本允许这样。

<script type=”text/javascript”> var localHandler = function(data)
{
alert(‘我是本地函数,可以被跨域的remote.js文件调用,远程js带来的数据是:’

  • data.result); }; </script> <script type=”text/javascript”
    src=”;
1
2
3
4
5
6
7
<script type="text/javascript">
var localHandler = function(data)
{
    alert(‘我是本地函数,可以被跨域的remote.js文件调用,远程js带来的数据是:’ + data.result);
};
</script>
<script type="text/javascript" src="http://remoteserver.com/remote.js"></script>

远端的服务器上面定义的remote.js是这样的:

JavaScript

localHandler({“result”:”我是远程js带来的数据”});

1
localHandler({"result":"我是远程js带来的数据"});

上面首先在本地定义了一个函数localHandler,然后远端返回的JS的内容是调用这个函数,返回到浏览器端执行。同时在JS内容中将客户端需要的数据返回,这样数据就被传输到了浏览器端,浏览器端只需要修改处理方法即可。这里有一些限制:1、客户端脚本和服务端需要一些配合;2、调用的数据必须是json格式的,否则客户端脚本无法处理;3、只能给被引用的服务端网址发送get请求。

<script type=”text/javascript”> var localHandler = function(data)
{
alert(‘我是本地函数,可以被跨域的remote.js文件调用,远程js带来的数据是:’

  • data.result); }; </script> <script type=”text/javascript”
    src=”;
1
2
3
4
5
6
7
<script type="text/javascript">
var localHandler = function(data)
{
    alert(‘我是本地函数,可以被跨域的remote.js文件调用,远程js带来的数据是:’ + data.result);
};
</script>
<script type="text/javascript" src="http://remoteserver.com/remote.php?callBack=localHandler"></script>

服务端的PHP函数可能是这样的:

PHP

<?php $data = “…….”; $callback = $_GET[‘callback’]; echo
$callback.'(‘.json_encode($data).’)’; exit; ?>

1
2
3
4
5
6
7
8
<?php
 
$data = "…….";
$callback = $_GET[‘callback’];
echo $callback.'(‘.json_encode($data).’)’;
exit;
 
?>

这样即可根据客户端指定的回调拼装调用过程。

Flash player跨域访问

Flash
player访问指定资源之前,访问根URL下的crossdomain.xml,例如访问资源http://test.com/path/to/a.m3u8之前会访问http://test.com/crossdomain.xml,由Flash
player解析并判断是否可以进行跨域访问。

crossdomain.xml的范例

<?xml version="1.0"?>   
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
    <site-control permitted-cross-domain-policies="master-only"/>
    <allow-access-from domain="*.yy.com"/>
    <allow-access-from domain="*.yypm.com"/>
    <allow-access-from domain="*"/>
    <allow-http-request-headers-from domain="*.yy.com" headers="SOAPAction"/>
</cross-domain-policy>

假如我们要访问资源: 有两种情况:

2、CORS(Cross-origin resource sharing)跨域访问

上述的JSONP由于有诸多限制,已经无法满足各种灵活的跨域访问请求。现在浏览器支持一种新的跨域访问机制,基于服务端控制访问权限的方式。简而言之,浏览器不再一味禁止跨域访问,而是需要检查目的站点返回的消息的头域,要检查该响应是否允许当前站点访问。通过HTTP头域的方式来通知浏览器:

JavaScript

Response headers[edit] Access-Control-Allow-Origin
Access-Control-Allow-Credentials Access-Control-Expose-Headers
Access-Control-Max-Age Access-Control-Allow-Methods
Access-Control-Allow-Headers

1
2
3
4
5
6
7
Response headers[edit]
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Expose-Headers
Access-Control-Max-Age
Access-Control-Allow-Methods
Access-Control-Allow-Headers

服务端利用这几个HTTP头域通知浏览器该资源的访问权限信息。在访问资源前,浏览器会先发出OPTIONS请求,获取这些权限信息,并比对当前站点的脚本是否有权限,然后再将实际的脚本的数据请求发出。发现权限不允许,则不会发出请求。逻辑流程图为:

图片 5

浏览器也可以直接将GET请求发出,数据和权限同时到达浏览器端,但是数据是否交给脚本处理需要浏览器检查权限对比后作出决定。

一次具体的跨域访问的流程为:

图片 6

因此权限控制交给了服务端,服务端一般也会提供对资源的CORS的配置。

跨域访问还有其他几种方式:本站服务端代理、跨子域时使用修改域标识等方法,但是应用场景的限制更多。目前绝大多数的跨域访问都由JSONP和CORS这两类方式组成。

1 赞 1 收藏
评论

图片 4

浏览器跨域访问

具体参照http://www.ruanyifeng.com/blog/2016/04/cors.html

1.  我们直接在浏览器上输入该网址。那么该请求的HTTP Referer 就为null

Flash player与OSS的跨域访问

如果需要Flash player跨域访问OSS里面的视频资源,需要设置:

  1. 编写crossdomain.xml,放在bucket的根目录下
  2. 将域名添加到防盗链配置中(如果防盗链配置为空,则忽略)
  3. 将域名规则添加到跨域(Cors)配置规则中(如果规则列表为空,则忽略)

2.  如果我们在其他其他页面中,通过点击,如 ) 上有一个  这样的链接,那么该请求的HTTP
Referer 就为)

OSS与CDN的防盗链

OSS和CDN的防盗链配置是分离的。配置可以分为下面几种情况:

  1. 只配置OSS
    安全性一般,可能会通过CDN的域名扫描到资源,而且会因CDN的缓存配置导致有时候200有时候403的情况。
  2. 只配置CDN
    安全性一般,可能会通过OSS的域名扫描到资源。
  3. OSS和CDN都配置但不保持一致
    很容易混乱,出问题很难查,不建议
  4. OSS和CDN都配置并且保持一致
    这是最安全的做法,但保持一致成本较高

总的来说,1和2的安全性是一致的,所以如果安全性不高选择2,安全性高则选择4。

3.  知道上述原理后,我们可以用Filter去实现这个防盗链功能。网上的做法多是用列举的方式去做的,而我这里是用正则去做,相对比较灵活点,另外,我效仿了spring的filter做法,加了个shouldBeFilter的方法,考虑到,比如假如你要拦截*.Action的一部分方法,而不是全部时,我们就可以先看看请求的URL是否shouldBeFilter,如果不是的话,那么就直接放行,在效率上有所提高。废话不说,直接上代码吧。

OSS与CDN的跨域配置

OSS和CDN的跨域配置是分离的。配置可以分为下面几种情况:

  1. 只配置OSS
    安全性一般,可能会通过CDN的域名扫描到资源,而且这样做会因CDN的缓存配置导致有时候200有时候403的情况。
  2. 只配置CDN
    安全性一般,可能会通过OSS的域名扫描到资源。
  3. OSS和CDN都配置但不保持一致
    很容易混乱,出问题很难查,不建议
  4. OSS和CDN都配置并且保持一致
    这是最安全的做法,但保持一致成本较高

总的来说,1和2的安全性是一致的,所以如果安全性不高选择2,安全性高则选择4。

//防盗链filter

public class PreventLinkFilter implements Filter {

    private static Logger logger = LoggerFactory

           .getLogger(PreventLinkFilter.class);

    // 限制访问地址列表正则

    private static List<Pattern> urlLimit = new ArrayList<Pattern>();

    // 允许访问列表

    private static List<String> urlAllow = new ArrayList<String>();

    // 错误地址列表

    private static String urlError = "";



    // 必须过Filter的请求

    protected boolean shouldBeFilter(HttpServletRequest request)

           throws ServletException {

       String path = request.getServletPath();

       for (int i = 0; i < urlLimit.size(); i++) {

           Matcher m = urlLimit.get(i).matcher(path);

           if (m.matches()) {

              logger.debug("当前的Path为{}" + path + "必须进行过滤");

              return true;

           }

       }

       return false;

    }



    public void destroy() {

       // TODO Auto-generated method stub



    }



    public void doFilter(ServletRequest request, ServletResponse response,

           FilterChain chain) throws IOException, ServletException {

       HttpServletRequest httpRequest = (HttpServletRequest) request;

       HttpServletResponse httpResponse = (HttpServletResponse) response;

       if (null == httpRequest || null == httpResponse) {

           return;

       }

       // 放行不符合拦截正则的Path

       if (!shouldBeFilter(httpRequest)) {

           chain.doFilter(request, response);

           return;

       }



       String requestHeader = httpRequest.getHeader("referer");

       if (null == requestHeader) {

           httpResponse.sendRedirect(urlError);

           return;

       }

       for (int i = 0; i < urlAllow.size(); i++) {

           if (requestHeader.startsWith(urlAllow.get(i))) {

              chain.doFilter(httpRequest, httpResponse);

              return;

           }

       }

       httpResponse.sendRedirect(urlError);

       return;

    }



    public void init(FilterConfig fc) throws ServletException {

       logger.debug("防盗链配置开始...");

       String filename;

       try {

           filename = fc.getServletContext().getRealPath(

                  "/WEB-INF/classes/preventLink.properties");

           File f = new File(filename);

           InputStream is = new FileInputStream(f);

           Properties pp = new Properties();

           pp.load(is);

           // 限制访问的地址正则

           String limit = pp.getProperty("url.limit");

           // 解析字符串,变成正则,放在urlLimit列表中

           parseRegx(limit);

           // 不受限的请求头

           String allow = pp.getProperty("url.allow");

           // 将所有允许访问的请求头放在urlAllow列表中

           urlAllow = parseStr(urlAllow, allow);

           urlError = pp.getProperty("url.error");

       } catch (Exception e) {

           e.printStackTrace();

       }



    }



    private void parseRegx(String str) {

       if (null != str) {

           String[] spl = str.split(",");

           if (null != spl) {

              for (int i = 0; i < spl.length; i++) {

                  Pattern p = Pattern.compile(spl[i].trim());

                  urlLimit.add(p);

              }

           }

       }



    }

    private List<String> parseStr(List<String> li, String str) {

       if (null == str || str.trim().equals("")) {

           return null;

       }

       String[] spl = str.split(",");

       if (null != spl && spl.length > 0) {

           li = Arrays.asList(spl);

       }

       return li;

    }

}

 

文件/WEB-INF/classes/preventLink.properties

##用于限制的url正则,用逗号分隔多个(在这里我拦截了诸如/csdn/index!beacher_Ma.action,/csdn/index!beacher_Ma.action?adsfdf)

url.limit=/.+/index/!.+//.action.*,/index/!.+.action?.+

##这里是Http Refer是否以指定前缀开始,前两个是本地调试用的。。

url.allow=)

##这里是被盗链后,response到以下的错误页面

url.error=

参考文章:

  
这篇文章是拦截所有的请求的,他fileter中的url-pattern是/*,这样的话,连/css
/jpg等都话被filter拦截到,要么在里面进行shouldBeFilter的判断,要么就在url-pattern中缩写拦截范围,这个要看具体你要拦截什么样的请求,另外图片防盗链,下载反盗链也是一样的原理的。

来源:

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图