网络—node中网络通信模块

目前,我们处于互联网时代,互联网产品百花齐放。例如,当打开浏览器,可以看到各种信息,浏览器是如何跟服务器进行通信的?当打开微信跟朋友聊天时,你是如何跟朋友进行消息传递的?这些都得靠网络进程之间的通信,都得依赖于socket。那什么是socket?node中有哪些跟网络通信有关的模块?这些问题是本文研究的重点。

1. Socket

Socket源于Unix,而Unix的基本哲学是『一些皆文件』,都可以用『打开open ==> 读/写(read/write) ==> 关闭(close)』模式来操作,Socket也可以采用这种方法进行理解。关于Socket,可以总结如下几点:

  • 可以实现底层通信,几乎所有的应用层都是通过socket进行通信的,因此『一切且socket』
  • 对TCP/IP协议进行封装,便于应用层协议调用,属于二者之间的中间抽象层
  • 各个语言都与相关实现,例如C、C++、node
  • TCP/IP协议族中,传输层存在两种通用协议: TCP、UDP,两种协议不同,因为不同参数的socket实现过程也不一样

2. node中网络通信的架构实现

node中的模块,从两种语言实现角度来说,存在javscript、c++两部分,通过process.binding来建立关系。具体分析如下:

  • 标准的node模块有net、udp、dns、http、tls、https等
  • V8是chrome的内核,提供了javascript解释运行功能,里面包含tcp_wrap.h、udp_wrap.h、tls_wrap.h等
  • OpenSSL是基本的密码库,包括了MD5、SHA1、RSA等加密算法,构成了node标准模块中的crypto
  • cares模块用于DNS的解析
  • libuv实现了跨平台的异步编程
  • http_parser用于http的解析

3. net使用

net模块是基于TCP协议的socket网路编程模块,http模块就是建立在该模块的基础上实现的,先来看看基本使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 创建socket服务器 server.js
const net = require('net')
const server = net.createServer();
server.on('connection', (socket) => {
socket.pipe(process.stdout);
socket.write('data from server');
});
server.listen(3000, () => {
console.log(`server is on ${JSON.stringify(server.address())}`);
});

// 创建socket客户端 client.js
const net = require('net');
const client = net.connect({port: 3000});
client.on('connect', () => {
client.write('data from client');
});
client.on('data', (chunk) => {
console.log(chunk.toString());
client.end();
});
// 打开两个终端,分别执行`node server.js`、`node client.js`,可以看到客户端与服务器进行了数据通信。

使用const server = net.createServer();创建了server对象,那server对象有哪些特点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// net.js
exports.createServer = function(options, connectionListener) {
return new Server(options, connectionListener);
};
function Server(options, connectionListener) {
EventEmitter.call(this);
...
if (typeof connectionListener === 'function') {
this.on('connection', connectionListener);
}
...
this._handle = null;
}
util.inherits(Server, EventEmitter);

上述代码可以分为几个点:

  • createServer就是一个语法糖,帮助new生成server对象
  • server对象继承了EventEmitter,具有事件的相关方法
  • _handle是server处理的句柄,属性值最终由c++部分的TCPPipe类创建
  • connectionListener也是语法糖,作为connection事件的回调函数

再来看看connectionListener事件的回调函数,里面包含一个socket对象,该对象是一个连接套接字,是个五元组(server_host、server_ip、protocol、client_host、client_ip),相关实现如下:

1
2
3
4
5
6
7
8
function onconnection(err, clientHandle) {
...
var socket = new Socket({
...
});
...
self.emit('connection', socket);
}

因为Socket是继承了stream.Duplex,所以Socket也是一个可读可写流,可以使用流的方法进行数据的处理。

接下来就是很关键的端口监听(port),这是server与client的主要区别,代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Server.prototype.listen = function() {
...
listen(self, ip, port, addressType, backlog, fd, exclusive);
...
}
function listen(self, address, port, addressType, backlog, fd, exclusive) {
...
if (!cluster) cluster = require('cluster');
if (cluster.isMaster || exclusive) {
self._listen2(address, port, addressType, backlog, fd);
return;
}
cluster._getServer(self, {
...
}, cb);
function cb(err, handle) {
...
self._handle = handle;
self._listen2(address, port, addressType, backlog, fd);
...
}
}
Server.prototype._listen2 = function(address, port, addressType, backlog, fd) {
if (this._handle) {
...
} else {
...
rval = createServerHandle(address, port, addressType, fd);
...
this._handle = rval;
}
this._handle.onconnection = onconnection;
var err = _listen(this._handle, backlog);
...
}

function _listen(handle, backlog) {
return handle.listen(backlog || 511);
}

上述代码有几个点需要注意:

  • 监听的对象可以是端口、路径、定义好的server句柄、文件描述符
  • 当通过cluster创建工作进程(worker)时,exclusive判断是否进行socket连接的共享
  • 事件监听最终还是通过TCP/Pipe的listen来实现
  • backlog规定了socket连接的限制,默认最多为511

接下来分析下listen中最重要的_handle了,_handle决定了server的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createServerHandle(address, port, addressType, fd) {
...
if (typeof fd === 'number' && fd >= 0) {
...
handle = createHandle(fd);
...
} else if(port === -1 && addressType === -1){
handle = new Pipe();
} else {
handle = new TCP();
}
...
return handle;
}
function createHandle(fd) {
var type = TTYWrap.guessHandleType(fd);
if (type === 'PIPE') return new Pipe();
if (type === 'TCP') return new TCP();
throw new TypeError('Unsupported fd type: ' + type);
}

_handle由C++中的Pipe、TCP实现,因而要想完全搞清楚node中的网络通信,必须深入到V8的源码里面。

4. UDP/dgram使用

跟net模块相比,基于UDP通信的dgram模块就简单了很多,因为不需要通过三次握手建立连接,所以整个通信的过程就简单了很多,对于数据准确性要求不太高的业务场景,可以使用该模块完成数据的通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// server端实现
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
server.on('message', (msg, addressInfo) => {
console.log(addressInfo);
console.log(msg.toString());
const data = Buffer.from('from server');
server.send(data, addressInfo.port);
});
server.bind(3000, () => {
console.log('server is on ', server.address());
});
// client端实现
const dgram = require('dgram');
const client = dgram.createSocket('udp4');
const data = Buffer.from('from client');
client.send(data, 3000);
client.on('message', (msg, addressInfo) => {
console.log(addressInfo);
console.log(msg.toString());
client.close();
});

从源码层面分析上述代码的原理实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
exports.createSocket = function(type, listener) {
return new Socket(type, listener);
};
function Socket(type, listener) {
...
var handle = newHandle(type);
this._handle = handle;
...
this.on('message', listener);
...
}
util.inherits(Socket, EventEmitter);
const UDP = process.binding('udp_wrap').UDP;
function newHandle(type) {
if (type == 'udp4') {
const handle = new UDP();
handle.lookup = lookup4;
return handle;
}

if (type == 'udp6') {
const handle = new UDP();
handle.lookup = lookup6;
handle.bind = handle.bind6;
handle.send = handle.send6;
return handle;
}
...
}
Socket.prototype.bind = function(port_ /*, address, callback*/) {
...
startListening(self);
...
}
function startListening(socket) {
socket._handle.onmessage = onMessage;
socket._handle.recvStart();
...
}
function onMessage(nread, handle, buf, rinfo) {
...
self.emit('message', buf, rinfo);
...
}
Socket.prototype.send = function(buffer, offset, length, port, address, callback) {
...
self._handle.lookup(address, function afterDns(ex, ip) {
doSend(ex, self, ip, list, address, port, callback);
});
}
const SendWrap = process.binding('udp_wrap').SendWrap;
function doSend(ex, self, ip, list, address, port, callback) {
...
var req = new SendWrap();
...
var err = self._handle.send(req, list, list.length, port, ip, !!callback);
...
}

上述代码存在几个点需要注意:

  • UDP模块没有继承stream,仅仅继承了EventEmit,后续的所有操作都是基于事件的方式
  • UDP在创建的时候需要注意ipv4和ipv6
  • UDP的_handle是由UDP类创建的
  • 通信过程中可能需要进行DNS查询,解析出ip地址,然后再进行其他操作

5. DNS使用

DNS(Domain Name System)用于域名解析,也就是找到host对应的ip地址,在计算机网络中,这个工作是由网络层的ARP协议实现。在node中存在net模块来完成相应功能,其中dns里面的函数分为两类:

  • 依赖底层操作系统实现域名解析,也就是我们日常开发中,域名的解析规则,可以回使用浏览器缓存、本地缓存、路由器缓存、dns服务器,该类仅有dns.lookup
  • 该类的dns解析,直接到nds服务器执行域名解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const dns = require('dns');
const host = 'bj.meituan.com';
dns.lookup(host, (err, address, family) => {
if (err) {
console.log(err);
return;
}
console.log('by net.lookup, address is: %s, family is: %s', address, family);
});

dns.resolve(host, (err, address) => {
if (err) {
console.log(err);
return;
}
console.log('by net.resolve, address is: %s', address);
})
// by net.resolve, address is: 103.37.152.41
// by net.lookup, address is: 103.37.152.41, family is: 4

在这种情况下,二者解析的结果是一样的,但是假如我们修改本地的/etc/hosts文件呢

1
2
3
4
5
6
// 在/etc/host文件中,增加:
10.10.10.0 bj.meituan.com

// 然后再执行上述文件,结果是:
by net.resolve, address is: 103.37.152.41
by net.lookup, address is: 10.10.10.0, family is: 4

接下来分析下dns的内部实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const cares = process.binding('cares_wrap');
const GetAddrInfoReqWrap = cares.GetAddrInfoReqWrap;
exports.lookup = function lookup(hostname, options, callback) {
...
callback = makeAsync(callback);
...
var req = new GetAddrInfoReqWrap();
req.callback = callback;
var err = cares.getaddrinfo(req, hostname, family, hints);
...
}

function resolver(bindingName) {
var binding = cares[bindingName];
return function query(name, callback) {
...
callback = makeAsync(callback);
var req = new QueryReqWrap();
req.callback = callback;
var err = binding(req, name);
...
return req;
}
}
var resolveMap = Object.create(null);
exports.resolve4 = resolveMap.A = resolver('queryA');
exports.resolve6 = resolveMap.AAAA = resolver('queryAaaa');
...
exports.resolve = function(hostname, type_, callback_) {
...
resolver = resolveMap[type_];
return resolver(hostname, callback);
...
}

上面的源码有几个点需要关注:

  • lookup与resolve存在差异,使用的时候需要注意
  • 不管是lookup还是resolve,均依赖于cares库
  • 域名解析的type很多: resolve4、resolve6、resolveCname、resolveMx、resolveNs、resolveTxt、resolveSrv、resolvePtr、resolveNaptr、resolveSoa、reverse

6. HTTP使用

在WEB开发中,HTTP作为最流行、最重要的应用层,是每个开发人员应该熟知的基础知识,我面试的时候必问的一块内容。同时,大多数同学接触node时,首先使用的恐怕就是http模块。先来一个简单的demo看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const http = require('http');
const server = http.createServer();
server.on('request', (req, res) => {
res.setHeader('foo', 'test');
res.writeHead(200, {
'Content-Type': 'text/html',
});
res.write('<!doctype>');
res.end(`<html></html>`);
});

server.listen(3000, () => {
console.log('server is on ', server.address());
var req = http.request({ host: '127.0.0.1', port: 3000});
req.on('response', (res) => {
res.on('data', (chunk) => console.log('data from server ', chunk.toString()) );
res.on('end', () => server.close() );
});
req.end();
});
// 输出结果如下:
// server is on { address: '::', family: 'IPv6', port: 3000 }
// data from server <!doctype>
// data from server <html></html>

针对上述demo,有很多值得深究的地方,一不注意服务就挂掉了,下面根据node的官方文档,逐个进行研究。

6.1 http.Agent

因为HTTP协议是无状态协议,每个请求均需通过三次握手建立连接进行通信,众所周知三次握手、慢启动算法、四次挥手等过程很消耗时间,因此HTTP1.1协议引入了keep-alive来避免频繁的连接。那么对于tcp连接该如何管理呢?http.Agent就是做这个工作的。先看看源码中的关键部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function Agent(options) {
...
EventEmitter.call(this);
...
self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets;
self.maxFreeSockets = self.options.maxFreeSockets || 256;
...
self.requests = {}; // 请求队列
self.sockets = {}; // 正在使用的tcp连接池
self.freeSockets = {}; // 空闲的连接池
self.on('free', function(socket, options) {
...
// requests、sockets、freeSockets的读写操作
self.requests[name].shift().onSocket(socket);
freeSockets.push(socket);
...
}
}
Agent.defaultMaxSockets = Infinity;
util.inherits(Agent, EventEmitter);
// 关于socket的相关增删改查操作
Agent.prototype.addRequest = function(req, options) {
...
if (freeLen) {
var socket = this.freeSockets[name].shift();
...
this.sockets[name].push(socket);
...
} else if (sockLen < this.maxSockets) {
...
} else {
this.requests[name].push(req);
}
...
}
Agent.prototype.createSocket = function(req, options, cb) { ... }
Agent.prototype.removeSocket = function(s, options) { ... }
exports.globalAgent = new Agent();

上述代码有几个点需要注意:

  • maxSockets默认情况下,没有tcp连接数量的上限(Infinity)
  • 连接池管理的核心是对socketsfreeSockets的增删查
  • globalAgent会作为http.ClientRequest的默认agent

下面可以测试下agent对请求本身的限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// req.js
const http = require('http');

const server = http.createServer();
server.on('request', (req, res) => {
var i=1;
setTimeout(() => {
res.end('ok ', i++);
}, 1000)
});

server.listen(3000, () => {
var max = 20;
for(var i=0; i<max; i++) {
var req = http.request({ host: '127.0.0.1', port: 3000});
req.on('response', (res) => {
res.on('data', (chunk) => console.log('data from server ', chunk.toString()) );
res.on('end', () => server.close() );
});
req.end();
}
});
// 在终端中执行time node ./req.js,结果为:
// real 0m1.123s
// user 0m0.102s
// sys 0m0.024s

// 在req.js中添加下面代码
http.globalAgent.maxSockets = 5;
// 然后同样time node ./req.js,结果为:
real 0m4.141s
user 0m0.103s
sys 0m0.024s

当设置maxSockets为某个值时,tcp的连接就会被限制在某个值,剩余的请求就会进入requests队列里面,等有空余的socket连接后,从request队列中出栈,发送请求。

6.2 http.ClientRequest

当执行http.request时,会生成ClientRequest对象,该对象虽然没有直接继承Stream.Writable,但是继承了http.OutgoingMessage,而http.OutgoingMessage实现了write、end方法,因为可以当跟stream.Writable一样的使用。

1
2
3
4
5
6
7
var req = http.request({ host: '127.0.0.1', port: 3000, method: 'post'});
req.on('response', (res) => {
res.on('data', (chunk) => console.log('data from server ', chunk.toString()) );
res.on('end', () => server.close() );
});
// 直接使用pipe,在request请求中添加数据
fs.createReadStream('./data.json').pipe(req);

接下来,看看http.ClientRequest的实现, ClientRequest继承了OutgoingMessage:

1
2
3
4
5
6
7
const OutgoingMessage = require('_http_outgoing').OutgoingMessage;
function ClientRequest(options, cb) {
...
OutgoingMessage.call(self);
...
}
util.inherits(ClientRequest, OutgoingMessage);

6.3 http.Server

http.createServer其实就是创建了一个http.Server对象,关键源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
exports.createServer = function(requestListener) {
return new Server(requestListener);
};
function Server(requestListener) {
...
net.Server.call(this, { allowHalfOpen: true });
if (requestListener) {
this.addListener('request', requestListener);
}
...
this.addListener('connection', connectionListener);
this.timeout = 2 * 60 * 1000;
...
}
util.inherits(Server, net.Server);
function connectionListener(socket) {
...
socket.on('end', socketOnEnd);
socket.on('data', socketOnData)
...
}

有几个需要要关注的点:

  • 服务的创建依赖于net.server,通过net.server在底层实现服务的创建
  • 默认情况下,服务的超时时间为2分钟
  • connectionListener处理tcp连接后的行为,跟net保持一致

6.4 http.ServerResponse

看node.org官方是如何介绍server端的response对象的:

This object is created internally by an HTTP server–not by the user. It is passed as the second parameter to the ‘request’ event.

The response implements, but does not inherit from, the Writable Stream interface.

跟http.ClientRequest很像,继承了OutgoingMessage,没有继承Stream.Writable,但是实现了Stream的功能,可以跟Stream.Writable一样灵活使用:

1
2
3
4
5
6
function ServerResponse(req) {
...
OutgoingMessage.call(this);
...
}
util.inherits(ServerResponse, OutgoingMessage);

6.5 http.IncomingMessage

An IncomingMessage object is created by http.Server or http.ClientRequest and passed as the first argument to the ‘request’ and ‘response’ event respectively. It may be used to access response status, headers and data.

http.IncomingMessage有两个地方时被内部创建,一个是作为server端的request,另外一个是作为client请求中的response,同时该类显示地继承了Stream.Readable。

1
2
3
4
5
6
7
function IncomingMessage(socket) {
Stream.Readable.call(this);
this.socket = socket;
this.connection = socket;
...
}
util.inherits(IncomingMessage, Stream.Readable);

原文

http://zhenhua-lee.github.io/node/socket.html

模块—你需要了解的Node.js 模块

Node 使用两个核心模块来管理模块依赖:

  • require 模块,在全局范围可用——无需 require(‘require’)。
  • module 模块,在全局范围可用——无需 require(‘module’)。

你可以将 require 模块视为命令,将 module 模块视为所有必需模块的组织者。
在 Node 中获取一个模块并不复杂。

1
const config = require('/path/to/file');

由 require 模块导出的主要对象是一个函数(如上例所用)。 当 Node 使用本地文件路径作为函数的唯一参数调用该 require() 函数时,Node 将执行以下步骤:

  • 解析:找到文件的绝对路径。
  • 加载:确定文件内容的类型.
  • 封装:给文件其私有作用域。 这使得 require 和 module 对象两者都可以下载我们需要的每个文件。
  • 评估:这是 VM 对加载的代码最后需要做的。
  • 缓存:当我们再次需要这个文件时,不再重复所有的步骤。

在本文中,我将尝试用示例解释这些不同的阶段,以及它们是如何影响我们在 Node 中编写模块的方式的。

先在终端创建一个目录来保存所有示例:

1
mkdir ~/learn-node && cd ~/learn-node

本文之后所有命令都在 ~/learn-node 下运行。

解析本地路径

我现在向你介绍 module 对象。你可以在一个的 REPL(译者注:Read-Eval-Print-Loop,就是一般控制台干的事情)会话中很容易地看到它:

1
2
3
4
5
6
7
8
9
10
~/learn-node $ node
> module
Module {
id: '<repl>',
exports: {},
parent: undefined,
filename: null,
loaded: false,
children: [],
paths: [ ... ] }

每个模块对象都有一个 id 属性作为标识。这个 id 通常是文件的完整路径,不过在 REPL 会话中,它只是

Node 模块与文件系统有着一对一的关系。请求模块就是把文件内容加载到内存中。

不过,因为 Node 中有很多方法用于请求文件(比如,使用相对路径,或预定义的路径),在我们把文件内容加载到内存之前,我们需要找到文件的绝对位置。
现在请求 ‘find-me’ 模块,但不指定路径:

1
require('find-me');

Node 会按顺序在 module.paths 指定的路径中去寻找 find-me.js。

1
2
3
4
5
6
7
8
9
10
~/learn-node $ node
> module.paths
[ '/Users/samer/learn-node/repl/node_modules',
'/Users/samer/learn-node/node_modules',
'/Users/samer/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/samer/.node_modules',
'/Users/samer/.node_libraries',
'/usr/local/Cellar/node/7.7.1/lib/node' ]

路径列表基本上会是从当前目录到根目录下的每一个 node_modules 目录。它也会包含一些不推荐使用的遗留目录。

如果 Node 在这些目录下仍然找不到 find-me.js,它会抛出 “cannot find module error.(不能找到模块)” 这个错误消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
~/learn-node $ node
> require('find-me')
Error: Cannot find module 'find-me'
at Function.Module._resolveFilename (module.js:470:15)
at Function.Module._load (module.js:418:25)
at Module.require (module.js:498:17)
at require (internal/module.js:20:19)
at repl:1:1
at ContextifyScript.Script.runInThisContext (vm.js:23:33)
at REPLServer.defaultEval (repl.js:336:29)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
at REPLServer.onLine (repl.js:533:10)

现在创建一个局部的 node_modules 目录,放入一个 find-me.js,require(‘find-me’) 就能找到它。

1
2
3
4
5
6
7
~/learn-node $ mkdir node_modules 
~/learn-node $ echo "console.log('I am not lost');" > node_modules/find-me.js
~/learn-node $ node
> require('find-me');
I am not lost
{}
>

如果别的路径下存在另一个 find-me.js 文件,例如在 home 目录下存在 node_modules 目录,其中有一个不同的 find-me.js:

1
2
$ mkdir ~/node_modules
$ echo "console.log('I am the root of all problems');" > ~/node_modules/find-me.js

现在 learn-node 目录也包含 node_modules/find-me.js —— 在这个目录下 require(‘find-me’),那么 home 目录下的 find-me.js 根本不会被加载:

1
2
3
4
5
~/learn-node $ node
> require('find-me')
I am not lost
{}
>

如果删除了~/learn-node 目录下的的 node_modules 目录,再次尝试请求 find-me.js,就会使用 home 目录下 node_modules 目录中的 find-me.js 了:

1
2
3
4
5
6
~/learn-node $ rm -r node_modules/
~/learn-node $ node
> require('find-me')
I am the root of all problems
{}
>

请求一个目录

模块不一定是文件。我们也可以在 node_modules 目录下创建一个 find-me 目录,并在其中放一个 index.js 文件。同样的 require(‘find-me’) 会使用这个目录下的 index.js 文件:

1
2
3
4
5
6
7
~/learn-node $ mkdir -p node_modules/find-me
~/learn-node $ echo "console.log('Found again.');" > node_modules/find-me/index.js
~/learn-node $ node
> require('find-me');
Found again.
{}
>

注意如果存在局部模块,home 下 node_modules 路径中的相应模块仍然会被忽略。

在请求一个目录的时候,默认会使用 index.js,不过我们可以通过 package.json 中的 main 选项来改变起始文件。比如,希望 require(‘find-me’) 在 find-me 目录下去使用另一个文件,只需要在那个目录下添加 package.json 文件来完成这个事情:

1
2
3
4
5
6
7
~/learn-node $ echo "console.log('I rule');" > node_modules/find-me/start.js
~/learn-node $ echo '{ "name": "find-me-folder", "main": "start.js" }' > node_modules/find-me/package.json
~/learn-node $ node
> require('find-me');
I rule
{}
>

require.resolve

如果你只是想找到模块,并不想执行它,你可以使用 require.resolve 函数。除了不加载文件,它的行为与主函数 require 完全相同。如果文件不存在它会抛出错误,如果找到了指定的文件,它会返回完整路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> require.resolve('find-me');
'/Users/samer/learn-node/node_modules/find-me/start.js'
> require.resolve('not-there');
Error: Cannot find module 'not-there'
at Function.Module._resolveFilename (module.js:470:15)
at Function.resolve (internal/module.js:27:19)
at repl:1:9
at ContextifyScript.Script.runInThisContext (vm.js:23:33)
at REPLServer.defaultEval (repl.js:336:29)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
at REPLServer.onLine (repl.js:533:10)
at emitOne (events.js:101:20)
at REPLServer.emit (events.js:191:7)
>

这很有用,比如,检查一个可选的包是否安装并在它已安装的情况下使用它。

相对路径和绝对路径

除了在 node_modules 目录中查找模块之外,我们也可以把模块放置于任何位置,然后通过相对路径(./ 和 ../)请求,也可以通过以 / 开始的绝对路径请求。

比如,如果 find-me.js 是放在 lib 目录而不是 node_modules 目录下,可以这样请求:

1
require('./lib/find-me');

文件中的父子关系

创建 lib/util.js 文件并添加一行 console.log 代码来识别它。console.log 会输出模块自身的 module 对象:

1
2
~/learn-node $ mkdir lib
~/learn-node $ echo "console.log('In util', module);" > lib/util.js

在 index.js 文件中干同样的事情,稍后我们会通过 node 命令执行这个文件。让 index.js 文件请求 lib/util.js:

1
~/learn-node $ echo "console.log('In index', module); require('./lib/util');" > index.js

现在用 node 执行 index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
~/learn-node $ node index.js
In index Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/samer/learn-node/index.js',
loaded: false,
children: [],
paths: [ ... ] }
In util Module {
id: '/Users/samer/learn-node/lib/util.js',
exports: {},
parent:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/samer/learn-node/index.js',
loaded: false,
children: [ [Circular] ],
paths: [...] },
filename: '/Users/samer/learn-node/lib/util.js',
loaded: false,
children: [],
paths: [...] }

注意到现在的列表中主模块 index (id: ‘.’) 是 lib/util 模块的父模块。不过 lib/util 模块并未作为 index 的子模块列出来。不过那里有个 [Circular] 值因为那里存在循环引用。如果 Node 打印 lib/util 模块对象,它就会陷入一个无限循环。因此这里用 [Circular] 代替了 lib/util 引用。

现在更重要的问题是,如果 lib/util 模块又请求了 index 模块,会发生什么事情?这就是我们需要了解的循环依赖,Node 允许这种情况存在。

在理解它之前,我们先来搞明白 module 对象中的另外一些概念。

exports、module.exports 以及同步加载模块

exports 是每个模块都有的一个特殊对象。如果你观察仔细,会发现上面示例中每次打印的模块对象中都存在一个 exports 属性,到目前为止它只是个空对象。我们可以给这个特殊的 exports 对象任意添加属性。例如,我们为 index.js 和 lib/util.js 导出 id 属性:

1
2
3
4
// Add the following line at the top of lib/util.js
exports.id = 'lib/util';
// Add the following line at the top of index.js
exports.id = 'index';

现在执行 index.js,我们会看到这些属性受到 module 对象管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~/learn-node $ node index.js
In index Module {
id: '.',
exports: { id: 'index' },
loaded: false,
... }
In util Module {
id: '/Users/samer/learn-node/lib/util.js',
exports: { id: 'lib/util' },
parent:
Module {
id: '.',
exports: { id: 'index' },
loaded: false,
... },
loaded: false,
... }

上面的输出中我去掉了一些属性,这样看起来比较简洁,不过请注意 exports 对象已经包含了我们在每个模块中定义的属性。你可以在 exports 对象中任意添加属性,也可以直接把 exports 整个替换成另一个对象。比如,可以把 exports 对象变成一个函数,我们会这样做:

1
2
// Add the following line in index.js before the console.log 
module.exports = function() {};

现在运行 index.js,你会看到 exports 对象是一个函数:

1
2
3
4
5
6
~/learn-node $ node index.js
In index Module {
id: '.',
exports: [Function],
loaded: false,
... }

注意,我没有通过 exports = function() {} 来将 exports 对象改变为函数。这样做是不行的,因为模块中的 exports 变量只是 module.exports 的引用,它用于管理导出属性。如果我们重新给 exports 变量赋值,就会丢失对 module.exports 的引用,实际会产生一个新的变量,而不是改变了 module.exports。

每个模块中的 module.exports 对象就是通过 require 函数请求那个模块返回的。比如,把 index.js 中的 require(‘./lib/util’) 改为:

1
2
const UTIL = require('./lib/util');
console.log('UTIL:', UTIL);

这段代码会输出 lib/util 导出到 UTIL 常量中的属性。现在运行 index.js,输出如下:

1
UTIL: { id: 'lib/util' }

再来谈谈每个模块的 loaded 属性。到目前为止,每次我们打印一个模块对象的时候,都会看到这个对象的 loaded 属性值为 false。

module 模块使用 loaded 属性来跟踪哪些模块是加载过的(true值),以及哪些模块还在加载中(false 值)。比如我们可以通过调用 setImmediate 来打印 modules 对象,在下一事件循环中看看完成加载的 index.js 模块:

1
2
3
4
// In index.js
setImmediate(() => {
console.log('The index.js module object is now loaded!', module)
});

输出是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
The index.js module object is now loaded! Module {
id: '.',
exports: [Function],
parent: null,
filename: '/Users/samer/learn-node/index.js',
loaded: true,
children:
[ Module {
id: '/Users/samer/learn-node/lib/util.js',
exports: [Object],
parent: [Circular],
filename: '/Users/samer/learn-node/lib/util.js',
loaded: true,
children: [],
paths: [Object] } ],
paths:
[ '/Users/samer/learn-node/node_modules',
'/Users/samer/node_modules',
'/Users/node_modules',
'/node_modules' ] }

注意理解它是如何推迟 console.log,使其在 lib/util.js 和 index.js 加载完成之后再产生输出的。

Node 完成加载模块(并标记)之后 exports 对象就完成了。整个请求/加载某个模块的过程是同步的。因此我们可以在一个事件循环周期过后看到模块已经完成加载。

这也就是说,我们不能异步改变 exports 对象。比如在某个模块中干这样的事情:

1
2
3
4
fs.readFile('/etc/passwd', (err, data) => {
if (err) throw err;
exports.data = data; // Will not work.
});

循环依赖模块

现在来回答关于 Node 循环依赖模块这个重要的问题:如果模块1需要模块2,模块2也需要模块1,会发生什么事情?

为了观察结果,我们在 lib/ 下创建两个文件,module1.js 和 module2.js,它们相互请求对象:

1
2
3
4
5
6
7
8
9
// lib/module1.js
exports.a = 1;
require('./module2');
exports.b = 2;
exports.c = 3;

// lib/module2.js
const Module1 = require('./module1');
console.log('Module1 is partially loaded here', Module1);

运行 module1.js 可以看到:

1
2
~/learn-node $ node lib/module1.js
Module1 is partially loaded here { a: 1 }

我们在 module1 完全加载前请求了 module2,而 module2 在未完全加载时又请求了 module1,那么,在那一时刻,能得到的是在循环依赖之前导出的属性。只有 a 属性打印出来了,因为 b 和 c 是在请求了module2 并打印了 module1 之后才导出的。

Node 让这件事变得简单。在加载某个模块的时候,它会创建 exports 对象。你可以在一个模块加载完成之前请求它,但只会得到部分导出的对象,它只包含到目前为止已经定义的项。

JSON 和 C/C++ addon

我们可以利用 require 函数在本地引入 JSON 文件和 C++ addon 文件。这么做不需要指定文件扩展名。

如果没有指定文件扩展名,Node 首先要处理 .js 文件。如果找不到 .js 文件,就会尝试寻找 .json 文件,如果发现为 JSON 文本文件,便将其解析为 .json 文件。 之后,它将尝试找到一个二进制 .node 文件。为了消除歧义,当需要使用 .js 文件以外的其他格式后缀时,你需要制定一个文件扩展名。

引入 JSON 文件在某些情况下是很有用的,例如,当你在该文件中需要管理的所有内容都是些静态配置值时,或者你需要定期从某个外部源读入值时。假设我们有以下 config.json 文件:

1
2
3
4
{
"host": "localhost",
"port": 8080
}

我们可以像这样直接请求:

1
2
const { host, port } = require('./config');
console.log(`Server will run at http://${host}:${port}`);

运行上面的代码,输出如下:

1
Server will run at http://localhost:8080

如果 Node 不能找到 .js 或 .json 文件,它会寻找 .node 文件,它会被认为是编译好的插件模块。

Node 文档中有一个插件文件示例,它是用 C++ 写的。它只是一个导出了 hello() 函数的简单模块,这个 hello 函数输出 “world”。

你可以使用 node-gyp 包来编译和构建 .cc 文件,生成 .addon 文件。只需要配置一个 binding.gyp 文件来告诉 node-gyp 做什么。

得到 addon.node (或其它在 binding.gyp 中指定的名称)文件后,你可以像请求其它模块一样请求它:

1
2
const addon = require('./addon');
console.log(addon.hello());

我们可以在 require.extensions 中看到实际支持的三个扩展名:

img

看看每个扩展名对应的函数,你就清楚 Node 在怎么使用它们。它使用 module._compile 处理 .js 文件,使用 JSON.parse 处理 .json 文件,以及使用 process.dlopen 处理 .node 文件。

在 Node 编写的所有代码将封装到函数中

有人经常误解 Node 的封装模块的用途。让我们通过 exports/module.exports 之间的关系来了解它。

我们可以使用 exports 对象导出属性,但是我们不能直接替换 exports 对象,因为它仅是对 module.exports 的引用

1
2
3
exports.id = 42; // This is ok.
exports = { id: 42 }; // This will not work.
module.exports = { id: 42 }; // This is ok.

对于每个模块而言这个 exports 对象看似是全局的,这和将其定义为 module 对象的引用,那到底什么是 exports 对象呢?

在解释 Node 的封装过程之前,让我再问一个问题。

在浏览器中,当我们在脚本中如下所示地声明一个变量:

1
var answer = 42;

在定义 answer 变量的脚本之后,该变量将在所有脚本中全局可见。

这在 Node 中根本不是问题。我们在某个模块中定义的变量,其它模块是访问不到的。那么为什么 Node 中变量的作用域这么神奇?

答案很简单。在编译模块之前,Node 会把模块代码封装在一个函数中,我们可以通过 module 模块的 wrapper 属性看出来。

1
2
3
4
5
~ $ node
> require('module').wrapper
[ '(function (exports, require, module, __filename, __dirname) { ',
'\n});' ]
>

Node 不会直接执行你写在文件中的代码。它执行这个包装函数,你写的代码只是它的函数体。因此所有定义在模块中的顶层变量都受限于模块的作用域。

这个包装函数有5个参数:exports, require, module, filename 和 dirname。它们看起来像是全局的,但实际它们在每个模块内部。

所有这些参数都会在 Node 执行包装函数的时候获得值。exports 是 module.exports 的引用。require 和 module 都有特定的功能。filename/dirname 变量包含了模块文件名及其所有目录的绝对路径。

如果你的脚本在第一行出现错误,你就会看到它是如何包装的:

1
2
3
4
5
6
7
~/learn-node $ echo "euaohseu" > bad.js
~/learn-node $ node bad.js
~/bad.js:1
(function (exports, require, module, __filename, __dirname) {
euaohseu
^
ReferenceError: euaohseu is not defined

注意上例中的第一行并非是真的错误引用,而是为了在错误报告中输出包装函数。

此外,既然每个模块都封装在函数中,我们可以通过 arguments 关键字来使用函数的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
~/learn-node $ echo "console.log(arguments)" > index.js
~/learn-node $ node index.js
{ '0': {},
'1':
{ [Function: require]
resolve: [Function: resolve],
main:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/samer/index.js',
loaded: false,
children: [],
paths: [Object] },
extensions: { ... },
cache: { '/Users/samer/index.js': [Object] } },
'2':
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/samer/index.js',
loaded: false,
children: [],
paths: [ ... ] },
'3': '/Users/samer/index.js',
'4': '/Users/samer' }

第一个参数是 exports 对象,它一开始是空的。然后是 require/module 对象,它们与在执行的 index.js 文件的实例关联,并非全局变量。最后 2 个参数是文件的路径及其所在目录的路径。

包装函数的返回值是 module.exports。在包装函数的内部我们可以通过改变 module.exports 属性来改变 exports 对象,但不能直接对 exports 赋值,因为它只是一个引用。

这个事情大致像这样:

1
2
3
4
function (require, module, __filename, __dirname) {
let exports = module.exports; // Your Code...
return module.exports;
}

如果我们直接改变 exports 对象,它就不再是 module.exports 的引用。JavaScript 在任何地方都是这样引用对象,并非只是在这个环境中。

require 对象

require 没什么特别,它主要是作为一个函数来使用,接受模块名称或路径作为参数,返回 module.exports 对象。如果我们想改变 require 对象的逻辑,也很容易。

比如,为了进行测试,我们想让每个 require 调用都被模拟为返回一个假对象来代替模块导出的对象。这个简单的调整就像这样:

1
2
3
require = function() {
return { mocked: true };
}

在上面重新对 require 赋值之后,调用 require(‘something’) 就会返回模拟的对象。

require 对象也有自己的属性。我们已经看到了 resolve 属性,它也是一个函数,是 require 处理过程中解析路径的步骤。上面我们还看到了 require.extensions。

还有一个 require.main 可用于检查代码是通过请求来运行的还是直接运行的。

再来看个例子,定义在 print-in-frame.js 中的 printInFrame 函数:

1
2
3
4
5
6
// In print-in-frame.js
const printInFrame = (size, header) => {
console.log('*'.repeat(size));
console.log(header);
console.log('*'.repeat(size));
};

这个函数需要一个数值型的参数 size 和一个字符串型的参数 header,它会在打印一个由指定数量的星号生成的框架,并在其中打印 header。

我们希望通过两种方式来使用这个文件:

  1. 从命令行直接运行:
1
~/learn-node $ node print-in-frame 8 Hello

在命令行传入 8 和 Hello 作为参数,它会打印出由 8 个星号组成的框架中的 “Hello”。

  1. 通过 require 来使用。假设所需要的模块会导出 printInFrame 函数,然后就可以这样做:
1
2
const print = require('./print-in-frame');
print(5, 'Hey');

它在由 5 个星号组成的框架中打印 “Hey”。

这是两种不同的使用方式。我们得想办法检测文件是独立运行的还是由其它脚本请求的。

这里用一个简单的 if 语句来解决:

1
2
3
if (require.main === module) {
// The file is being executed directly (not with require)
}

我们可以使用这个条件,以不同的方式调用 printInFrame 来满足需求:

1
2
3
4
5
6
7
8
9
10
11
12
// In print-in-frame.js
const printInFrame = (size, header) => {
console.log('*'.repeat(size));
console.log(header);
console.log('*'.repeat(size));
};

if (require.main === module) {
printInFrame(process.argv[2], process.argv[3]);
} else {
module.exports = printInFrame;
}

如果文件不是被请求的,我们使用 process.argv 来调用 printInFrame。否则,我们将 module.exports 修改为 printInFrame 引用。

所有模块都会被缓存

理解缓存很重要。我们用一个简单的示例来说明缓存。

假设有一个 ascii-art.js,可以打印炫酷的标头:

我们想每次请**求这个文件的时候都能看到这些标头,那么如果我们请求这个文件两次,期望会看到两次标头输出。

1
2
require('./ascii-art') // 会显示标头。
require('./ascii-art') // 不会显示标头。

因为模块缓存,第二次请求不会显示标头。Node 会在第一次调用的时候缓存文件,所以第二次调用的时候就不会重新加载了。

我们可以在第一次请求之后通过打印 require.cache 来看缓存的内容。缓存注册表只是一个简单的对象,它的每个属性对应着每次请求的模块。那些属性值是每个模块中的 module 对象。只需要从 require.cache 里删除某个属性就可以使对应的缓存失效。如果这样做,Node 会再次加载模块并再加将它加入缓存。

不过在现在这个情况下,这样做并不是一个高效的解决办法。简单的办法是在 ascii-art.js 中把输出语句包装为一个函数,然后导出它。用这个办法,我们请求 ascii-art.js 文件的时候会得到一个函数,然后每次执行这个函数都可以看到输出:

1
2
require('./ascii-art')() // 会显示标头。
require('./ascii-art')() // 也会显示标头。

以上,就是我这次要说的内容!

原文

https://www.oschina.net/translate/requiring-modules-in-node-js-everything-you-need-to-know

SQL注入

如何理解SQL注入(攻击)?

  1. SQL注入是一种将SQL代码添加到输入参数中,传递到服务器解析并执行的一种攻击手法。
  2. SQL注入攻击是输入参数未经过滤,然后直接拼接到SQL语句当中解析,执行达到预想之外的一种行为,称之为SQL注入攻击。

SQL注入是怎么产生的?

  1. WEB开发人员无法保证所有的输入都已经过滤
  2. 攻击者利用发送给SQL服务器的输入参数构造可执行的SQL代码(可加入到get请求、post请求、http头信息、cookie中)
  3. 数据库未做相应的安全配置

如何进行SQL注入攻击

要想发动sql注入攻击,就要知道正在使用的系统数据库,不然就没法提取重要的数据。
首先从Web应用技术上就给我们提供了判断的线索:

  • ASP和.NET:Microsoft SQL Server
  • PHP:MySQL、PostgreSQL
  • Java:Oracle、MySQL

Web容器也给我们提供了线索,比如安装IIS作为服务器平台,后台数据及很有可能是Microsoft SQL Server,而允许Apache和PHP的Linux服务器就很有可能使用开源的数据库,比如MySQL和PostgreSQL。

基于错误识别数据库

大多数情况下,要了解后台是什么数据库,只需要看一条详细的错误信息即可。比如判断我们事例中使用的数据库,我们加个单引号。

1
error:You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''' at line 1

从错误信息中,我们就可以发现是MySQL。

1
2
Microsoft OLE DB Provider for ODBC Drivers 错误 '80040e14'
[Microsoft][ODBC SQL Server Driver][SQL Server]Line 1:

上面错误信息可以发现是Microsoft SQL Server,如果错误信息开头是ORA,就可以判断数据库是Oracle,很简单,道理都是一样的,就不一一列举了。

UINON语句提取数据

UNION操作符可以合并两条或多条SELECT语句的查询结果,基本语法如下:

1
2
3
select column-1 column-2 from table-1
UNION
select column-1 column-2 from table-2

枚举数据库

我们只以MySQL数据库为例了,枚举数据库并提取数据遵循一种层次化的方法,首先我们提取数据库名称,然后提取表,再到列,最后才是数据本身。要想获取远程数据库的表、列,就要访问专门保存描述各种数据库结构的表。通常将这些结构描述信息成为元数据。在MySQL中,这些表都保存在information_schema数据库中

第一步:提取数据库

在MySQL中,数据库名存放在information_schema数据库下schemata表schema_name字段中

1
id=1 union select null,schema_name,null from information_schema.schemata

第二步:提取表名

在MySQL中,表名存放在information_schema数据库下tables表table_name字段中

1
?id=1 union select null,table_name,null from information_schema.tables where table_schema='ichunqiu'

第三步:提取字段名

在MySQL中,字段名存放在information_schema数据库下columns表column_name字段中

1
select null,column_name,null from information_schema.columns where table_name='users' and table_schema=''

第四步:提取数据
1
select username,password,null from users

SQL盲注

布尔型注入

例如:在参数后面加上or 1=1,可返回所有数据,因为 or 1=1永远为真

联合查询注入

在参数后面加上:

1
UNION select column-1 column-2 from table-2

多语句注入

在参数后面加上:

1
;drop table a; select * from tableb;

字符串注册

‘#’:’#’后所有的字符串都会被当成注释来处理用户名输入:user’#(单引号闭合user左边的单引号),密码随意输入,如:111,然后点击提交按钮。等价于SQL语句:

1
SELECT * FROM user WHERE username = 'user'#'ADN password = '111'

如何预防SQL注入

  1. 严格检查输入变量的类型和格式
    对于整数参数,加判断条件:不能为空、参数类型必须为数字
    对于字符串参数,可以使用正则表达式进行过滤:如:必须为[0-9a-zA-Z]范围内的字符串
  2. 对URL进行编码
  3. 对进入数据库的特殊字符(’”\尖括号&*;等)进行转义处理,或编码转换
  4. 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到SQL语句中,即不要直接拼接SQL语句
  5. 避免网站打印出SQL错误信息,比如类型错误、字段不匹配等,把代码里的SQL语句暴露出来,以防止攻击者利用这些错误信息进行SQL注入。
  6. 利用sql的预编译机制
    把sql语句的模板(变量采用占位符进行占位)发送给数据库服务器,数据库服务器对sql语句的模板进行编译,编译之后根据语句的优化分析对相应的索引进行优化,在最终绑定参数时把相应的参数传送给数据库服务器,直接进行执行,节省了sql查询时间,以及数据库服务器的资源,达到一次编译、多次执行的目的,除此之外,还可以防止SQL注入。具体是怎样防止SQL注入的呢?实际上当将绑定的参数传到数据库服务器,服务器对参数进行编译,即填充到相应的占位符的过程中,做了转义操作。

参考

https://blog.csdn.net/github_36032947/article/details/78442189
https://www.jianshu.com/p/ba35a7e1c67d
https://paper.seebug.org/15/
https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/09.4.md

CSP简介

跨域脚本攻击 XSS是最常见、危害最大的网页安全漏洞。为了防止它们,要采取很多编程措施,非常麻烦。很多人提出,能不能根本上解决问题,浏览器自动禁止外部注入恶意脚本?这就是”网页安全政策”(Content Security Policy,缩写 CSP)的来历。

一、简介

CSP 的实质就是白名单制度,开发者明确告诉客户端,哪些外部资源可以加载和执行,等同于提供白名单。它的实现和执行全部由浏览器完成,开发者只需提供配置。

CSP 大大增强了网页的安全性。攻击者即使发现了漏洞,也没法注入脚本,除非还控制了一台列入了白名单的可信主机。

两种方法可以启用 CSP。一种是通过 HTTP 头信息的Content-Security-Policy的字段。

1
2
Content-Security-Policy: script-src 'self'; object-src 'none';
style-src cdn.example.org third-party.org; child-src https:

另一种是通过网页的标签。

1
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:">

上面代码中,CSP 做了如下配置。

  • 脚本:只信任当前域名
  • <object>标签:不信任任何URL,即不加载任何资源
  • 样式表:只信任cdn.example.orgthird-party.org
  • 框架(frame):必须使用HTTPS协议加载
  • 其他资源:没有限制

启用后,不符合 CSP 的外部资源就会被阻止加载。

二、限制选项

CSP 提供了很多限制选项,涉及安全的各个方面。

2.1 资源加载限制

以下选项限制各类资源的加载。

  • script-src:外部脚本
  • style-src:样式表
  • img-src:图像
  • media-src:媒体文件(音频和视频)
  • font-src:字体文件
  • object-src:插件(比如 Flash)
  • child-src:框架
  • frame-ancestors:嵌入的外部资源(比如<frame><iframe><embed><applet>
  • connect-src:HTTP 连接(通过 XHR、WebSockets、EventSource等)
  • worker-srcworker脚本
  • manifest-src:manifest 文件

2.2 default-src

default-src用来设置上面各个选项的默认值。

1
Content-Security-Policy: default-src 'self'

上面代码限制所有的外部资源,都只能从当前域名加载。

如果同时设置某个单项限制(比如font-src)和default-src,前者会覆盖后者,即字体文件会采用font-src的值,其他资源依然采用default-src的值。

2.3 URL 限制

有时,网页会跟其他 URL 发生联系,这时也可以加以限制。

  • frame-ancestors:限制嵌入框架的网页
  • base-uri:限制<base#href>
  • form-action:限制<form#action>

2.4 其他限制

其他一些安全相关的功能,也放在了 CSP 里面。

  • block-all-mixed-content:HTTPS 网页不得加载 HTTP 资源(浏览器已经默认开启)
  • upgrade-insecure-requests:自动将网页上所有加载外部资源的 HTTP 链接换成 HTTPS 协议
  • plugin-types:限制可以使用的插件格式
  • sandbox:浏览器行为的限制,比如不能有弹出窗口等。

2.5 report-uri

有时,我们不仅希望防止 XSS,还希望记录此类行为。report-uri就用来告诉浏览器,应该把注入行为报告给哪个网址。

1
Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

上面代码指定,将注入行为报告给/my_amazing_csp_report_parser这个 URL。

浏览器会使用POST方法,发送一个JSON对象,下面是一个例子。

1
2
3
4
5
6
7
8
9
{
"csp-report": {
"document-uri": "http://example.org/page.html",
"referrer": "http://evil.example.com/",
"blocked-uri": "http://evil.example.com/evil.js",
"violated-directive": "script-src 'self' https://apis.google.com",
"original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser"
}
}

img

三、Content-Security-Policy-Report-Only

除了Content-Security-Policy,还有一个Content-Security-Policy-Report-Only字段,表示不执行限制选项,只是记录违反限制的行为。
它必须与report-uri选项配合使用。

1
Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

四、选项值

每个限制选项可以设置以下几种值,这些值就构成了白名单。

  • 主机名:example.orghttps://example.com:443
  • 路径名:example.org/resources/js/
  • 通配符:*.example.org*://*.example.com:*(表示任意协议、任意子域名、任意端口)
  • 协议名:https:data:
  • 关键字'self':当前域名,需要加引号
  • 关键字'none':禁止加载任何外部资源,需要加引号

多个值也可以并列,用空格分隔。

1
Content-Security-Policy: script-src 'self' https://apis.google.com

如果同一个限制选项使用多次,只有第一次会生效。

1
2
3
4
5
# 错误的写法
script-src https://host1.com; script-src https://host2.com

# 正确的写法
script-src https://host1.com https://host2.com

如果不设置某个限制选项,就是默认允许任何值。

五、script-src 的特殊值

除了常规值,script-src还可以设置一些特殊值。注意,下面这些值都必须放在单引号里面。

  • ‘unsafe-inline’:允许执行页面内嵌的&lt;script>标签和事件监听函数

  • unsafe-eval:允许将字符串当作代码执行,比如使用evalsetTimeoutsetIntervalFunction等函数。

  • nonce值:每次HTTP回应给出一个授权token,页面内嵌脚本必须有这个token,才会执行

  • hash值:列出允许执行的脚本代码的Hash值,页面内嵌脚本的哈希值只有吻合的情况下,才能执行。

以 Node.js 为例, 计算脚本的 hashes 值:

1
2
3
4
5
6
7
const crypto = require('crypto');

function getHashByCode(code, algorithm = 'sha256') {
return algorithm + '-' + crypto.createHash(algorithm).update(code, 'utf8').digest("base64");
}

getHashByCode('console.log("hello world");'); // 'sha256-wxWy1+9LmiuOeDwtQyZNmWpT0jqCUikqaqVlJdtd

nonce值的例子如下,服务器发送网页的时候,告诉浏览器一个随机生成的token。

1
Content-Security-Policy: script-src 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa'

页面内嵌脚本,必须有这个token才能执行。

1
2
3
<script nonce=EDNnf03nceIOfn39fn3e9h3sdfa>
// some code
</script>

hash值的例子如下,服务器给出一个允许执行的代码的hash值。

1
Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='

下面的代码就会允许执行,因为hash值相符。

1
<script>alert('Hello, world.');</script>

注意,计算hash值的时候,

XSS攻击及预防

XSS攻击是什么

  • XSS又称CSS,全称Cross SiteScript跨站脚本攻击的缩写,是一种网站应用程序的安全漏洞攻击,是代码注入的一种。
  • 通常是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。
  • 这些恶意网页程序通常是JavaScript,但实际上也可以包括Java,VBScript,ActiveX,Flash或者甚至是普通的HTML。
  • 攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和盗取用户Cookie、破坏页面结构、重定向到其它网站等

XSS攻击基本原理——代码注入

在web的世界里有各种各样的语言,于是乎对于语句的解析大家各不相同,有一些语句在一种语言里是合法的,但是在另外一种语言里是非法的。这种二义性使得黑客可以用代码注入的方式进行攻击——将恶意代码注入合法代码里隐藏起来,再诱发恶意代码,从而进行各种各样的非法活动。只要破坏跨层协议的数据/指令的构造,我们就能攻击。
历史悠久的SQL注入XSS注入都是这种攻击方式的典范。现如今,随着参数化查询的普及,我们已经离SQL注入很远了。但是,历史同样悠久的XSS却没有远离我们。
XSS的基本实现思路很简单——比如持久型XSS通过一些正常的站内交互途径,例如发布评论,提交含有JavaScript的内容文本。这时服务器端如果没有过滤或转义掉这些脚本,作为内容发布到了页面上,其他用户访问这个页面的时候就会运行这些脚本,从而被攻击。

攻击分类举例

DOM-based XSS

基于DOM的XSS,通过对具体DOM代码进行分析,根据实际情况构造dom节点进行XSS跨站脚本攻击,该攻击特点是中招的人是少数人。
场景一
当我登录a.com后,我发现它的页面某些内容是根据url中的一个叫content参数直接显示的,猜测它测页面处理可能是这样,其它语言类似:

1
2
3
4
5
6
7
8
9
10
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPEhtmlPUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>XSS测试</title>
</head>
<body>
页面内容:<%=request.getParameter("content")%>
</body>
</html>

我知道了Tom也注册了该网站,并且知道了他的邮箱(或者其它能接收信息的联系方式),我做一个超链接发给他,超链接地址为:http://www.a.com?content=,当Tom点击这个链接的时候(假设他已经登录a.com),浏览器就会直接打开b.com,并且把Tom在a.com中的cookie信息发送到b.com,b.com是我搭建的网站,当我的网站接收到该信息时,我就盗取了Tom在a.com的cookie信息,cookie信息中可能存有登录密码,攻击成功!这个过程中,受害者只有Tom自己。那当我在浏览器输入a.com?content=,浏览器展示页面内容的过程中,就会执行我的脚本,页面输出xss字样,这是攻击了我自己,那我如何攻击别人并且获利呢?

持久型XSS

也叫存储型XSS——主动提交恶意数据到服务器,攻击者在数据中嵌入代码,这样当其他用户请求后,服务器从数据库中查询数据并发给用户,用户浏览此类页面时就可能受到攻击。由于其攻击代码已经存储到服务器上或者数据库中,所以受害者是很多人。可以描述为:恶意用户的HTML或JS输入服务器->进入数据库->服务器响应时查询数据库->用户浏览器。

场景二
a.com可以发文章,我登录后在a.com中发布了一篇文章,文章中包含了恶意代码,,保存文章。这时Tom和Jack看到了我发布的文章,当在查看我的文章时就都中招了,他们的cookie信息都发送到了我的服务器上,攻击成功!这个过程中,受害者是多个人。Stored XSS漏洞危害性更大,危害面更广。

反射型XSS

反射性XSS,也就是被动的非持久性XSS。诱骗用户点击URL带攻击代码的链接,服务器解析后响应,在返回的响应内容中隐藏和嵌入攻击者的XSS代码,被浏览器执行,从而攻击用户。
URL可能被用户怀疑,但是可以通过短网址服务将之缩短,从而隐藏自己。

使用XSS Filter

  • 输入过滤,所有用户输入都是不可信的。”(注意: 攻击代码不一定在中),对用户提交的数据进行有效性验证,仅接受指定长度范围内并符合我们期望格式的的内容提交,阻止或者忽略除此外的其他任何数据。

    | less-than character (<) | < |
    | ———————————————————— | ——————————————————– |
    | greater-than character (>) | > |
    | ampersand character (&) | & |
    | double-quote character (“) | " |
    | space character( ) |   |
    | Any ASCII code character whose code is greater-than or equal to 0x80 | &#, where is the ASCII character value. |

比如用户输入:,保存后最终存储的会是<script>window.location.href="http://www.baidu.com"</script>在展现时浏览器会对这些字符转换成文本内容显示,而不是一段可执行的代码。

  • 输出转义,当需要将一个字符串输出到Web网页时,同时又不确定这个字符串中是否包括XSS特殊字符,为了确保输出内容的完整性和正确性,输出HTML属性时可以使用HTML转义编码(HTMLEncode)进行处理,输出到

CSRF攻击及预防

什么是CSRF?

CSRF(Cross-Site Request Forgery,跨站点伪造请求,也被称为:one click attack/session riding,缩写为:CSRF/XSRF)是一种网络攻击方式,该攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在未授权的情况下执行在权限保护之下的操作,具有很大的危害性。具体来讲,可以这样理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

CSRF原理

下图简单阐述了CSRF的原理:

从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成以下两个步骤:

  • 登录受信任网站A,并在本地生成Cookie。
  • 在不登出A的情况下,访问危险网站B。

看到这里,你也许会问:“如果我不满足以上两个条件中的一个,我就不会受到CSRF攻击”。是滴,确实如此,但是你不能保证以下情况不会发生:

  • 你不能保证你登录了一个网站之后,不再打开一个tab页面并访问其它的网站。
  • 你不能保证你关闭浏览器之后,你本地的Cookie立刻过期,你上次的会话已经结束。
  • 上述中所谓的攻击网站,可能就是一个钓鱼网站。

CSRF攻击实例

CSRF 攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在并未授权的情况下执行在权限保护之下的操作。比如说,受害者 Bob 在银行有一笔存款,通过对银行的网站发送请求 http://bank.example/withdraw?account=bob&amount=1000000&for=bob2 可以使 Bob 把 1000000 的存款转到 bob2 的账号下。通常情况下,该请求发送到网站后,服务器会先验证该请求是否来自一个合法的 session,并且该 session 的用户 Bob 已经成功登陆。黑客 Mallory 自己在该银行也有账户,他知道上文中的 URL 可以把钱进行转帐操作。Mallory 可以自己发送一个请求给银行:http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory。但是这个请求来自 Mallory 而非 Bob,他不能通过安全认证,因此该请求不会起作用。这时,Mallory 想到使用 CSRF 的攻击方式,他先自己做一个网站,在网站中放入如下代码: src=”http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory ”,并且通过广告等诱使 Bob 来访问他的网站。当 Bob 访问该网站时,上述 url 就会从 Bob 的浏览器发向银行,而这个请求会附带 Bob 浏览器中的 cookie 一起发向银行服务器。大多数情况下,该请求会失败,因为他要求 Bob 的认证信息。但是,如果 Bob 当时恰巧刚访问他的银行后不久,他的浏览器与银行网站之间的 session 尚未过期,浏览器的 cookie 之中含有 Bob 的认证信息。这时,悲剧发生了,这个 url 请求就会得到响应,钱将从 Bob 的账号转移到 Mallory 的账号,而 Bob 当时毫不知情。等以后 Bob 发现账户钱少了,即使他去银行查询日志,他也只能发现确实有一个来自于他本人的合法请求转移了资金,没有任何被攻击的痕迹。而 Mallory 则可以拿到钱后逍遥法外。

CSRF如何防御

验证 HTTP Referer 字段

根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。在通常情况下,访问一个安全受限页面的请求来自于同一个网站,比如需要访问 http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用户必须先登陆 bank.example,然后通过点击页面上的按钮来触发转账事件。这时,该转帐请求的 Referer 值就会是转账按钮所在的页面的 URL,通常是以 bank.example 域名开头的地址。而如果黑客要对银行网站实施 CSRF 攻击,他只能在他自己的网站构造请求,当用户通过黑客的网站发送请求到银行时,该请求的 Referer 是指向黑客自己的网站。因此,要防御 CSRF 攻击,银行网站只需要对于每一个转账请求验证其 Referer 值,如果是以 bank.example 开头的域名,则说明该请求是来自银行网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求。

这种方法的显而易见的好处就是简单易行,网站的普通开发人员不需要操心 CSRF 的漏洞,只需要在最后给所有安全敏感的请求统一增加一个拦截器来检查 Referer 的值就可以。特别是对于当前现有的系统,不需要改变当前系统的任何已有代码和逻辑,没有风险,非常便捷。

然而,这种方法并非万无一失。Referer 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。事实上,对于某些浏览器,比如 IE6 或 FF2,目前已经有一些方法可以篡改 Referer 值。如果 bank.example 网站支持 IE6 浏览器,黑客完全可以把用户浏览器的 Referer 值设为以 bank.example 域名开头的地址,这样就可以通过验证,从而进行 CSRF 攻击。

即便是使用最新的浏览器,黑客无法篡改 Referer 值,这种方法仍然有问题。因为 Referer 值会记录下用户的访问来源,有些用户认为这样会侵犯到他们自己的隐私权,特别是有些组织担心 Referer 值会把组织内网中的某些信息泄露到外网中。因此,用户自己可以设置浏览器使其在发送请求时不再提供 Referer。当他们正常访问银行网站时,网站会因为请求没有 Referer 值而认为是 CSRF 攻击,拒绝合法用户的访问。

添加token验证

CSRF攻击之所以能够成功,是因为攻击者可以完全伪造用户的请求,该请求中所有的用户验证信息都存在cookie中,因此攻击者可以在不知道这些验证信息的情况下直接利用用户自己的cookie来通过安全验证。要防止CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于cookie之中。可以在HTTP请求中以参数的形式加入一个随机产生的token,并在服务器建立一个拦截器来验证这个token,如果请求中没有token或者token不正确,则认为可能是CSRF攻击而拒绝该请求。
现在业界一致的做法就是使用Anti CSRF Token来防御CSRF。

  • 用户访问某个表单页面。
  • 服务端生成一个Token,放在用户的Session中,或者浏览器的Cookie中。
  • 在页面表单附带上Token参数。
  • 用户提交请求后,服务端验证表单中的Token是否与用户Session(或Cookies)中的Token一致,一致为合法请求,不是则非法请求。

这个Token值必须是随机的,不可预测的。由于Token的存在,攻击者无法再构造一个带有合法Token的请求实施CSRF攻击。另外使用Token应注意Token的保密性,尽量把敏感操作由GET改成POST,以form或者AJAX形式提交,避免Token泄露。

验证码

验证码,强制用户必须与应用进行交互,才能完成最终请求。通常情况下,验证码能够很好的遏制CSRF攻击。但是出于用户体验考虑,网站不能给所有的操作都加上验证码。因此验证码只能作为一种辅助手段。

尽量使用POST,限制GET

GET接口能够直接将请求地址暴露给攻击者,所以要防止CSRF一定最好不要用GET。当然POST并不是万无一失,攻击者只需要构造一个form表单就可以,但需要在第三方页面做,这样就增加了暴露的可能性。

在HTTP头部添加自定义属性

这种方法也是使用token并验证,但是它是把token放在HTTP请求头部中。通过使用AJAX我们可以在我们的请求头部中添加我们的自定义属性,但是这种方法要求我们将整个站的请求全部改成AJAX,如果是新站还好,老站的话无疑是需要重写整个站点的,这是很不可取的。

参考:

https://www.cnblogs.com/cxying93/p/6035031.html
https://www.ibm.com/developerworks/cn/web/1102_niugang_csrf/index.html
https://www.imooc.com/article/18069

Node.js中的crypto模块

一、 概述

互联网时代,网络上的数据量每天都在以惊人的速度增长。同时,各类网络安全问题层出不穷。在信息安全重要性日益凸显的今天,作为一名开发者,需要加强对安全的认识,并通过技术手段增强服务的安全性。

crypto模块是nodejs的核心模块之一,它提供了安全相关的功能,如摘要运算、加密、电子签名等。很多初学者对着长长的API列表,不知如何上手,因此它背后涉及了大量安全领域的知识。

本文重点讲解API背后的理论知识,主要包括如下内容:

  1. 摘要(hash)、基于摘要的消息验证码(HMAC)
  2. 对称加密、非对称加密、电子签名
  3. 分组加密模式

二、摘要(hash)

摘要(digest):将长度不固定的消息作为输入,通过运行hash函数,生成固定长度的输出,这段输出就叫做摘要。通常用来验证消息完整、未被篡改。

摘要运算是不可逆的。也就是说,输入固定的情况下,产生固定的输出。但知道输出的情况下,无法反推出输入。

伪代码如下。

1
digest = Hash(message)

常见的摘要算法 与 对应的输出位数如下:

  • MD5:128位
  • SHA-1:160位
  • SHA256 :256位
  • SHA512:512位

nodejs中的例子:

1
2
3
4
5
6
7
8
9
var crypto = require('crypto');
var md5 = crypto.createHash('md5');

var message = 'hello';
var digest = md5.update(message, 'utf8').digest('hex');

console.log(digest);
// 输出如下:注意这里是16进制
// 5d41402abc4b2a76b9719d911017c592

备注:在各类文章或文献中,摘要、hash、散列 这几个词经常会混用,导致不少初学者看了一脸懵逼,其实大部分时候指的都是一回事,记住上面对摘要的定义就好了。

三、MAC、HMAC

MAC(Message Authentication Code):消息认证码,用以保证数据的完整性。运算结果取决于消息本身、秘钥。

MAC可以有多种不同的实现方式,比如HMAC。

HMAC(Hash-based Message Authentication Code):可以粗略地理解为带秘钥的hash函数。

nodejs例子如下:

1
2
3
4
5
6
7
8
9
const crypto = require('crypto');

// 参数一:摘要函数
// 参数二:秘钥
let hmac = crypto.createHmac('md5', '123456');
let ret = hmac.update('hello').digest('hex');

console.log(ret);
// 9c699d7af73a49247a239cb0dd2f8139

四、对称加密、非对称加密

加密/解密:给定明文,通过一定的算法,产生加密后的密文,这个过程叫加密。反过来就是解密。

1
2
encryptedText = encrypt( plainText )
plainText = decrypt( encryptedText )

秘钥:为了进一步增强加/解密算法的安全性,在加/解密的过程中引入了秘钥。秘钥可以视为加/解密算法的参数,在已知密文的情况下,如果不知道解密所用的秘钥,则无法将密文解开。

1
2
encryptedText = encrypt(plainText, encryptKey)
plainText = decrypt(encryptedText, decryptKey)

根据加密、解密所用的秘钥是否相同,可以将加密算法分为对称加密非对称加密

1、对称加密

加密、解密所用的秘钥是相同的,即encryptKey === decryptKey

常见的对称加密算法:DES、3DES、AES、Blowfish、RC5、IDEA。

加、解密伪代码:

1
2
encryptedText = encrypt(plainText, key); // 加密
plainText = decrypt(encryptedText, key); // 解密

2、非对称加密

又称公开秘钥加密。加密、解密所用的秘钥是不同的,即encryptKey !== decryptKey

加密秘钥公开,称为公钥。解密秘钥保密,称为秘钥。

常见的非对称加密算法:RSA、DSA、ElGamal。

加、解密伪代码:

1
2
encryptedText = encrypt(plainText, publicKey);  // 加密
plainText = decrypt(encryptedText, priviteKey); // 解密

3、对比与应用

除了秘钥的差异,还有运算速度上的差异。通常来说:

  1. 对称加密速度要快于非对称加密。
  2. 非对称加密通常用于加密短文本,对称加密通常用于加密长文本。

两者可以结合起来使用,比如HTTPS协议,可以在握手阶段,通过RSA来交换生成对称秘钥。在之后的通讯阶段,可以使用对称加密算法对数据进行加密,秘钥则是握手阶段生成的。

备注:对称秘钥交换不一定通过RSA,还可以通过类似DH来完成,这里不展开。

五、数字签名

签名大致可以猜到数字签名的用途。主要作用如下:

  1. 确认信息来源于特定的主体。
  2. 确认信息完整、未被篡改。

为了达到上述目的,需要有两个过程:

  1. 发送方:生成签名。
  2. 接收方:验证签名。

1、发送方生成签名

  1. 计算原始信息的摘要。
  2. 通过私钥对摘要进行签名,得到电子签名。
  3. 将原始信息、电子签名,发送给接收方。

附:签名伪代码

1
2
digest = hash(message); // 计算摘要
digitalSignature = sign(digest, priviteKey); // 计算数字签名

2、接收方验证签名

  1. 通过公钥解开电子签名,得到摘要D1。(如果解不开,信息来源主体校验失败)
  2. 计算原始信息的摘要D2。
  3. 对比D1、D2,如果D1等于D2,说明原始信息完整、未被篡改。

附:签名验证伪代码

1
2
3
digest1 = verify(digitalSignature, publicKey); // 获取摘要
digest2 = hash(message); // 计算原始信息的摘要
digest1 === digest2 // 验证是否相等

3、对比非对称加密

由于RSA算法的特殊性,加密/解密、签名/验证 看上去特别像,很多同学都很容易混淆。先记住下面结论,后面有时间再详细介绍。

  1. 加密/解密:公钥加密,私钥解密。
  2. 签名/验证:私钥签名,公钥验证。

六、分组加密模式、填充、初始化向量

常见的对称加密算法,如AES、DES都采用了分组加密模式。这其中,有三个关键的概念需要掌握:模式、填充、初始化向量。

搞清楚这三点,才会知道crypto模块对称加密API的参数代表什么含义,出了错知道如何去排查。

1、分组加密模式

所谓的分组加密,就是将(较长的)明文拆分成固定长度的块,然后对拆分的块按照特定的模式进行加密。

常见的分组加密模式有:ECB(不安全)、CBC(最常用)、CFB、OFB、CTR等。

以最简单的ECB为例,先将消息拆分成等分的模块,然后利用秘钥进行加密。


更多关于分组加密模式的介绍可以参考 wiki

后面假设每个块的长度为128位

2、初始化向量:IV

为了增强算法的安全性,部分分组加密模式(CFB、OFB、CTR)中引入了初始化向量(IV),使得加密的结果随机化。也就是说,对于同一段明文,IV不同,加密的结果不同。

以CBC为例,每一个数据块,都与前一个加密块进行亦或运算后,再进行加密。对于第一个数据块,则是与IV进行亦或。

IV的大小跟数据块的大小有关(128位),跟秘钥的长度无关。

3、填充:padding

分组加密模式需要对长度固定的块进行加密。分组拆分完后,最后一个数据块长度可能小于128位,此时需要进行填充以满足长度要求。

填充方式有多重。常见的填充方式有PKCS7

假设分组长度为k字节,最后一个分组长度为k-last,可以看到:

  1. 不管明文长度是多少,加密之前都会会对明文进行填充 (不然解密函数无法区分最后一个分组是否被填充了,因为存在最后一个分组长度刚好等于k的情况)
  2. 如果最后一个分组长度等于k-last === k,那么填充内容为一个完整的分组 k k k … k (k个字节)
  3. 如果最后一个分组长度小于k-last < k,那么填充内容为 k-last mod k
1
2
3
4
5
6
         01 -- if lth mod k = k-1
02 02 -- if lth mod k = k-2
.
.
.
k k ... k k -- if lth mod k = 0

概括来说

  1. 分组加密:先将明文切分成固定长度的块(128位),再进行加密。
  2. 分组加密的几种模式:ECB(不安全)、CBC(最常用)、CFB、OFB、CTR。
  3. 填充(padding):部分加密模式,当最后一个块的长度小于128位时,需要通过特定的方式进行填充。(ECB、CBC需要填充,CFB、OFB、CTR不需要填充)
  4. 初始化向量(IV):部分加密模式(CFB、OFB、CTR)会将 明文块 与 前一个密文块进行亦或操作。对于第一个明文块,不存在前一个密文块,因此需要提供初始化向量IV(把IV当做第一个明文块 之前的 密文块)。此外,IV也可以让加密结果随机化。

七、相关链接

Nodejs学习笔记

Cryptographic hash function

Hash-based message authentication code

HMAC vs MAC functions

What is the difference between MAC and HMAC?

Block cipher mode of operation

RSA的公钥和私钥到底哪个才是用来加密和哪个用来解密? - 刘巍然-学酥的回答 - 知乎

原文:https://www.cnblogs.com/chyingp/p/nodejs-learning-crypto-theory.html

数字证书的基础知识

在讲数字证书之前必须要讲非对称加密算法摘要算法,因为数字证书的基础就是各种加解密算法(非对称加密、摘要算法),而其中的核心就是非对称加密算法了。目前而言加密方法可以分为两大类。一类是单钥加密(private key cryptography)也可以称为对称加密,还有一类叫做双钥加密(public key cryptography)也可称为非对称加密。前者的加密和解密过程都用同一套密码,后者的加密和解密过程用的是不同的密码。下面来看看对称加密、非对称加密以及摘要算法,他们是怎样应用在数字证书中的。

对称加密

对称加密(也叫私钥加密)指加密和解密使用相同密钥的加密算法。有时又叫传统密码算法,就是加密密钥能够从解密密钥中推算出来,同时解密密钥也可以从加密密钥中推算出来。而在大多数的对称算法中,加密密钥和解密密钥是相同的,所以也称这种加密算法为秘密密钥算法或单密钥算法。

在应用该算法时,它要求发送方和接收方在安全通信之前,商定一个密钥。对称算法的安全性依赖于密钥,泄漏密钥就意味着任何人都可以对他们发送或接收的消息解密,所以密钥的保密性对通信性至关重要。对称加密算法的特点是算法公开、计算量小、加密速度快、加密效率高。对称加密有很多种算法,由于它效率很高,所以被广泛使用在很多加密协议的核心当中。不足之处是,交易双方都使用同样钥匙,安全性得不到保证。

常见的对称加密算法

  • DES:数据加密标准(DES,Data Encryption Standard)是一种使用密钥加密的块密码,它基于使用56位密钥的对称算法,这个算法因为包含一些机密设计元素,相对短的密钥长度以及被怀疑内含美国国家安全局(NSA)的后门而在开始时是有争议的,DES现在已经不被视为一种安全的加密算法,主要因为它使用的56位密钥过短,导致容易被破解。为了提供实用所需的安全性,可以使用DES的派生算法3DES来进行加密,虽然3DES也存在理论上的攻击方法。
  • AES:高级加密标准(英语:Advanced Encryption Standard,缩写:AES),这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用,2006年,高级加密标准已然成为对称密钥加密中最流行的算法之一。AES的区块长度固定为128比特,密钥长度则可以是128,192或256比特
  • RC4:RC4加密算法是大名鼎鼎的RSA三人组中的头号人物Ronald Rivest在1987年设计的密钥长度可变的流加密算法簇。该算法的速度可以达到DES加密的10倍左右,且具有很高级别的非线性。RC4起初是用于保护商业机密的。但是在1994年9月,它的算法被发布在互联网上,也就不再有什么商业机密了。
  • IDEA:是旅居瑞士中国青年学者来学嘉和著名密码专家J.Massey于1990年提出的。它在1990年正式公布并在以后得到增强。这种算法是在DES算法的基础上发展出来的,类似于三重DES,和DES一样IDEA也是属于对称密钥算法。发展IDEA也是因为感到DES具有密钥太短等缺点,已经过时。IDEA的密钥为128位,这么长的密钥在今后若干年内应该是安全的。

非对称加密

与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey);并且加密密钥和解密密钥是成对出现的。非对称加密算法在加密和解密过程使用了不同的密钥,非对称加密也称为公钥加密,在密钥对中,其中一个密钥是对外公开的,所有人都可以获取到,称为公钥,其中一个密钥是不公开的称为私钥。

非对称加密的特性

  • 对于一个公钥,有且只有一个对应的私钥。
  • 公钥是公开的,并且不能通过公钥反推出私钥。
  • 通过私钥加密的密文只能通过公钥能解密,通过公钥加密的密文也只能通过私钥能解密。

通过公钥是极难推算出私钥的,只能通过穷举,所以只要密钥足够长,要想从公钥推算出私钥几乎不可能的。

非对称加密的主要用途

  • 对信息保密,防止中间人攻击:将明文使用公钥加密,传输给接收者,这样确保信息只能被有私钥的拥有者解密,其他人无法获得明文信息,因为没有私钥无法进行解密。该方法一般用于交换对称密钥
  • 身份验证和防止信息篡改:私钥拥有者使用私钥加密一段授权明文,并将授权明文和加密后的密文,以及公钥一并发送出来,接收方只需要通过公钥将密文解密后与授权明文对比是否一致,就可以判断明文在中途是否被篡改过。此方法用于数字签名

常见的非对称加密算法

  • RSA:1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字命名,叫做RSA算法。从那时直到现在,RSA算法一直是最广为使用的”非对称加密算法”。毫不夸张地说,只要有计算机网络的地方,就有RSA算法。这种算法非常可靠,密钥越长,它就越难破解。根据已经披露的文献,目前被破解的最长RSA密钥是768个二进制位。也就是说,长度超过768位的密钥,还无法破解(至少没人公开宣布)。因此可以认为,1024位的RSA密钥基本安全,2048位的密钥极其安全。适用于数字签名和密钥交换。 该算法是目前应用最广泛的公钥加密算法,特别适用于通过 Internet 传送的数据。
  • DSA:数字签名算法 (Digital Signature Algorithm, DSA) 由美国国家安全署 (United States National Security Agency, NSA) 发明,已作为数字签名的标准。在DSA数字签名和认证中,发送者使用自己的私钥对文件或消息进行签名,接受者收到消息后使用发送者的公钥来验证签名的真实性。DSA只是一种算法,和RSA不同之处在于它不能用作加密和解密,也不能进行密钥交换,只用于签名,它比RSA要快很多。DSA 算法的安全性取决于自计算离散算法的困难。这种算法,不适用于数据加密,仅适用于数字签名。
  • Diffie-Hellman:一种确保共享KEY安全穿越不安全网络的方法。Whitefield与Martin Hellman在1976年提出了一个奇妙的密钥交换协议,称为Diffie-Hellman密钥交换协议/算法(Diffie-Hellman Key Exchange/Agreement Algorithm)。这个机制的巧妙在于需要安全通信的双方可以用这个方法确定对称密钥。然后可以用这个密钥进行加密和解密。但是注意,这个密钥交换协议/算法只能用于密钥的交换,而不能进行消息的加密和解密。双方确定要用的密钥后,要使用其他对称密钥操作加密算法实际加密和解密消息。该算法仅适用于密钥交换。
  • ECC:椭圆加密算法(ECC)是一种公钥加密体制,最初由Koblitz和Miller两人于1985年提出,与经典的RSA,DSA等公钥密码体制相比,椭圆密码体制有以下优点:160位的椭圆密钥与1024位的RSA密钥安全性相同;在私钥的加密解密速度上,ECC算法比RSA、DSA速度更快;存储空间占用小;带宽要求低;ECC算法的数学理论非常深奥和复杂,在工程应用中比较难于实现,但它的单位安全强度相对较高。

非对称加密算法可能是世界上最重要的算法,它是当今电子商务等领域的基石。非对称加密算法如此强大可靠,却有一个弊端,就是加解密比较耗时。因此,在实际使用中,往往与对称加密和摘要算法结合使用。列如在实体之间交换对称密钥时,或者在签署一封邮件的散列时(数字签名)。

散列是通过应用一种单向数学函数获得的一个定长结果,对于数据而言,叫做散列算法。

摘要算法

摘要算法是一个神奇的算法,也称为散列或者散列值。是一种与基于密钥(对称密钥或公钥)的加密不同的数据转换类型。散列就是通过把一个叫做散列算法的单向数学函数应用于数据,将任意长度的一块数据转换为一个定长的、不可逆转的数字,其长度通常在128~256位之间。所产生的散列值的长度应足够长,因此使找到两块具有相同散列值的数据的机会很少。

摘要算法具有以下特性:

  • 只要源文本不同,计算得到的结果,必然不同(或者说机会很少)。
  • 无法从结果反推出源数据(那是当然的,不然就能量不守恒了)。

常见的摘要算法:

  • MD5:是RSA数据安全公司开发的一种单向散列算法,MD5被广泛使用,可以用来把不同长度的数据块进行暗码运算成一个固定位位的数值(通常是128位)。
  • SHA-1:与 DSA 公钥算法相似,安全散列算法1(SHA-1)也是由 NSA 设计的,并由 NIST 将其收录到 FIPS 中,作为散列数据的标准。它可产生一个 160 位的散列值。SHA-1 是流行的用于创建数字签名的单向散列算法。
  • MAC(Message Authentication Code):消息认证代码,是一种使用密钥的单向函数,可以用它们在系统上或用户之间认证文件或消息,常见的是HMAC(用于消息认证的密钥散列算法)。
  • CRC(Cyclic Redundancy Check):循环冗余校验码,CRC校验由于实现简单,检错能力强,被广泛使用在各种数据校验应用中。占用系统资源少,用软硬件均能实现,是进行数据传输差错检测地一种很好的手段(CRC 并不是严格意义上的散列算法,但它的作用与散列算法大致相同,所以归于此类)。

摘要算法用于对比信息源是否一致,因为只要源数据发生变化,得到的摘要必然不同。因为通常结果比源数据要短很多,所以称为“摘要”。

应用场景,如发件人生成邮件的散列值并加密它,然后将它与邮件本身一起发送。而收件人同时解密邮件和散列值,并由接收到的邮件产生另外一个散列值,然后将两个散列值进行比较。如果两者相同,邮件极有可能在传输期间没有发生任何改变。

数字签名

数字签名就是对非对称加密和摘要算法的一种应用,能够确保信息在发布后不会被篡改(摘要算法特性),保证数据的完整性和可信性;同时也可以防止数据被他人伪造(非对称加密算法特性);列如,我们有一段授权文本需要发布时,为了防止中途篡改发布的内容,保证发布文本的完整性,以及文本是由指定的发布者发布的。那么,可以通过摘要算法得到发布内容的摘要,得到摘要之后,发布者使用私钥加密得到密文(签名),这时候将源文本、密文(签名)以及公钥一起发布出去即可。

验证过程为:首先验证公钥是否是发布者的公钥,然后用公钥对密文进行解密,得到摘要,使用发布者对文本同样的摘要算法得到摘要文本,比对摘要是否一致即可确认信息是否被篡改或者是指定发布者发布的。

数字签名可以快速验证文本的完整性和合法性,已广泛应用于各个领域。

公钥的验证在后续数字证书的授权链中提到验证方法。

数字证书

现实生活中的证书

在现实生活中,证书顾名思义,就是权限机构颁发的证明。比如英语6级证书,就是教育部门颁发给通过了6级考核的个人的证明,证明这个人的英语能力。我们来看一下这个证书的组成:

  • 被证明人:老王
  • 内容:通过了英语六级
  • 盖章:教育部门的公章或钢印

当老王用这张证书找工作时,用人单位会通过查看证书的各项内容(尤其是公章),来验证证书的合法性和老王的能力。在现实生活中经常有假的6级证书,这些假证书最重要的就是有一个假公章。现实生活中使用法律法规来约束私刻假公章的行为,但是用人单位可能不能十分准确的判断公章是真是假。而数字签字可以来解决该类问题。

数字证书

数字证书就是通过数字签名实现的数字化的证书,在现实生活中公章可以被伪造,但是在计算数字世界中,数字签名是没办法被伪造的,比如上述证书中,在一个证书文件中写明了证书内容,颁发证书时,教育部门用他们的私钥对文件的摘要信息进行签名,将签名和证书文件一起发布,这样就能确保该证书无法被伪造。验证证书是否合法时,首先用教育部门的公钥(公钥是公开的谁都可以获取到)对签名进行解密得到一个摘要信息,使用教育部门同样的摘要算法得到证书的另一个摘要信息,对比两个摘要信息是否一致就能确定该证书是否合法。在一般的证书组成中部分中,还加入了一些其他信息,比如证书的有效期。

数字证书也有很多的签发机构,不同的签发机构签发的证书,用途也是不一样的,比如iOS开发中,使用到的ipa文件签名证书,需要到苹果申请。而在Web访问中为了防止Web内容在网络中安全传输,需要用到的SSL证书则需要向几家公认的机构签发。这些签发机构统称为CA(Certificate Authority)。

Web访问相关的证书可以向国际公认的几个机构:

  1. WebTrust
  2. GlobalSign
  3. GTE
  4. Nortel
  5. Verisign

数字证书的验证

申请证书是为了验证,比如Web应用相关的SSL证书验证方是浏览器,iOS各种证书的验证方是iOS设备。因为数字证书是基于数字签名的,所有数字证书的合法性验证就是验证数字证书的签名是否正确,对于签名的验证在是需要签发机构的公钥才能验证;

对于iOS开发证书来说,申请完签名证书后,还需要安装苹果的公钥证书(XCode安装后会自动安装),这样才能确保我们申请的证书是可以被验证通过的(合法的),可用来进行ipa文件签名的。对于Web相关的证书签名的验证,则是由浏览器来验证,对于国际公认的几个证书签发机构浏览器会内置其公钥证书,用来验证数字证书的可信性。

当数字证书通过验证后,便可以用数字证书做对应的事情,iOS开发证书可以用来对APP进行签名,SSL证书可以用来做Web内容加密处理相关的事情。所以有了这些证书之后,能保证在数据的传输过程中,数据是不会被篡改的,并且信息来源也是不能不修改的,从而确保信息安全。

对于iOS,iOS系统已经将这个验证过程固化在系统中了,除非越狱,否则无法绕过

数字证书的授权链

数字证书还包含一个授权链信息,举个例子:如果你要申请休假1周,需要你的上司审批,你的上司需要他的上司同意,最终需要大老板同意,那么这一层层的授权,形成了一个授权链,大老板是授权链的根(root),中间这些环节分别是被更接近root的人授权的。

比如苹果开发者的APP签名证书,该证书可以用来对APP进行签名,该证书实际上是由苹果的Worldwide Developer Relations Certificate Authority(WDRCA)授权签名的,而它是由Apple Certificate Authority授权签名的。在这个关系链中苹果的CA是根。 苹果CA根证书默认是内置在苹果系统中的,所以WDRCA的可信性可以由苹果内置的CA根证书来验证其可信性。

Web相关的SSL证书顶部CA根,则就是上述提到的几家公认的签发机构,当我们需要Web做SSL的证书时,便可以向上述机构申请,通常向根机构申请费用都会比较高,也可以向一些二级授权机构进行申请,选择根机构证书签发的好处就是目前大多数的浏览器都会预装内置了这些权威CA的公钥证书,这样,在使用这些权威CA签发过的证书的时候,浏览器一般不会报风险提示。

总结

数字证书签名的基础是非对称加密算法,利用了非对称加密的身份验证和防止信息篡改的特性来实现的,在一些其他方面比如HTTPS中密钥交换用的就是非对称加密的保密特性来实现的,在非对称加密算法中RSA应用最广。非对称加密虽好,但却有一个弊端,就是加解密比较耗时,所以一般都是配合对称加密一起使用。

原文:http://www.enkichen.com/2016/02/26/digital-certificate-based/

CDN的主要功能

CDN主要功能是在不同的地点缓存内容,通过负载均衡技术,将用户的请求定向到最合适的缓存服务器上去获取内容,比如说,是北京的用户,我们让他访问北京的节点,深圳的用户,我们让他访问深圳的节点。通过就近访问,加速用户对网站的访问。解决Internet网络拥堵状况,提高用户访问网络的响应速度。

简单介绍下CDN与传统网站访问的区别:
传统访问访问:

使用了CDN的网站访问:

与传统访问方式不同,CDN网络则是在用户和服务器之间增加缓存层,将用户的访问请求引导到最优的缓存节点而不是服务器源站点,从而加速访问速度。

完整的CDN工作流程:

总结一下CDN的工作原理:通过权威DNS服务器来实现最优节点的选择,通过缓存来减少源站的压力。
CDN应用场景:
静态网页图片小文件、博客
大文件下载软件下载、视频点播或图片存储网站
动态加速直播网站
应用加速手机APP

原文:https://www.zhihu.com/question/37353035

详解https是如何确保安全的

Https 介绍

什么是Https

HTTPS(全称:Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL

Https的作用

内容加密建立一个信息安全通道,来保证数据传输的安全;
身份认证确认网站的真实性
数据完整性防止内容被第三方冒充或者篡改

Https的劣势

对数据进行加解密决定了它比http慢
需要进行非对称的加解密,且需要三次握手。首次连接比较慢点,当然现在也有很多的优化。
出于安全考虑,浏览器不会在本地保存HTTPS缓存。实际上,只要在HTTP头中使用特定命令,HTTPS是可以缓存的。Firefox默认只在内存中缓存HTTPS。但是,只要头命令中有Cache-Control: Public,缓存就会被写到硬盘上。 IE只要http头允许就可以缓存https内容,缓存策略与是否使用HTTPS协议无关。

HTTPS和HTTP的区别

https协议需要到CA申请证书。
http是超文本传输协议,信息是明文传输;https 则是具有安全性的ssl加密传输协议。
http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
http默认使用80端口,https默认使用443端口

下面就是https的整个架构,现在的https基本都使用TLS了,因为更加安全,所以下图中的SSL应该换为SSL/TLS

下面就上图中的知识点进行一个大概的介绍。

加解密相关知识

对称加密

对称加密(也叫私钥加密)指加密和解密使用相同密钥的加密算法。有时又叫传统密码算法,就是加密密钥能够从解密密钥中推算出来,同时解密密钥也可以从加密密钥中推算出来。而在大多数的对称算法中,加密密钥和解密密钥是相同的,所以也称这种加密算法为秘密密钥算法或单密钥算法。

常见的对称加密有:DES(Data Encryption Standard)、AES(Advanced Encryption Standard)、RC4、IDEA

非对称加密

与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey);并且加密密钥和解密密钥是成对出现的。非对称加密算法在加密和解密过程使用了不同的密钥,非对称加密也称为公钥加密,在密钥对中,其中一个密钥是对外公开的,所有人都可以获取到,称为公钥,其中一个密钥是不公开的称为私钥。

非对称加密算法对加密内容的长度有限制,不能超过公钥长度。比如现在常用的公钥长度是 2048 位,意味着待加密内容不能超过 256 个字节。

摘要算法

数字摘要是采用单项Hash函数将需要加密的明文“摘要”成一串固定长度(128位)的密文,这一串密文又称为数字指纹,它有固定的长度,而且不同的明文摘要成密文,其结果总是不同的,而同样的明文其摘要必定一致。“数字摘要“是https能确保数据完整性和防篡改的根本原因。

数字签名

数字签名技术就是对“非对称密钥加解密”和“数字摘要“两项技术的应用,它将摘要信息用发送者的私钥加密,与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文产生一个摘要信息,与解密的摘要信息对比。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改,否则说明信息被修改过,因此数字签名能够验证信息的完整性。

数字签名的过程如下:明文 –> hash运算 –> 摘要 –> 私钥加密 –> 数字签名

数字签名有两种功效:
一、能确定消息确实是由发送方签名并发出来的,因为别人假冒不了发送方的签名。
二、数字签名能确定消息的完整性。

注意:
数字签名只能验证数据的完整性,数据本身是否加密不属于数字签名的控制范围

数字证书

为什么要有数字证书?

对于请求方来说,它怎么能确定它所得到的公钥一定是从目标主机那里发布的,而且没有被篡改过呢?亦或者请求的目标主机本本身就从事窃取用户信息的不正当行为呢?这时候,我们需要有一个权威的值得信赖的第三方机构(一般是由政府审核并授权的机构)来统一对外发放主机机构的公钥,只要请求方这种机构获取公钥,就避免了上述问题的发生。

数字证书的颁发过程

用户首先产生自己的密钥对,并将公共密钥及部分个人身份信息传送给认证中心。认证中心在核实身份后,将执行一些必要的步骤,以确信请求确实由用户发送而来,然后,认证中心将发给用户一个数字证书,该证书内包含用户的个人信息和他的公钥信息,同时还附有认证中心的签名信息(根证书私钥签名)。用户就可以使用自己的数字证书进行相关的各种活动。数字证书由独立的证书发行机构发布,数字证书各不相同,每种证书可提供不同级别的可信度。

证书包含哪些内容

证书颁发机构的名称
证书本身的数字签名
证书持有者公钥
证书签名用到的Hash算法

验证证书的有效性

浏览器默认都会内置CA根证书,其中根证书包含了CA的公钥
证书颁发的机构是伪造的:浏览器不认识,直接认为是危险证书

证书颁发的机构是确实存在的,于是根据CA名,找到对应内置的CA根证书、CA的公钥。用CA的公钥,对伪造的证书的摘要进行解密,发现解不了,认为是危险证书。

对于篡改的证书,使用CA的公钥对数字签名进行解密得到摘要A,然后再根据签名的Hash算法计算出证书的摘要B,对比A与B,若相等则正常,若不相等则是被篡改过的。

证书可在其过期前被吊销,通常情况是该证书的私钥已经失密。较新的浏览器如Chrome、Firefox、Opera和Internet Explorer都实现了在线证书状态协议(OCSP)以排除这种情形:浏览器将网站提供的证书的序列号通过OCSP发送给证书颁发机构,后者会告诉浏览器证书是否还是有效的。

1、2点是对伪造证书进行的,3是对于篡改后的证书验证,4是对于过期失效的验证。

SSL 与 TLS

SSL (Secure Socket Layer,安全套接字层)

SSL为Netscape所研发,用以保障在Internet上数据传输之安全,利用数据加密(Encryption)技术,可确保数据在网络上之传输过程中不会被截取,当前为3.0版本。

SSL协议可分为两层: SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。 SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

TLS (Transport Layer Security,传输层安全协议)

用于两个应用程序之间提供保密性和数据完整性。

TLS 1.0是IETF(Internet Engineering Task Force,Internet工程任务组)制定的一种新的协议,它建立在SSL 3.0协议规范之上,是SSL 3.0的后续版本,可以理解为SSL 3.1,它是写入了 RFC 的。该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。较低的层为 TLS 记录协议,位于某个可靠的传输协议(例如 TCP)上面。

SSL/TLS协议作用:

认证用户和服务器,确保数据发送到正确的客户机和服务器;
加密数据以防止数据中途被窃取;
维护数据的完整性,确保数据在传输过程中不被改变。

TLS比SSL的优势

对于消息认证使用密钥散列法:TLS 使用“消息认证代码的密钥散列法”(HMAC),当记录在开放的网络(如因特网)上传送时,该代码确保记录不会被变更。SSLv3.0还提供键控消息认证,但HMAC比SSLv3.0使用的(消息认证代码)MAC 功能更安全。

增强的伪随机功能(PRF):PRF生成密钥数据。在TLS中,HMAC定义PRF。PRF使用两种散列算法保证其安全性。如果任一算法暴露了,只要第二种算法未暴露,则数据仍然是安全的。

改进的已完成消息验证:TLS和SSLv3.0都对两个端点提供已完成的消息,该消息认证交换的消息没有被变更。然而,TLS将此已完成消息基于PRF和HMAC值之上,这也比SSLv3.0更安全。

一致证书处理:与SSLv3.0不同,TLS试图指定必须在TLS之间实现交换的证书类型。

特定警报消息:TLS提供更多的特定和附加警报,以指示任一会话端点检测到的问题。TLS还对何时应该发送某些警报进行记录。

SSL、TLS的握手过程

SSL与TLS握手整个过程如下图所示,下面会详细介绍每一步的具体内容:

客户端首次发出请求

由于客户端(如浏览器)对一些加解密算法的支持程度不一样,但是在TLS协议传输过程中必须使用同一套加解密算法才能保证数据能够正常的加解密。在TLS握手阶段,客户端首先要告知服务端,自己支持哪些加密算法,所以客户端需要将本地支持的加密套件(Cipher Suite)的列表传送给服务端。除此之外,客户端还要产生一个随机数,这个随机数一方面需要在客户端保存,另一方面需要传送给服务端,客户端的随机数需要跟服务端产生的随机数结合起来产生后面要讲到的 Master Secret 。

客户端需要提供如下信息:
支持的协议版本,比如TLS 1.0版
一个客户端生成的随机数,稍后用于生成”对话密钥”
支持的加密方法,比如RSA公钥加密
支持的压缩方法

服务端首次回应

服务端在接收到客户端的Client Hello之后,服务端需要确定加密协议的版本,以及加密的算法,然后也生成一个随机数,以及将自己的证书发送给客户端一并发送给客户端,这里的随机数是整个过程的第二个随机数。

服务端需要提供的信息:
协议的版本
加密的算法
随机数
服务器证书

客户端再次回应

客户端首先会对服务器下发的证书进行验证,验证通过之后,则会继续下面的操作,客户端再次产生一个随机数(第三个随机数),然后使用服务器证书中的公钥进行加密,以及放一个ChangeCipherSpec消息即编码改变的消息,还有整个前面所有消息的hash值,进行服务器验证,然后用新秘钥加密一段数据一并发送到服务器,确保正式通信前无误。

客户端使用前面的两个随机数以及刚刚新生成的新随机数,使用与服务器确定的加密算法,生成一个Session Secret。

ChangeCipherSpec

ChangeCipherSpec是一个独立的协议,体现在数据包中就是一个字节的数据,用于告知服务端,客户端已经切换到之前协商好的加密套件(Cipher Suite)的状态,准备使用之前协商好的加密套件加密数据并传输了。

服务器再次响应

服务端在接收到客户端传过来的第三个随机数的 加密数据之后,使用私钥对这段加密数据进行解密,并对数据进行验证,也会使用跟客户端同样的方式生成秘钥,一切准备好之后,也会给客户端发送一个 ChangeCipherSpec,告知客户端已经切换到协商过的加密套件状态,准备使用加密套件和 Session Secret加密数据了。之后,服务端也会使用 Session Secret 加密一段 Finish 消息发送给客户端,以验证之前通过握手建立起来的加解密通道是否成功。

后续客户端与服务器间通信

确定秘钥之后,服务器与客户端之间就会通过商定的秘钥加密消息了,进行通讯了。整个握手过程也就基本完成了。

值得特别提出的是:
SSL协议在握手阶段使用的是非对称加密,在传输阶段使用的是对称加密,也就是说在SSL上传送的数据是使用对称密钥加密的!因为非对称加密的速度缓慢,耗费资源。其实当客户端和主机使用非对称加密方式建立连接后,客户端和主机已经决定好了在传输过程使用的对称加密算法和关键的对称加密密钥,由于这个过程本身是安全可靠的,也即对称加密密钥是不可能被窃取盗用的,因此,保证了在传输过程中对数据进行对称加密也是安全可靠的,因为除了客户端和主机之外,不可能有第三方窃取并解密出对称加密密钥!如果有人窃听通信,他可以知道双方选择的加密方法,以及三个随机数中的两个。整个通话的安全,只取决于第三个随机数(Premaster secret)能不能被破解。

其他补充

对于非常重要的保密数据,服务端还需要对客户端进行验证,以保证数据传送给了安全的合法的客户端。服务端可以向客户端发出 Cerficate Request 消息,要求客户端发送证书对客户端的合法性进行验证。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。

PreMaster secret前两个字节是TLS的版本号,这是一个比较重要的用来核对握手数据的版本号,因为在Client Hello阶段,客户端会发送一份加密套件列表和当前支持的SSL/TLS的版本号给服务端,而且是使用明文传送的,如果握手的数据包被破解之后,攻击者很有可能串改数据包,选择一个安全性较低的加密套件和版本给服务端,从而对数据进行破解。所以,服务端需要对密文中解密出来对的PreMaster版本号跟之前Client Hello阶段的版本号进行对比,如果版本号变低,则说明被串改,则立即停止发送任何消息。

session的恢复

有两种方法可以恢复原来的session:一种叫做session ID,另一种叫做session ticket。

session ID

session ID的思想很简单,就是每一次对话都有一个编号(session ID)。如果对话中断,下次重连的时候,只要客户端给出这个编号,且服务器有这个编号的记录,双方就可以重新使用已有的”对话密钥”,而不必重新生成一把。

session ID是目前所有浏览器都支持的方法,但是它的缺点在于session ID往往只保留在一台服务器上。所以,如果客户端的请求发到另一台服务器,就无法恢复对话

session ticket

客户端发送一个服务器在上一次对话中发送过来的session ticket。这个session ticket是加密的,只有服务器才能解密,其中包括本次对话的主要信息,比如对话密钥和加密方法。当服务器收到session ticket以后,解密后就不必重新生成对话密钥了。
目前只有Firefox和Chrome浏览器支持。

总结

https实际就是在TCP层与http层之间加入了SSL/TLS来为上层的安全保驾护航,主要用到对称加密、非对称加密、证书,等技术进行客户端与服务器的数据加密传输,最终达到保证整个通信的安全性。

原文