由于年初新冠疫情爆发,我参与了腾讯防疫健康码的项目研发工作中。疫情健康码项目无疑是非常成功的,它覆盖9亿+人口和300+市县。但是项目的研发过程确实非常艰辛,该项目团队成员是在疫情期间临时组建起来的。疫情健康码项目研发团队由腾讯云同学主导+腾讯志愿者协助+合作伙伴公司的同学组成。大家都是远程在家办公,因此工作中也遇到了一系列的问题。还好有腾讯众多产品的保驾护航,才让项目能够高效成功落地,下面我从个人的研发视角剖析一下远程办公项的痛点,以及我们是怎么解决问题的。
上述的几个问题大部分通过腾讯的Saas产品很好的解决了。例如:
上述的几个产品在我们项目中频繁使用,对我们的项目研发管理协作起到了非常积极的促进作用。
但是远程办公对开发同学还是不友好的,我们使用腾讯的云产品作为项目的开发环境,例如:mysql、redis、es等存储服务。很多开发同学习惯了本地调试代码,即本地起应用连腾讯云的存储服务,使用腾讯云产品作为开发环境时,需要解决公网用户连接腾讯云网络连通权限。例如用户开发的应用A,需要连接mysql 和 es 存储服务来开发调试,那么需要给用户开通他个人出口IP到mysql 和 es 的白名单和3306端口以及9200端口权限。那么可能存在这么几个问题:
问题核心在于远程办公大家都在公网环境,腾讯云服务不能对公网完全开通安全策略,这和裸奔没啥区别。怎么解决呢?因为我之前在研发网关产品,所有我首先想到的就是准入网关的方式来解决,也可以理解成安全网络代理,如下图:
网关的转发安全策略可以用如下伪代码表示:
let urls = {
"msyql_host:3306": 1,
"es_host:9200": 1
}
let user = {
"userA":"tokenA",
"userB":"tokenB",
}
let ip = {
"ipA": 1,
"ipB":1
}
if (urls[req.url] && (authUser(req) || ip[req.remoteAddress])) {
socket.connect(req.url); // 连接目标服务
socket.pipe(req.socket); // 管道转发
}
function authUser(req) {
// 可根据客户选择的算法支持,basic auth 或其他自定义算法判断请求是否合法,return 1 or 0
}
可以直接去找一些开源代理来实现网关功能。
上面网关完成部署之后,我们的代码怎么使用呢?
假设网关支持基本认证(Basic access authentication)和自定义ip白名单的方式(自定义白名单至少解决ip数量的限制), 由于参与的防疫小程序项目使用的是Java作为研发语言,我首先想到的是配置,配置jdk参数方式让应用程序请求远程网络时使用代理。
指定使用代理通信: -DsocksProxyHost=xxx.xxx.xxx.xxx -DsocksProxyPort=1080 -Djava.net.socks.username=xxx -Djava.net.socks.password=xxx 详细可以参考:
jdk8: https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html
找到Jdk源码配置参数使用的类是:DefaultProxySelector.java,由于我们用的是jdk8,因此不支持配置代理基本认证即配置:用户名和密码设置无效,也可以自定义实现Authenticator类,但这种方式会侵代码,下面是JDK11中DefaultProxySelector.java 设置用户名和密码的代码片段。
Authenticator.setDefault (new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication ("username", "password".toCharArray());
}
});
由于上述原因就放弃基本认证方式,而是到网关验证客户端ip。
代理之后es 客户端还是有问题,我这边用的ES客户端版本如下:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.4.3</version>
</dependency>
这个版本的client请求es没有走代理,初次发现问题是es实例化http客户端时,没有用到系统属性,需要显示调用,代码片段如下:
builder.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
@Override
public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
httpClientBuilder.useSystemProperties(); //显示调用使用系统属性。
return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
}
});
显示调用之后,由于es客户端是http请求,还需添加jvm情动参数 -Dhttp.ProxyHost=xxx.xxx.xxx.x xx-Dhttp.ProxyPort=8080 , es客户端才能正常使用代理。
例如:proxifier代理软件,该方式最方便,直接使用网关作为http代理,配置客户端将需要连的中间件都走到代理,工程代码不需要任何配置和代理的改动。但是有两个问题:
该方式自己开发网关客户端,监控本地指定端口,转发到http代理上,这种方式需要将工程里的中间件配置修改指向本地127.0.0.1和映射的端口,如果是https的中间还需要配置hosts,但该方式可以自定义安全策略,灵活自己设计签名算法,较为安全。客户端实现代码片段如下:
const map = {
{
"3306": {
"proxy": "gateway.com:443",
"auth": "", //认证签名串
"target": "mysql_host:3306"
},
"9200": {
"proxy": "gateway.com:443",
"auth": "", //认证签名串
"target": "es_host:9200"
}
}
for (const port in map) {
const cfg = map[port];
const pxyopt = (i => ({ host: i[0], port: i[1]}))(cfg.proxy.split(':'));
const server = net.createServer(socket => {
if (pxyopt['port'] !== '443') {
const proxy = net.connect(pxyopt, () => {
var timestamp = (Date.now() / 1000).toFixed();
var random = Math.floor(Math.random() * 10000);
var result = signature(cfg.auth, random, timestamp);
var pxyauth = result ? `Proxy-Authorization: Basic ${result}\r\n` : '';
proxy.write(`CONNECT ${cfg.target} HTTP/1.1\r\nHost: ${cfg.target}\r\n${pxyauth}\r\n`);
proxy.once('data', d => {
let s = d.toString();
if (s.startsWith('HTTP/1.1 200 ')) {
setImmediate(() => socket.pipe(proxy).pipe(socket));
} else {
proxy.destroy(new Error(s));
}
});
});
var onerr = err => {
socket.destroy();
proxy.destroy();
log(err.message);
};
socket.on('error', onerr)
socket.setTimeout(TIMEOUT, () => proxy.destroy(new Error('timeout')));
proxy.on('error', onerr);
proxy.setTimeout(TIMEOUT, () => proxy.destroy(new Error('timeout')))
} else {
var timestamp = (Date.now() / 1000).toFixed();
var random = Math.floor(Math.random() * 10000);
var result = signature(cfg.auth, random, timestamp);
var options = {
hostname : pxyopt['host'],
port : pxyopt['port'],
path : cfg.target,
method : 'CONNECT',
headers: {
'Proxy-Authorization': `Basic ${result}`
}
};
var req = https.request(options);
req.on('connect', function(res, skt) {
setImmediate(() => socket.pipe(skt).pipe(socket));
socket.on('end', function() {
console.log('socket end.');
});
var onerr = err => {
socket.destroy();
skt.destroy();
log(err.message);
};
socket.on('error', onerr)
socket.setTimeout(TIMEOUT, () => skt.destroy(new Error('timeout')));
skt.on('error', onerr);
skt.setTimeout(TIMEOUT, () => skt.destroy(new Error('timeout')))
});
req.end();
}
}).listen(port, '127.0.0.1', () => console.log(`127.0.0.1:${port} => ${cfg.proxy} => ${cfg.target}`));
server.timeout = TIMEOUT;
}
function signature(appkey, random, timestamp) {
// 自定义签名算法
}
最后我们研发团队使用了自研客户端的方式,nodejs实现,支持可以打包成mac、linux、windows 等多平台运行。使用起来也方便。
1、我们先给每个项目研发成员分配个人的auth签名;
2、将每个人的auth签名配置到网关上;
3、网关认证用户来源是否合法。
上述的实现只是简单的认证了签名的方式,我们还可以拓展自定义更多灵活的安全策略。到此开发环境可以比较方便且安全的连上腾讯云服务了。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。