请教, 我有100多个ssl证书, 而且数量会变化, 作为https服务端怎么根据每个请求的域名不同, 使用不同的证书呢?

0

能否在 TcpConnection 的 stream_socket_enable_crypto 之前, 提供一个 beforeSslHandshake 回调方法来修改 socket 的 contentx, 来实现这个功能?

已邀请:

mgzhenhong

赞同来自:

看了些资料, SSL在握手阶段, 客户端发的第一个Hello握手包里有域名, 要实现这个功能, 必须取得这个包里的Extension server_name数据. 现有的socket和stream函数好像没有这样的功能. 不知道是不是要用openssl自己实现ssl握手过程才行.

six

赞同来自:

区分域名的话看起来要openssl手动ssl握手了,这个难度有点大。

mgzhenhong

赞同来自:

$ctx = stream_context_create(["ssl" => [
"local_cert" => "/path/to/cert.pem",
"SNI_server_certs" => [
"domain1.com" => "/path/to/domain1.pem",
"*.domain2.com" => "/path/to/domain2.pem",
"domain3.com" => "/path/to/domain3.pem"
]
]]);

找到一些资料, PHP 5.6以后, stream_socket_server 的 context 可以支持 SNI (Server Name Indication).
这样的话, 这个问题可以解决一半了, 服务端可以支持针对不同的域名使用不同的证书.


剩下的问题就是, 证书数量变化时, 在不重启 Server 的前提下, 如何平滑地重载 SNI 证书列表.

mgzhenhong

赞同来自:

此问题已解决, 目前我这里运行良好. 关键代码和说明如下:


第一步: 声明 context, 启动服务.
$context 的 SNI_server_certs 部分留空, 但最终要将 SNI_server_certs 部分填充为注释所示的样子.


$context = [
'ssl' => [
'verify_peer' => false,
'disable_compression' => true,
'SNI_enabled' => true,
'SNI_server_certs' => [
/*
"*.domain1.com" => [
'local_cert' => "{$this->certFileRoot}/domain1.com/_.domain1.com.pem",
'local_pk' => "{$this->certFileRoot}/domain1.com/_.domain1.com.key",
],
"*.domain2.com" => [
'local_cert' => "{$this->certFileRoot}/domain2.com/_.domain2.com.crt",
'local_pk' => "{$this->certFileRoot}/domain2.com/_.domain2.com.key",
],
"domain3.com" => [
'local_cert' => "{$this->certFileRoot}/domain3.com/domain3.com.crt",
'local_pk' => "{$this->certFileRoot}/domain3.com/domain3.com.key",
],
"www.domain3.com" => [
'local_cert' => "{$this->certFileRoot}/domain3.com/www.domain3.com.crt",
'local_pk' => "{$this->certFileRoot}/domain3.com/www.domain3.com.key",
],
*/
],
],
];

$server = new WorkerX("http://0.0.0.0:443", $context);
$server->count = 10;
$server->transport = 'ssl';
$server->name = 'Https Server';

第二步: 继承并重写 Worker 类, 以便于可以在运行时设置 stream_context
原Worker类中, 使用一个 protected 的 _context 属性保存socket上下文, 外部无法直接修改, 所以需要继承 Worker后,在我们实现的子类中修改.


另外, socket 上下文在php中是一个 resource 类型, 反映到php中可以视为内存地址引用. 对此变量的赋值操作不会创建新的对象.


class WorkerX extends \Workerman\Worker
{
public function contextGetOptions()
: array
{
if(is_resource($this->_context))
{
return stream_context_get_options($this->_context);
}

return [];
}

public function contextSetOptions(array $options)
: bool
{
if(is_resource($this->_context))
{
return stream_context_set_option($this->_context, $options);
}

return false;
}
}

第三步: 在$server的onWorkerStart回调中, 通过Channel注册事件, 允许外部通知服务器动态载入证书信息.
可以启动另外一个专用的api服务, api服务接收管理端的调用后, 发布 EVENT_REFRESH_CERT 事件, 事件数据中标明需要重载哪个站点的证书.


$server->onWorkerStart = function($worker)
{
WorkerDI::init($worker);

$this->refreshCert($worker, 0);

\Channel\Client::on('EVENT_REFRESH_CERT', function($eventData) use ($worker)
{
$siteId = intval($eventData['site_id']);
$this->refreshCert($worker, $siteId);
});
};

private function refreshCert(WorkerX $worker, int $siteId = 0)
{
$sslContextOptions = $worker->contextGetOptions();

// 此处通过 $siteId 查询数据库, 取得站点绑定的域名, 和域名对应的证书文件路径
$domain = '*.domain1.com';
$certFilePath = '/path/to/certFile.crt';
$keyFilePath = '/path/to/certFile.key';

$sslContextOptions['ssl']['SNI_server_certs'][$domain] = [
'local_cert' => $certFilePath,
'local_pk' => $pkFilePath,
];

$setResult = $worker->contextSetOptions($sslContextOptions);

echo "设置" . ($setResult ? "成功" : "失败") . "\n";
}

刷新页面, 此时证书已经在服务中生效, 功能完成.

要回复问题请先登录注册