nginx代理入门

本文通过几个的实例,介绍nginx配置代理的几种方式,以及全局代理的配置。

注:本文中所有实例均可以直接使用,不需要改变任何配置(包括域名,域名解析到127.0.0.1,注意:由于https证书自动续期的需要,仅国内线路的*.local.lyz810.com指向127.0.0.1,国外线路已变更为服务器的IP地址
原始站点(被代理站点)
测试后面所有实例时,请确保该源站配置存在

server {
    listen 80;
    server_name origin.local.lyz810.com;
    default_type text/plain;
    location / {
        return 200 "This is origin.local.lyz810.com$request_uri";
    }

    location =/setcookie/ {
        add_header Set-Cookie "testcookie=123; domain=origin.local.lyz810.com; path=/";
        return 200 "Please find result in response header";
    }

    location =/showcookie/ {
        return 200 $http_cookie;
    }
}

当访问origin.local.lyz810.com的任意页面,都可以看到This is origin.local.lyz810.com后面跟着url

实例一:
通过其他域名进行代理(如目前线上访问google.lyz810.com即可实现代理google.com.hk)
代理配置:

server {
    listen 80;
    server_name proxy.local.lyz810.com;

    location / {
        proxy_pass http://origin.local.lyz810.com/;
    }
}

访问proxy.local.lyz810.com的任意页面,会对应访问origin.local.lyz810.com的页面
如访问proxy.local.lyz810.com/test,返回This is origin.local.lyz810.com/test

实例二:
代理服务器上的url与源站url存在差异,访问proxy.local.lyz810/test/下的所有页面,要求返回origin.local.lyz810.com/hello/对应页面(test改为源站的hello)
代理配置:

server {
    listen 80;
    server_name proxy.local.lyz810.com;

    location /test/ {
        proxy_pass http://origin.local.lyz810.com/hello/;
    }
}

请注意hello后面的/,这个是必须有的,nginx在处理url时,会将location匹配的路径从访问url中去掉后拼接到proxy_pass指令后面
例如访问proxy.local.lyz810.com/test/abc.html,匹配location为/test/,把匹配的部分从url中去掉,还剩abc.html,直接拼接到proxy_pass后面,即http://origin.local.lyz810.com/hello/abc.html
一种典型的错误写法:

    ...
    location /test {
        proxy_pass http://origin.local.lyz810.com/hello/;
    }
    ...

上面的写法语法上没有问题,但是跟需求不匹配。也许你会发现,这个错误的写法在访问proxy.local.lyz810.com/test/abc.html时仍然返回This is origin.local.lyz810.com/hello//abc.html(nginx会对url进行标准化处理,两个/跟一个/访问的是同一个资源)
这是由于hello以/结尾,nginx会将/test后面的内容直接接到proxy_pass最后,造成2个/
但如果访问proxy.local.lyz810.com/test123/abc.html则会返回http://origin.local.lyz810.com/hello/123/abc.html,这个与预期不符。

实例三:
源站要设cookie,由于域名不同,跨域无法设置cookie,通过nginx可以解决这个问题。如访问origin.local.lyz810.com/setcookie/后会在origin.local.lyz810.com域下根目录种一个cookie,现在希望通过代理,访问源站,并在proxy.local.lyz810.com的/test上种上相同的cookie,配置如下:

server {
    listen 80;
    server_name proxy.local.lyz810.com;

    location / {
        proxy_pass http://origin.local.lyz810.com/;
        proxy_cookie_domain origin.local.lyz810.com $host;
        proxy_cookie_path / /test;
    }
}

访问proxy.local.lyz810.com/setcookie/,查看响应头(Set-Cookie:testcookie=123; domain=proxy.local.lyz810.com; path=/test)。
这种使用场景是使用一个域名代理另一个域名的页面,并可以代理认证信息。
我们知道,浏览器是按照域名携带cookie的,所以访问proxy.local.lyz810.com的时候只能带着proxy.local.lyz810.com的cookie访问源站,显然用户不可能自己在proxy.local.lyz810.com上添加cookie,所以我们需要把访问源站登录时,设置的cookie转换为代理域下的cookie,这样访问代理域就和访问源站域完全一样了,相当于镜像站。
通常做完整镜像站时,只需要将cookie的domain修改为代理域即可,path保持一致不需要配置。

实例四:
有时候,页面上的js会对域名做判断,上面所有的方法只能骗过源站的服务器,而不能骗过浏览器的location.hostname,这种情况下,nginx是无法完美解决,如果是开发调试,可以通过配置host来实现。
例如,我的项目地址是demo.lyz810.com,我负责前端开发,需要调用后端接口(后端接口都在/fetch/下)demo.lyz810.com/fetch/api.php?action=getJson(完全可以通过实例三的方法设置另一个域,然后做cookie共享,但我不喜欢开发和线上访问的不一样),那么可以通过下面的方式来实现:
1.设置host 127.0.0.1 demo.lyz810.com
2.nginx配置如下:

server {
    listen 80;
    server_name demo.lyz810.com;
    default_type text/html;
    location /fetch/ {
        proxy_pass http://133.130.97.238/fetch/;
        proxy_set_header Host $host;
    }

    location / {
        return 200 "Please open console to see result<script src='/fetch/api.php?action=getJson'></script>";
    }
}

访问demo.lyz810.com,打开浏览器的控制台,看到发送请求/fetch/api.php返回了信息(这个是线上真实的接口数据),本例中使用了return指令直接返回了一段html代码,正常开发时,此处一般是由root指令指定的静态文件目录,也就是我们的工程目录。
几点说明:
1.proxy_pass需要填写服务器的IP地址,因为本机设置了host,并且nginx在本机部署,所以会受hosts的影响
2.proxy_set_header是nginx访问线上服务器时携带的请求头,因为上面写的是IP,而服务器不能直接通过IP访问(服务器上挂了那么多站点,你用IP访问,它也不知道返回哪个站点),加上Host这个请求头,服务器就知道返回哪个站点下的资源。

实例五:
终极代理大法一,访问本机8888端口,代理任何网站(需要配hosts文件)
代理配置:

server {
    listen 8888;
    server_name _ default;
    resolver 119.29.29.29;
    location / {
        proxy_pass https://$host$request_uri$is_args$args;
    }
}

绑定hosts:127.0.0.1 www.sogou.com,访问http://www.sogou.com:8888/web?query=lyz810&_asf=www.sogou.com可以看到结果,这里用80端口也是完全可以的,hosts文件只要指向本机就可以,如果是局域网内其他机器访问,可以直接配置hosts文件指向nginx服务器的IP,可以通过nginx代理任意站点。
注意:server_name 后面的default表示如果没有匹配到其他的域名,就用当前server,nginx配置文件中同一个端口最多有1个default的server,否则后面设置的default会无效。proxy_pass中域名使用了变量,所以需要配置resolver,即dns服务器地址,这里用的是腾讯云提供的公共dns。此例中代理走https协议,因为目前大部分网站支持https,这里暂且用https,后面有更灵活配置方式。
这种配置的一种应用是共享账号,可以登录某个网站后,将cookie记下来,配置在服务端的proxy_set_header Cookie …中,其他人访问该代理服务器,不需要登录就可以访问公共账号登录的内容。

实例六:
全局代理,无需配置hosts

server {
    listen 80;
    server_name proxy.local.lyz810.com;
    resolver 119.29.29.29;

    location ~ ^/([^/]*)(.*)$ {
        proxy_pass $scheme://$1$2$is_args$args;
    }
}

注意:这种方法只适合于代理任意已知地址,由于网页中很少使用相对地址,所以一般不能直接通过这种代理访问网站,仅限开发调试使用。
这里给出的例子并不完美,转发使用了$scheme根据访问的协议进行同协议转发,但只监听了80端口,所以此处一定为http,可以同时监听443并开启ssl,同时设置证书,已超出本文的内容,不再赘述。
这种配置也有应用场景,目前用于统一代理某些已知接口,不需要每次修改配置文件,只需要写好正确的地址,即可拿到数据。
线上应用在proxy.lyz810.com上,通过此服务器进行跨域管理,所有需要跨域访问的内容交给服务器去访问,返回的结果通过add_header增加跨域头,这样就可以让应用轻松的跨域,具体即实现方法以后介绍。

Nginx配置WordPress与HSTS

本文介绍HSTS基本概念,以及在nginx上配置HSTS的方法,配置站点基于WordPress。

本博客(https://blog.lyz810.com)已开启HSTS,读者可以尝试访问http协议,并观察浏览器的行为。
一、背景介绍
HSTS(HTTP Strict Transport Security)是HTTP严格传输安全,它是全站HTTPS时的一个更安全的策略,并且对网站性能有一定的优化。
全站HTTPS是一个必然的趋势,目前全站HTTPS通常的做法是同时开启80和443端口,当请求访问的是http协议时,通过rewrite将请求301重定向到https协议的对应URI。这么做的缺点是当用户访问http协议的页面时,总会有一次301重定向,增加网络请求以及服务器负担。
使用HSTS后,浏览器在第一次访问http协议时,会通过rewrite重定向到https协议,并根据https协议头中设定的相关响应头缓存下HSTS配置,下次用户再访问该站点下的任意http页面,浏览器会自动通过307跳转到https对应的URI上,不需要向服务器多发送一次请求。
HSTS配置方法如下:
1.当客户端通过HTTPS发出请求时,在服务器返回的超文本传输协议响应头中包含Strict-Transport-Security字段。非加密传输时设置的HSTS字段无效。
2.Strict-Transport-Security有3个参数,max-age表示有效期(如max-age=31536000表示未来的一年内,访问该域名会强制使用https,其中数字表示一年的秒数);includeSubDomains表示子域名也强制使用https;preload主要用于加入preload列表使用。
preload列表是一个站点的列表,他将会被通过硬编码写入 Chrome 浏览器中,列表中的站点将会默认使用 HTTPS 进行访问,此外,Firefox 、Safari 、IE 11 和 Edge 也同样一份 HSTS 站点列表,其中包括了 Chrome 的列表
加入preload列表的条件:

  • 有一张有效的证书
  • 重定向所有的 HTTP 流量到 HTTPS ( HTTPS ONLY )
  • 全部子域名的流量均通过 HTTPS ,如果子域名的 www 存在的话也同样需要通过 HTTPS 传输。
  • 为https协议添加的响应头Strict-Transport-Securit内容必须满足:max-age必须大于18周,必须指定includeSubDomains和preload,如果从当前https站点重定向到其他的站点,那个站点也必须启用HSTS

加入HSTS preload list:https://hstspreload.appspot.com/
二、Nginx配置

server {
  ......
  if ( $https != "on" ) {#https变量的值在https协议下为on,否则为空字符串
     rewrite ^(.*) https://$host$1 permanent;#301定向到https协议的相同URI,主要是首次访问本站的请求使用
     break;
  }

  location / {
     try_files $uri $uri/ /index.php$args;#此行是WordPress使用的,它会将请求都交给/index.php处理
     add_header Strict-Transport-Security max-age=86400000;#添加响应头,过期时间1000天,即1000天之内,浏览器会自动将本站http协议转为https协议
  }

  location ~ \.php$ {
     fastcgi_pass localhost:9000;#fastcgi监听9000端口,其他fastcgi配置写在了公用配置文件中
     add_header Strict-Transport-Security max-age=86400000;#由于正则的优先级更高,所以所有动态的页面均会走这里,需要添加响应头
   }
}

三、使用HSTS的弊端
用户只要访问一次站点,就会在max-age指定的时间内,强制访问https协议,在此期间,如果网站https出现了问题(如证书配置错误),用户就无法进行访问,没有配置HSTS时,证书错误用户可以选择忽略错误继续访问。
如果想要在过期时间内再次开启http访问,是无法做到的(对没有访问过站点的用户可行,只要访问过,过期时间前用户不能访问http协议),如果加入到HSTS Preload List中,就更没有办法启用http访问。
四、浏览器支持度
Chromium和Google Chrome从4.0.211.0版本开始支持HSTS
Firefox 4及以上版本
Opera 12及以上版本
Safari从OS X Mavericks起
Internet Explorer和Microsoft Edge从Windows 10开始支持

Nginx loaction优先级探究

本文通过配置实例来探究Nginx中location的优先级,在Nginx官方文档中,有优先级的说明,我们通过实例对此进行验证,加深理解。

实验声明:
return指令
该指令可以直接返回一个状态码及文本信息,由于是实验环境,没有必要建立真实的目录来对比location的命中,也免去了查看日志等繁琐的步骤。
因此,此实验均使用return指令来返回结果,用到的指令格式为return code text

实验环境
本文中使用的server_name采用的域名test.local.lyz810.com为真实域名,绑定IP为127.0.0.1,所以读者可以完全复制到本机实验。另外,*.local.lyz810.com域名做了泛解析,全部解析到127.0.0.1,读者可以任意选用该域名的子域名进行各种实验。

location类型

  1. 精确匹配:=
  2. 正则大小写敏感:~
  3. 正则大小写不敏感:~*
  4. 前缀匹配(高优先级):^~
  5. 前缀匹配:/

location优先级
以下是官网中给出的优先级:

To find location matching a given request, nginx first checks locations defined using the prefix strings (prefix locations). Among them, the location with the longest matching prefix is selected and remembered. Then regular expressions are checked, in the order of their appearance in the configuration file. The search of regular expressions terminates on the first match, and the corresponding configuration is used. If no match with a regular expression is found then the configuration of the prefix location remembered earlier is used.

翻译:
Nginx首先检查使用前缀定义的location,会选择并记住一个最长匹配的location作为候选。
检查正则表达式,根据出现在配置文件中的顺序。
遇到第一个匹配的正则表达式就停止搜索。
如果没有匹配的正则表达式,就使用最开始候选的location。

由此可以看出,正则表达式优先级最高,多个正则表达式按从上到下就先匹配到的为准,其次是前缀location。
注意,上面是普通前缀与正则之间的比较,所有location中精确匹配优先级最高。

实验一
目的:验证精确匹配优先级最高
原理:让所有类型的location出现在一个server中,并将精确匹配放到最后,消除顺序可能带来的影响,观察是否命中精确匹配location
配置:

server {
  server_name test.local.lyz810.com;
  default_type text/plain;

  location /test123 {
    return 200 prefix;
  }
  
  location ~ "/test\d{3}$" {
    return 200 regular;
  }

  location =/test123 {
    return 200 exact;
  }
}

结果:访问/test123,返回exact.说明精确匹配优先级最高

实验二
目的:验证正则的优先级高于前缀
原理:访问一个同时满足前缀和正则的页面,观察返回结果。为了排除顺序的影响,提供两个不同顺序。
配置:

server {
  server_name test.local.lyz810.com;
  default_type text/plain;

  location /test123 {
    return 200 prefix;
  }
  
  location ~ "/test\d{3}" {
    return 200 regular;
  }

  location ~ "/welcome\d{3}$" {
    return 200 regular;
  }

  location /welcome123 {
    return 200 prefix;
  }
}

结果:访问/test123和/welcome123都返回regular,说明正则优先级高于前缀,并且与正则和前缀出现的顺序无关。

实验三
目的:验证前缀匹配只与匹配长度有关,与顺序无关。
原理:通过不同顺序的前缀匹配,观察输出结果。
配置:

server {
  server_name test.local.lyz810.com;
  default_type text/plain;

  location /test123 {
    return 200 test123;
  }
  
  location /test1234 {
    return 200 test1234;
  }

  location /welcome1234 {
    return 200 welcome1234;
  }

  location /welcome123 {
    return 200 welcome123;
  }
}

结果:访问/welcome1234和/test1234分别返回welcome1234和test1234,与顺序无关,只与匹配长度有关。

实验四
目的:比较高优先级前缀(^~)与正则的优先级
原理:将正则location置于最前,高优先级前缀location置于后面,通过观察输出结果,比较优先级。
配置:

server {
  server_name test.local.lyz810.com;
  default_type text/plain;

  location ~ "/test\d{3}" {
    return 200 regular;
  }

  location ^~ /test123 {
    return 200 prefix;
  }
}

结果:访问/test123返回prefix,证明^~优先级高于正则

实验五
目的:一个配置文件了解location优先级
配置:

server {
  server_name test.local.lyz810.com;
  default_type text/plain;

  location =/test12345 {
    return 200 =/test12345;
  }

  location /test12 {
    return 200 /test12;
  }

  location /test1234 {
    return 200 /test1234;
  }

  location ^~ /test123 {
    return 200 "^~/test123";
  }

  location ~ "/test\d{3}$" {
    return 200 "~/test\d{3}";
  }

  location ~ "/test\d{4}$" {
    return 200 "~/test\d{4}";
  }

  location ~ "/test\d*" {
    return 200 "~/test\d*";
  }
}

结果:
1.访问/test12返回~/test\d*,匹配项/test12、/test\d*,正则优先级高,故命中正则。
2.访问/test123返回^~/test123,匹配项/test12、^~/test123、~/test\d{3}、~/test\d*,先对前缀进行比较,发现有2个前缀/test12和^~/test123,后者长,用后者,然后发现前缀匹配带^~,不再查找正则,故命中^~/test123。
3.访问/test1234返回~/test\d{4},匹配项/test12、/test1234、^~/test123、~/test\d{4}、~/test\d*,先对前缀进行比较,发现有3个前缀/test12、/test1234、^~/test123,根据匹配长度,选中/test1234,再对正则进行匹配,~/test\d{4}是第一个匹配的正则,而之前选出的前缀中不带^~,故命中第一个匹配的正则。
4.访问/test12345返回=/test12345,匹配项=/test12345、/test12、/test1234、^~/test123、~/test\d*,有精确匹配存在,其他的都靠边站。
5.访问/test123456返回~/test\d*,匹配项/test12、/test1234、^~/test123、~/test\d*,前缀选择/test1234,无^~,命中第一个(唯一一个)匹配的正则表达式。
6.访问/welcome/test1234返回~/test\d{4},匹配项~/test\d{4}、~/test\d*,正则可以匹配path的一部分,而前缀只能从头开始匹配。
7.访问/test12345?params=1返回=/test12345,location匹配的是path部分,不包括后面的queryString。

总结
location匹配的算法描述如下(实验的出的结论,非源码中的算法):
1.令pathname=url路径部分,findPrefix=””
2.令prefixList=所有前缀location,regularList=所有正则location,exactList=所有精确匹配location
3.遍历exactList,查找location =pathname,如果找到,则命中,算法结束
4.遍历prefixList,如果匹配pathname则与findPrefix的长度进行比较
4.1如果长度大于findPrefix,令findPrefix=当前location
4.2如果长度小于findPrefix,不做任何操作,继续遍历
4.3如果长度等于findPrefix,在配置载入的时候,会报location重复的错误(如同时存在/123和^~/123),没有该分支
5.判断findPrefix是否带^~前缀,如果带,命中findPrefix,算法结束
6.遍历regularList,如果匹配pathname,命中当前location,算法结束
7.如果findPrefix不为空,命中findPrefix,算法结束
8.输出404

解决openssl升级到1.1.0后shadowsocks服务报错问题

本文适用于解决openssl升级到1.1.0以上版本,导致shadowsocks2.8.2启动报undefined symbol: EVP_CIPHER_CTX_cleanup错误。

最近将openssl升级到了1.1.0b版本,编译之后shadowsocks无法启动,报错如下:

Traceback (most recent call last):
  File "/usr/bin/ssserver", line 9, in 
    load_entry_point('shadowsocks==2.8.2', 'console_scripts', 'ssserver')()
  File "/usr/lib/python2.7/site-packages/shadowsocks/server.py", line 34, in main
    config = shell.get_config(False)
  File "/usr/lib/python2.7/site-packages/shadowsocks/shell.py", line 262, in get_config
    check_config(config, is_local)
  File "/usr/lib/python2.7/site-packages/shadowsocks/shell.py", line 124, in check_config
    encrypt.try_cipher(config['password'], config['method'])
  File "/usr/lib/python2.7/site-packages/shadowsocks/encrypt.py", line 44, in try_cipher
    Encryptor(key, method)
  File "/usr/lib/python2.7/site-packages/shadowsocks/encrypt.py", line 83, in __init__
    random_string(self._method_info[1]))
  File "/usr/lib/python2.7/site-packages/shadowsocks/encrypt.py", line 109, in get_cipher
    return m[2](method, key, iv, op)
  File "/usr/lib/python2.7/site-packages/shadowsocks/crypto/openssl.py", line 76, in __init__
    load_openssl()
  File "/usr/lib/python2.7/site-packages/shadowsocks/crypto/openssl.py", line 52, in load_openssl
    libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
  File "/usr/lib64/python2.7/ctypes/__init__.py", line 373, in __getattr__
    func = self.__getitem__(name)
  File "/usr/lib64/python2.7/ctypes/__init__.py", line 378, in __getitem__
    func = self._FuncPtr((name_or_ordinal, self))
AttributeError: /usr/local/ssl/lib/libcrypto.so.1.1: undefined symbol: EVP_CIPHER_CTX_cleanup
shadowsocks start failed

这个问题是由于在openssl1.1.0版本中,废弃了EVP_CIPHER_CTX_cleanup函数,如官网中所说:

EVP_CIPHER_CTX was made opaque in OpenSSL 1.1.0. As a result, EVP_CIPHER_CTX_reset() appeared and EVP_CIPHER_CTX_cleanup() disappeared. EVP_CIPHER_CTX_init() remains as an alias for EVP_CIPHER_CTX_reset().

EVP_CIPHER_CTX_reset函数替代了EVP_CIPHER_CTX_cleanup函数
EVP_CIPHER_CTX_reset函数说明:

EVP_CIPHER_CTX_reset() clears all information from a cipher context and free up any allocated memory associate with it, except the ctx itself. This function should be called anytime ctx is to be reused for another EVP_CipherInit() / EVP_CipherUpdate() / EVP_CipherFinal() series of calls.

EVP_CIPHER_CTX_cleanup函数说明:

EVP_CIPHER_CTX_cleanup() clears all information from a cipher context and free up any allocated memory associate with it. It should be called after all operations using a cipher are complete so sensitive information does not remain in memory.

可以看出,二者功能基本上相同,都是释放内存,只是应该调用的时机稍有不同,所以用reset代替cleanup问题不大。

修改方法:

  1. 用vi打开文件:vi /usr/lib/python2.7/site-packages/shadowsocks/crypto/openssl.py
  2. 跳转到52行(shadowsocks2.8.2版本,其他版本搜索一下cleanup)
  3. 进入编辑模式
  4. 将第52行libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
    改为libcrypto.EVP_CIPHER_CTX_reset.argtypes = (c_void_p,)
  5. 再次搜索cleanup(全文件共2处,此处位于111行),将libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx)
    改为libcrypto.EVP_CIPHER_CTX_reset(self._ctx)
  6. 保存并推出
  7. 启动shadowsocks服务:service shadowsocks start

之后我们就可以继续愉快的、科学的畅游互联网了。
提示:openssl1.1.0目前兼容性很不好,大部分的软件都不支持
目前支持的有nginx-1.11.5、curl-7.50.3
不支持的有PHP-7.0.12、openssh-7.3p1
所以如果决定使用openssl1.1.0需要考虑很多兼容问题,必须保留1.0.2或1.0.1(不推荐,存在一些已知漏洞,最重要的是如果服务器要开http2,由于新版chrome必须使用ALPN的限制,只有1.0.2版本支持ALPN,所以必须升级到1.0.2)版本以便编译其他程序。

Content Security Policy(CSP)指令

本文介绍CSP的指令,基于CSP level 3,由于文档截止发文时未定稿,可能出现内容陈旧、错误等问题。

为了减缓跨站脚本攻击,网站开发者应该包含控制脚本和插件资源的指令。他们可以这样做:

  • 包含script-src和object-src指令或
  • 一个default-src指令

开发者不应在策略中包含’unsafe-inline’或data:指令。他们都会允许通过文档中直接包含的代码进行XSS攻击。因此最好彻底避免使用这两个指令。

child-src
child-src指令管理嵌套浏览上下文(iframe、frame)和Worker执行上下文。
示例:

Content-Security-Policy: child-src https://example.com/
下面的代码会引发网络错误,因为url不匹配给出的url模式:
<iframe src="https://not-example.com"></iframe>
<script>
  var blockedWorker = new Worker("data:application/javascript,...");
</script>

connect-src
connect-src指令限制从脚本接口加载的url。
通过脚本接口加载资源的方式指:fetch、XHR、eventSource、Beacon、a标签的ping、WebSocket。
示例:

Content-Security-Policy: connect-src https://example.com/
下面代码会引发网络错误:
<a ping="https://not-example.com">...
<script>
  var xhr = new XMLHttpRequest();
  xhr.open('GET', 'https://not-example.com/');
  xhr.send();

  var ws = new WebSocket("https://not-example.com/");

  var es = new EventSource("https://not-example.com/");

  navigator.sendBeacon("https://not-example.com/", { ... });
</script>

default-src
default-src指令是在其他指令没有定义时的默认指令。
示例:

Content-Security-Policy: default-src 'self'
相当于下面的指令:
Content-Security-Policy: connect-src 'self';
                         font-src 'self';
                         frame-src 'self';
                         img-src 'self';
                         manifest-src 'self';
                         media-src 'self';
                         object-src 'self';
                         script-src 'self';
                         style-src 'self';
                         worker-src 'self'
以下指令:
Content-Security-Policy: default-src 'self'; script-src https://example.com
等价于:
Content-Security-Policy: connect-src 'self';
                         font-src 'self';
                         frame-src 'self';
                         img-src 'self';
                         manifest-src 'self';
                         media-src 'self';
                         object-src 'self';
                         script-src https://example.com;
                         style-src 'self';
                         worker-src 'self'

font-src
font-src指令限制字体文件资源加载的URL。
示例:

Content-Security-Policy: font-src https://example.com/
下面的代码会触发网络错误:
<style>
  @font-face {
    font-family: "Example Font";
    src: url("https://not-example.com/font");
  }
  body {
    font-family: "Example Font";
  }
</style>

frame-src
frame-src指令限制嵌套浏览上下文加载的URL。
示例:

Content-Security-Policy: frame-src https://example.com/
下面的代码会触发网络错误:
<iframe src="https://not-example.com/"></iframe>

img-src
img-src指令限制了图片资源加载的URL。
示例:

Content-Security-Policy: img-src https://example.com/
下面代码会触发网络错误:
<img src="https://not-example.com/img" />

manifest-src
manifest-src指令限制应用的manifests文件资源加载的URL。
示例:

Content-Security-Policy: manifest-src https://example.com/
下面代码会触发网络错误:
<link rel="manifest" href="https://not-example.com/manifest">

media-src
media-src指令限制了音频、视频、相关文本追踪的URL。
示例:

Content-Security-Policy: media-src https://example.com/
下面代码会触发网络错误:
<audio src="https://not-example.com/audio"></audio>
<video src="https://not-example.com/video">
    <track kind="subtitles" src="https://not-example.com/subtitles">
</video>

object-src
object-src指令限制插件内容加载的URL。
示例:

Content-Security-Policy: object-src https://example.com/
下面代码会触发网络错误:
<embed src="https://not-example.com/flash"></embed>
<object data="https://not-example.com/flash"></object>
<applet archive="https://not-example.com/flash"></applet>

如果插件的内容没有通过相关的URL(如object元素没有data属性,但是根据指定的类型加载了默认的插件),除非object-src的值是’none’必须阻止,否则都会允许。

script-src
script-src指令限制脚本执行的位置。这不仅包含直接通过script元素引用的URL还包括内联脚本块和XSLT样式表触发的脚本执行。

style-src
style-src指令限制样式应用的位置。包括link标签、@import、HTTP头加载的样式。

worker-src
worker-src指令限制Worker、SharedWorker或ServiceWorker加载的URL。
示例:

Content-Security-Policy: worker-src https://example.com/
下面代码会引发网络错误:
<script>
  var blockedWorker = new Worker("data:application/javascript,...");
  blockedWorker = new SharedWorker("https://not-example.com/");
  navigator.serviceWorker.register('https://not-example.com/sw.js');
</script>

base-uri
base-uri指令限制base元素中的URL。

plugin-types
plugin-types指令限制可被加载的插件类型。
示例:

Content-Security-Policy: plugin-types application/pdf
下面代码会引发网络错误:
<!-- No 'type' declaration(没有声明type) -->
<object data="https://example.com/flash"></object>

<!-- Non-matching 'type' declaration(声明的type和允许的type不一致) -->
<object data="https://example.com/flash" type="application/x-shockwave-flash"></object>

<!-- Non-matching resource(虽然声明的type一致,但其实返回的资源还是个flash类型,也会报错) -->
<object data="https://example.com/flash" type="application/pdf"></object>

下面的策略:
Content-Security-Policy: plugin-types application/x-shockwave-flash
会成功加载如下资源:
<!-- Matching 'type' declaration and resource -->
<object data="https://example.com/flash" type="application/x-shockwave-flash"></object>

sandbox
sandbox指令指定一个客户端需要应用于资源的HTML沙箱策略,就如同包含于带sandbox属性的iframe标签中。

disown-opener
disown-opener指令确保资源与其opener断绝关系。

form-action
form-action指令限制form表单提交的URL。

frame-ancestors
frame-ancestors指令限制可以被通过frame、iframe、object、embed、applet元素嵌入的资源的URL。

report-uri
report-uri指令用于报告非法请求,已过时,应使用report-to指令代替。

report-to
report-to指令用于发送非法的请求信息。

CSP3策略简介

Content Security Policy(CSP)定义了内容安全策略,它是一种缓解web应用受到内容注入攻击(如跨站脚本攻击XSS)的机制。它是一种由作者或服务器管理员声明的策略,用于描述web应用的客户端可以加载哪些源的资源。

一、介绍
为了减缓XSS攻击,一个web应用可以声明它只可以从指定的可信源加载脚本。这个声明允许客户端检测并阻止恶意的脚本被黑客注入到应用中。
CSP并不应作为防御内容注入漏洞的第一道防线,而是用于深度防护,它是为了减少由内容注入攻击造成的伤害。防御内容注入的第一道防线,应该由服务器来验证输入,以及对输出进行编码。
通常会对现有的web应用实现CSP策略,为了获得最大的利益,作者需要移除所有内联脚本和内联样式,例如将他们移动到外部脚本中。因为浏览器不能判断内联脚本是否是由攻击者注入的。
web应用程序通过Content-Security-Policy HTTP头来使用CSP策略。这些策略只适用于当前的资源。如果要为整个站点提供一个策略,服务器需要对每个资源进行声明。
应用示例:
开发者想要保护他们的应用免受跨站脚本攻击。他们只希望加载并执行可信的CDN上的脚本,并且他们希望确保在网页环境中不能执行插件,下面的政策可以实现:
Content-Security-Policy: script-src https://cdn.example.com/scripts/; object-src 'none'

二、第二版相对第一版本的变化
1.现在如果资源加载是重定向的结果,源表达式的路径组件会被忽略。
2.现在受保护的资源可以加载Workers由child-src而不是script-src控制。
3.Workers现在有自己的策略,从加载它们的保护资源中分离。
4.base-url控制受保护的资源可以从指定的文档基础URL加载。
5.child-url代替了frame-src,控制受保护的资源能够嵌入框架并加载Workers。(第三版又有变化)
6.form-action控制受保护的资源的表单提交。
7.frame-ancestors控制受保护资源能否嵌入其他文档。它意图取代X-Frame-Options HTTP请求头。
8.plugin-types控制受保护资源能否加载指定类型的插件。
9.特殊的内联脚本和样式可以通过随机数和哈希加入白名单。
10.CSP请求头现在跟随相关请求头发送。
11.SecurityPolicyViolationEvent事件会在违反策略时触发。
12.新增了一些违反策略的报告(通过report-uri提交以及通过SecurityPolicyViolationEvent事件)。它包括effectiveDirective、statusCode、sourceFile、lineNumber以及columnNumber。
13.现在在sandbox指令中的标志会影响Worker的实例。

三、第三版相对于第二版的变化
1.在CSP第二版中被废弃的frame-src指令,又生效了,worker-src指令添加了进来。如果两者都未出现则遵从child-src指令(如果仍没有则为default-src指令)。
2.URL匹配规则目前把不安全的模式和端口视为它们的安全变种。也就是说http://example.com:80将会同时匹配http://example.com:80和https://example.com:443
‘self’现在匹配https和wss,即使页面是http
3.从内联脚本和内联样式生成的违反报告现在报告“inline”作为阻止资源。阻止eval()的执行会报告“eval”。
4.增加了manifest-src指令。
5.report-uri指令废弃,使用新的report-to指令代替。
6.disown-opener指令确保资源不会被其他浏览器上下文控制。

四、策略的传达
服务器通过HTTP响应头向客户端传达策略。
1.Content-Security-Policy头域
Content-Security-Policy头域是传达策略的较好的机制,它的语法如下:
"Content-Security-Policy":策略字符串
例如,一个响应可能会包含以下头域:
Content-Security-Policy: script-src 'self'
服务器禁止发送多于1个叫做Content-Security-Policy的HTTP头域。
服务器可以针对不同的类型的资源发送不同的值。
在收到服务器响应至少一个Content-Security-Policy头域时,客户端必须执行其中的所有策略。

2.Content-Security-Policy-Report-Only头域
Content-Security-Policy-Report-Only头域只是监控策略而不执行,语法如下:
"Content-Security-Policy-Report-Only":策略字符串
例如,服务器操作员可能希望开发它们的安全策略迭代。操作者可以部署一个report-only策略以检测它们的表现:

Content-Security-Policy-Report-Only: script-src 'self';
                                     report-to csp-reporting-endpoint

在他们的网站中,违反策略的客户端会发送违反报告到report-to中指定的URL上,但是仍然允许违反策略的资源加载。一旦确认策略配置的正确,他们就可以使用Content-Security-Policy头域强制执行。
服务器禁止发送多于1个叫做Content-Security-Policy-Report-Only的HTTP头域。
服务器可以针对不同的类型的资源发送不同的值。
在收到服务器响应至少一个Content-Security-Policy-Report-Only头域时,客户端必须监控其中的所有策略。
Content-Security-Policy-Report-Only头在meta元素中不支持。

3.HTML meta元素
服务器可以通过一个或多个meta元素支持策略。例如:

<meta http-equiv="Content-Security-Policy" content="script-src 'self'">

4.CSP HTTP请求头
CSP头域声明了一个特殊的请求,它是一个策略的主题,它的值定义为下面的ABNF语法:
"CSP:" csp-header-value
csp-header-value = *WSP “active” *WSP
如果用户代理在监控或执行一个包含指令值为源列表的策略,那么用户代理必须与不匹配受保护的资源的来源请求一同发送一个叫做CSP的头域,它的值必须是active。
用户代理可以选择发送这个头,仅当请求的资源类型是活动的策略。也就是说,给定的策略img-src example.com,用户代理会同图片请求一同发送CSP:active头,可以在其他类型的资源中不添加这个头。

5.执行多个策略
上文说道,当有多条策略出现时,每一条必须根据相关类型被执行或报告:

Content-Security-Policy: default-src 'self' http://example.com http://example.net;
                         connect-src 'none';
Content-Security-Policy: connect-src http://example.com/;
                         script-src http://example.com/

连接到example.com是否允许?回答是不允许。执行多个策略指的是通过所有的策略。即使第二条策略会允许连接,但第一条策略包含connect-src ‘none’,所以它会阻止连接。向策略列表添加额外的策略只会更进一步的限制保护资源的能力。
进一步证明,考虑在这个页面的一个脚本。第一个策略会通过default-src指令锁定脚本资源到自身、http://example.com和http://example.net来源。第二条,只会允许脚本来源于http://example.com/。脚本只会在两个策略同时符合时加载,这个例子中来源只有是http://example.com/时符合。

五、策略语法
CSP使用分号“;”分割多个策略。每个策略由下面的语法构成:
WSP表示空白字符,VCHAR表示可打印字符

policy-token    = [ directive-token *( ";" [ directive-token ] ) ]
directive-token = *WSP [ directive-name [ WSP directive-value ] ]
directive-name  = 1*( ALPHA / DIGIT / "-" )
directive-value = *( WSP /  )

解析策略步骤:
1.让directives集合为空的集合
2.遍历每一个非空的token,返回以分号为分隔符对policy进行严格的拆分:
I.跳过空白字符
II.收集非空格的其他字符序列,它们是指令名称
III.如果在token中仍然有字符保留,跳过开头的一个字符(一定是一个空格)
IV.token剩下的字符(如果由)就是指令的值
V.如果指令集合已经包含一个大小写不敏感的匹配指令名,忽略这个指令,继续下一个token
VI.添加指令到指令集合中,名为指令名,值为指令值
3.返回指令集合

具体指令的用法会在后面的文章中说明。

在CentOS7上的nginx中部署Let’s Encrypt免费证书

本文介绍如何在CentOS7上的nginx中部署Let‘s Encrypt免费证书。

之前一直使用的是沃通免费证书,最近看到网上的一些消息,担心Firefox和Chrome会取消它的根证书,所以准备逐步替换为Let’s Encrypt免费证书。目前已在https://demo.lyz810.com中使用,未来可能会逐步将其他域替换为Let’s Encrypt免费证书。
下面将操作步骤记录一下,以便后续替换时查阅。

一、安装certbot
文档:https://certbot.eff.org/#centosrhel7-nginx
$ sudo yum install epel-release
$ sudo yum install certbot

二、为域名申请一个证书
-w后面是站点根目录
-d后面是站点域名,如果多个域名,可以使用多个-d参数,每个-d参数跟一个域名,-d之间用空格分开
certbot certonly --webroot -w /website/lyz810-main/demo/ -d demo.lyz810.com

1.提示输入邮箱,用于紧急通知以及密钥恢复
2.阅读文档,选Agree即可

如果成功证书和私钥会保存在/etc/letsencrypt/live/demo.lyz810.com/中

三、nginx配置证书
ssl_certificate /etc/letsencrypt/live/demo.lyz810.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/demo.lyz810.com/privkey.pem;
重启nginx服务器

四、证书自动续期
证书有效期为90天,所以需要写一个定时任务

#minute   hour    day  month  week    command
0         0,12    *    *      *       certbot renew > /var/log/certbot.log & echo certbot last renew at `date` >> /var/log/certbot.log

在每天0点和12点会更新一次证书,并将结果保存到/var/log/certbot.log日志中。
注意:
如果设置cron或systemd任务,建议一天执行两次(如果没有到需要更新的时间段内,运行此命令不会更新证书)

Referrer策略与防盗链

Referrer策略用于控制浏览器在何种情况下发送referrer信息。该策略可以保护用户的隐私,但也可以使得目前绝大多数站点的防盗链机制失效。

一、介绍
从一个文档发出的请求,以及从文档导航到其他页面,都会有一个Referer头。出于以下原因,我们有时会希望浏览器不发送referer头:
1.隐私
一个社交网站会有每个用户的简介页面,用户会在他们的个人主页中添加一些链接。社交网站可能不希望泄露用户的个人主页URL给被链接的网站(因为个人主页URL可能会泄露其主人的身份信息)。
一些社交网站可能想通知其他网站该链接是从社交网站发起的,但不想泄露包含用户信息的链接(例如,微博中的链接希望告诉对方该链接是在微博中连接过来的,但不希望告诉对方从谁的微博连接过来)。

2.安全
一个网站应用使用https和基于URL的会话标识。应用也许希望链接其他站点的https资源但不想泄露位于URL中的用户会话标识符。
或者,应用可以使用一些url自身的能力(如重置密码的链接、通过链接直接登录账号等)。控制referrer有助于阻止这些有特殊能力的url泄露referrer头。
注意,有其他方式避免这些url的泄露,控制referrer并不足以控制所有的泄露情况。

3.引用
基于HTTPS的博客可能希望连接到一个HTTP上的博客并收到引用链接。

二、referrer策略
Referrer策略包含以下值:

  1. 空字符串
  2. no-referrer
  3. no-referrer-when-downgrade
  4. same-origin
  5. origin
  6. strict-origin
  7. origin-when-cross-origin
  8. strict-origin-when-cross-origin
  9. unsafe-url

下面会详细讲解每种Referrer策略。
1.no-referrer
最简单的策略是“no-referrer”,表示所有的请求都不带referrer。
http://www.lyz810.com/demo/referrer/index.php?referrer=no-referrer

2.no-referrer-when-downgrade
主要针对于受TLS保护的URL(如https),简单的说就是https的页面中,当连接的资源也是https的,则发送完整的referrer,如果连接的资源是http的,就不发送referrer
https://www.lyz810.com/demo/referrer/index.php?referrer=no-referrer-when-downgrade
此例中,音乐可以播放,因为他是http的,不发送referrer,iframe可以显示referrer,因为是https协议。
这个是在没有特别指定referrer策略时,浏览器的默认行为。

3.same-origin
对于同源的链接,会发送referrer,其他的不会。同源意味着域名需要相同,example.com和not.example.com是非同源的。
http://www.lyz810.com/demo/referrer/index.php?referrer=same-origin
上面的例子中可以看到,音乐无法播放了(因为是他站资源),而iframe嵌套的同源页面仍然可以读到referrer。

4.origin
这个策略对于任何资源来说只发送源的信息,不发送完整的url。
http://www.lyz810.com/demo/referrer/index.php?referrer=origin
此例中,音乐无法播放,因为它发送了referrer,iframe中显示的referrer只包含源的信息,不包含完整的url。

5.strict-origin(浏览器可能不支持)
这个策略类似于origin和no-referrer-when-downgrade的合体,如果一个https页面中链接到http的页面或资源,则不会发送referrer。http页面链接以及https链接到https都只发送来源页面的源信息。
https://www.lyz810.com/demo/referrer/index.php?referrer=strict-origin
此例中,音乐正常播放,因为是http的资源,不发送referrer,而iframe中只有源信息。

6.origin-when-cross-origin
该策略在同源的链接中发送完整的URL,其他情况仅发送源信息。相同的域名,http和https协议被认为是非同源的。
http://www.lyz810.com/demo/referrer/index.php?referrer=origin-when-cross-origin
此例中,音乐不能播放,发送源信息,iframe显示完整url。

7.strict-origin-when-cross-origin(浏览器可能不支持)
对于同源请求,发送完整的URL;对于同为https的,只发送源信息;对于http页面只发送源信息;https页面中的http请求不发送referrer。

8.unsafe-url
这个主要是解决https页面中的http资源不发referrer的问题,它会使在https页面中http资源发送完整的referrer。
https://www.lyz810.com/demo/referrer/index.php?referrer=unsafe-url
此例中,音乐不能播放,虽然页面是https,资源是http,但unsafe-url使得浏览器仍发送referrer。

9.空字符串
空字符串表示没有referrer策略,默认为no-referrer-when-downgrade。

三、用法
Referrer策略可以通过以下方法声明:
1.通过http请求头中的Referrer-Policy字段
2.通过meta标签,name为referrer
3.通过<a>、<area>、<img>、<iframe>、<link>元素的referrerpolicy属性。
4.通过<a>、<area><link>元素的rel=noreferrer属性
5.通过隐式继承

四、用法举例
1.http请求头
Referrer-Policy: no-referrer
2.meta标签
<meta name=”referrer” content=”no-referrer” />
3.referrerpolicy属性
<a href=“http://example.com” referrerpolicy=“origin”>
4.rel=noreferrer属性

五、注意事项
Referrer策略还有其他历史遗留的值:
1.never等价于no-referrer
2.default等价于no-referrer-when-downgrade
3.always等价于unsafe-url
4.不建议使用上面三个值,建议使用后面的新值

六、兼容性
IE:不支持(IE高版本中隐式支持default,https页面拉取的http资源不会加referrer)
Edge:仅支持较早版本的值(never、always、origin、default)
Firefox:36+
Chrome:21+
Safari:7.1+(仅支持较早版本的4个值)
Opera:15+
iOS Safari:8+(仅支持较早版本的4个值)

七、关于防盗链
目前大部分网站采用的是判断referrer是否是当前域名或指定白名单域名下的url。而没有referrer的请求都会放行。
referrer策略普及后,单从referrer判断防盗链的方法会失效,所以需要考虑其他的技术手段实现防盗链机制。

CSS.supports API介绍

CSS.supports API用于通过JavaScript检测当前浏览器是否支持某属性及某属性的值。

一、接口用法
1.supports(DOMString property, DOMString value), returns boolean
2.supports(DOMString conditionText), returns boolean

第一种方法接收2个参数,第一个参数是属性的名称(如display、width、margin等);第二个参数是属性的值(如inline-block、100px、-10px等)
第二种方法接收1个参数,为条件字符串,支持or和and运算符,如”(display:inline-block)”、”(width: 100px) or (margin: -10px)”
注意:必须使用括号将每个条件包裹起来,即使只有一个条件

二、规范说明
当supports()方法通过两个参数property和value调用时,如果property是客户端支持的CSS属性名字面量,并且value可以正确的解析为对应属性支持的值,必须返回true。字面量匹配意思是不会对CSS进行处理,前后的空格不会被去除,所以如果有前后空格会导致方法返回false(例如下面的调用会返回false,因为width前面多了一个空格:CSS.supports(‘ width’, ‘100px’))。

当通过单独的一个条件字符串参数进行调用,如果条件字符串能够解析计算为一个支持的属性和值,则返回true;否则返回false。

三、其他说明
1.属性值和属性名不区分大小写
2.如果属性名包含连字符“-”,直接写原始值即可,不需要转换为驼峰方式

四、示例

CSS.supports('width', '-10px');//false,width不能小于0
CSS.supports('height', '50');//false,height需要有单位
CSS.supports('position', 'sticky');//false,也可能为true,测试浏览器不支持sticky
CSS.supports('Z-INDEX', '50');//true,不区分大小写
CSS.supports(' z-index', '50');//false,属性名前面不应该有空格
CSS.supports('z-index', '  50  ');//true,属性值前后可以有空格
CSS.supports('display:none');//false,没有用括号,应为CSS.supports('(display:none)')
CSS.supports('(    display  :  none  )');//true,这种写法前后可以有空格
CSS.supports('(display:none) and (width:-10px)');//false,第二个条件不支持所以整体为false

五、兼容性
IE:不支持
Edge:支持
Firefox:23+
Chrome:28+
Safari:9+
Opera:15+
iOS Safari:9.2+
Android:4.4+

nginx中文文档-ngx_stream_split_clients_module

此页面版本:2016-08-23,57bc4b4b-98b
ngx_stream_split_clients_module模块(1.11.3+)创建用于A/B测试(也叫作分离测试)的变量。

示例配置

stream {
    ...
    split_clients "${remote_addr}AAA" $upstream {
                  0.5%                feature_test1;
                  2.0%                feature_test2;
                  *                   production;
    }

    server {
        ...
        proxy_pass $upstream;
    }
}

split_clients

语法:split_clients string $variable { … }
默认:—
上下文:stream

为A/B测试创建变量,例如:

split_clients "${remote_addr}AAA" $variant {
               0.5%               .one;
               2.0%               .two;
               *                  "";
}

原始字符串的值通过MurmurHash2进行哈希。在给出的示例中,哈希值从0到21474835(0.5%)对应的$variant变量值为“.one”,哈希值从21474836到107374180(2%)对应的值为“.two”,哈希值从107374181到4294967295对应的值为“”(空字符串)。