Node.js(三)node.js的常用內置模塊


前言

因為Node.js是運行在服務區端的JavaScript環境,服務器程序和瀏覽器程序相比,最大的特點是沒有瀏覽器的安全限制了,而且,服務器程序必須能接收網絡請求,讀寫文件,處理二進制內容,所以,Node.js內置的常用模塊就是為了實現基本的服務器功能。這些模塊在瀏覽器環境中是無法被執行的,因為它們的底層代碼是用C/C++在Node.js運行環境中實現的。

global

在前面的JavaScript課程中,我們已經知道,JavaScript有且僅有一個全局對象,在瀏覽器中,叫window對象。而在Node.js環境中,也有唯一的全局對象,但不叫window,而叫global,這個對象的屬性和方法也和瀏覽器環境的window不同。進入Node.js交互環境,可以直接輸入:

> global.console
Console {
  log: [Function: bound ],
  info: [Function: bound ],
  warn: [Function: bound ],
  error: [Function: bound ],
  dir: [Function: bound ],
  time: [Function: bound ],
  timeEnd: [Function: bound ],
  trace: [Function: bound trace],
  assert: [Function: bound ],
  Console: [Function: Console] }

 

process

process也是Node.js提供的一個對象,它代表當前Node.js進程。通過process對象可以拿到許多有用信息:

> process === global.process;
true
> process.version;
'v5.2.0'
> process.platform;
'darwin'
> process.arch;
'x64'
> process.cwd(); //返回當前工作目錄
'/Users/michael'
> process.chdir('/private/tmp'); // 切換當前工作目錄
undefined
> process.cwd();
'/private/tmp'

JavaScript程序是由事件驅動執行的單線程模型,Node.js也不例外。Node.js不斷執行響應事件的JavaScript函數,直到沒有任何響應事件的函數可以執行時,Node.js就退出了。

如果我們想要在下一次事件響應中執行代碼,可以調用process.nextTick()

// test.js

// process.nextTick()將在下一輪事件循環中調用:
process.nextTick(function () {
    console.log('nextTick callback!');
});
console.log('nextTick was set!');

用Node執行上面的代碼node test.js,你會看到,打印輸出是:

nextTick was set!
nextTick callback!

這說明傳入process.nextTick()的函數不是立刻執行,而是要等到下一次事件循環。

Node.js進程本身的事件就由process對象來處理。如果我們響應exit事件,就可以在程序即將退出時執行某個回調函數:

// 程序即將退出時的回調函數:
process.on('exit', function (code) {
    console.log('about to exit with code: ' + code);
});

判斷JavaScript執行環境

有很多JavaScript代碼既能在瀏覽器中執行,也能在Node環境執行,但有些時候,程序本身需要判斷自己到底是在什么環境下執行的,常用的方式就是根據瀏覽器和Node環境提供的全局變量名稱來判斷:

if (typeof(window) === 'undefined') {
    console.log('node.js');
} else {
    console.log('browser');
}

后面,我們將介紹Node.js的常用內置模塊。

 

一、fs

Node.js內置的fs模塊就是文件系統模塊,負責讀寫文件。

和所有其它JavaScript模塊不同的是,fs模塊同時提供了異步和同步的方法。

回顧一下什么是異步方法。因為JavaScript的單線程模型,執行IO操作時,JavaScript代碼無需等待,而是傳入回調函數后,繼續執行后續JavaScript代碼。比如jQuery提供的getJSON()操作:

$.getJSON('http://example.com/ajax', function (data) {
    console.log('IO結果返回后執行...');
});
console.log('不等待IO結果直接執行后續代碼...');

而同步的IO操作則需要等待函數返回:

// 根據網絡耗時,函數將執行幾十毫秒~幾秒不等:
var data = getJSONSync('http://example.com/ajax');

同步操作的好處是代碼簡單,缺點是程序將等待IO操作,在等待時間內,無法響應其它任何事件。而異步讀取不用等待IO操作,但代碼較麻煩。

異步讀文件

按照JavaScript的標准,異步讀取一個文本文件的代碼如下:

'use strict';

var fs = require('fs');

fs.readFile('sample.txt', 'utf-8', function (err, data) {
    if (err) {
        console.log(err);
    } else {
        console.log(data);
    }
});

請注意,sample.txt文件必須在當前目錄下,且文件編碼為utf-8

異步讀取時,傳入的回調函數接收兩個參數,當正常讀取時,err參數為nulldata參數為讀取到的String。當讀取發生錯誤時,err參數代表一個錯誤對象,dataundefined。這也是Node.js標准的回調函數:第一個參數代表錯誤信息,第二個參數代表結果。后面我們還會經常編寫這種回調函數。

由於err是否為null就是判斷是否出錯的標志,所以通常的判斷邏輯總是:

if (err) {
    // 出錯了
} else {
    // 正常
}

如果我們要讀取的文件不是文本文件,而是二進制文件,怎么辦?

下面的例子演示了如何讀取一個圖片文件:

'use strict';

var fs = require('fs');

fs.readFile('sample.png', function (err, data) {
    if (err) {
        console.log(err);
    } else {
        console.log(data);
        console.log(data.length + ' bytes');
    }
});

當讀取二進制文件時,不傳入文件編碼時,回調函數的data參數將返回一個Buffer對象。在Node.js中,Buffer對象就是一個包含零個或任意個字節的數組(注意和Array不同)。

Buffer對象可以和String作轉換,例如,把一個Buffer對象轉換成String:

// Buffer -> String
var text = data.toString('utf-8');
console.log(text);

或者把一個String轉換成Buffer

// String -> Buffer
var buf = Buffer.from(text, 'utf-8');
console.log(buf);

同步讀文件

除了標准的異步讀取模式外,fs也提供相應的同步讀取函數。同步讀取的函數和異步函數相比,多了一個Sync后綴,並且不接收回調函數,函數直接返回結果。

fs模塊同步讀取一個文本文件的代碼如下:

'use strict';

var fs = require('fs');

var data = fs.readFileSync('sample.txt', 'utf-8');
console.log(data);

可見,原異步調用的回調函數的data被函數直接返回,函數名需要改為readFileSync,其它參數不變。

如果同步讀取文件發生錯誤,則需要用try...catch捕獲該錯誤:

try {
    var data = fs.readFileSync('sample.txt', 'utf-8');
    console.log(data);
} catch (err) {
    // 出錯了
}

寫文件

將數據寫入文件是通過fs.writeFile()實現的:

'use strict';

var fs = require('fs');

var data = 'Hello, Node.js';
fs.writeFile('output.txt', data, function (err) {
    if (err) {
        console.log(err);
    } else {
        console.log('ok.');
    }
});

writeFile()的參數依次為文件名、數據和回調函數。如果傳入的數據是String,默認按UTF-8編碼寫入文本文件,如果傳入的參數是Buffer,則寫入的是二進制文件。回調函數由於只關心成功與否,因此只需要一個err參數。

readFile()類似,writeFile()也有一個同步方法,叫writeFileSync()

'use strict';

var fs = require('fs');

var data = 'Hello, Node.js';
fs.writeFileSync('output.txt', data);

stat

如果我們要獲取文件大小,創建時間等信息,可以使用fs.stat(),它返回一個Stat對象,能告訴我們文件或目錄的詳細信息:

'use strict';

var fs = require('fs');

fs.stat('sample.txt', function (err, stat) {
    if (err) {
        console.log(err);
    } else {
        // 是否是文件:
        console.log('isFile: ' + stat.isFile());
        // 是否是目錄:
        console.log('isDirectory: ' + stat.isDirectory());
        if (stat.isFile()) {
            // 文件大小:
            console.log('size: ' + stat.size);
            // 創建時間, Date對象:
            console.log('birth time: ' + stat.birthtime);
            // 修改時間, Date對象:
            console.log('modified time: ' + stat.mtime);
        }
    }
});

運行結果如下:

isFile: true
isDirectory: false
size: 181
birth time: Fri Dec 11 2015 09:43:41 GMT+0800 (CST)
modified time: Fri Dec 11 2015 12:09:00 GMT+0800 (CST)

stat()也有一個對應的同步函數statSync(),請試着改寫上述異步代碼為同步代碼。

異步還是同步

fs模塊中,提供同步方法是為了方便使用。那我們到底是應該用異步方法還是同步方法呢?

由於Node環境執行的JavaScript代碼是服務器端代碼,所以,絕大部分需要在服務器運行期反復執行業務邏輯的代碼,必須使用異步代碼,否則,同步代碼在執行時期,服務器將停止響應,因為JavaScript只有一個執行線程。

服務器啟動時如果需要讀取配置文件,或者結束時需要寫入到狀態文件時,可以使用同步代碼,因為這些代碼只在啟動和結束時執行一次,不影響服務器正常運行時的異步執行

 

二、stream

stream是Node.js提供的又一個僅在服務區端可用的模塊,目的是支持“流”這種數據結構。

什么是流?流是一種抽象的數據結構。想象水流,當在水管中流動時,就可以從某個地方(例如自來水廠)源源不斷地到達另一個地方(比如你家的洗手池)。我們也可以把數據看成是數據流,比如你敲鍵盤的時候,就可以把每個字符依次連起來,看成字符流。這個流是從鍵盤輸入到應用程序,實際上它還對應着一個名字:標准輸入流(stdin)。

如果應用程序把字符一個一個輸出到顯示器上,這也可以看成是一個流,這個流也有名字:標准輸出流(stdout)。流的特點是數據是有序的,而且必須依次讀取,或者依次寫入,不能像Array那樣隨機定位。

 

 

有些流用來讀取數據,比如從文件讀取數據時,可以打開一個文件流,然后從文件流中不斷地讀取數據。有些流用來寫入數據,比如向文件寫入數據時,只需要把數據不斷地往文件流中寫進去就可以了。

在Node.js中,流也是一個對象,我們只需要響應流的事件就可以了:data事件表示流的數據已經可以讀取了,end事件表示這個流已經到末尾了,沒有數據可以讀取了,error事件表示出錯了。

下面是一個從文件流讀取文本內容的示例:

'use strict';

var fs = require('fs');

// 打開一個流:
var rs = fs.createReadStream('sample.txt', 'utf-8');

rs.on('data', function (chunk) {
    console.log('DATA:')
    console.log(chunk);
});

rs.on('end', function () {
    console.log('END');
});

rs.on('error', function (err) {
    console.log('ERROR: ' + err);
});

要注意,data事件可能會有多次,每次傳遞的chunk是流的一部分數據。

要以流的形式寫入文件,只需要不斷調用write()方法,最后以end()結束:

'use strict';

var fs = require('fs');

var ws1 = fs.createWriteStream('output1.txt', 'utf-8');
ws1.write('使用Stream寫入文本數據...\n');
ws1.write('END.');
ws1.end();

var ws2 = fs.createWriteStream('output2.txt');
ws2.write(new Buffer('使用Stream寫入二進制數據...\n', 'utf-8'));
ws2.write(new Buffer('END.', 'utf-8'));
ws2.end();

所有可以讀取數據的流都繼承自stream.Readable,所有可以寫入的流都繼承自stream.Writable

pipe

就像可以把兩個水管串成一個更長的水管一樣,兩個流也可以串起來。一個Readable流和一個Writable流串起來后,所有的數據自動從Readable流進入Writable流,這種操作叫pipe

在Node.js中,Readable流有一個pipe()方法,就是用來干這件事的。

讓我們用pipe()把一個文件流和另一個文件流串起來,這樣源文件的所有數據就自動寫入到目標文件里了,所以,這實際上是一個復制文件的程序:

 

'use strict';

var fs = require('fs');

var rs = fs.createReadStream('sample.txt');
var ws = fs.createWriteStream('copied.txt');

rs.pipe(ws);

默認情況下,當Readable流的數據讀取完畢,end事件觸發后,將自動關閉Writable流。如果我們不希望自動關閉Writable流,需要傳入參數:

readable.pipe(writable, { end: false });

 

 

三、http

Node.js開發的目的就是為了用JavaScript編寫Web服務器程序。因為JavaScript實際上已經統治了瀏覽器端的腳本,其優勢就是有世界上數量最多的前端開發人員。如果已經掌握了JavaScript前端開發,再學習一下如何將JavaScript應用在后端開發,就是名副其實的全棧了。

HTTP協議

要理解Web服務器程序的工作原理,首先,我們要對HTTP協議有基本的了解。如果你對HTTP協議不太熟悉,先看一看HTTP協議簡介

HTTP服務器

要開發HTTP服務器程序,從頭處理TCP連接,解析HTTP是不現實的。這些工作實際上已經由Node.js自帶的http模塊完成了。應用程序並不直接和HTTP協議打交道,而是操作http模塊提供的requestresponse對象。

request對象封裝了HTTP請求,我們調用request對象的屬性和方法就可以拿到所有HTTP請求的信息;

response對象封裝了HTTP響應,我們操作response對象的方法,就可以把HTTP響應返回給瀏覽器。

用Node.js實現一個HTTP服務器程序非常簡單。我們來實現一個最簡單的Web程序hello.js,它對於所有請求,都返回Hello world!

'use strict';

// 導入http模塊:
var http = require('http');

// 創建http server,並傳入回調函數:
var server = http.createServer(function (request, response) {
    // 回調函數接收request和response對象,
    // 獲得HTTP請求的method和url:
    console.log(request.method + ': ' + request.url);
    // 將HTTP響應200寫入response, 同時設置Content-Type: text/html:
    response.writeHead(200, {'Content-Type': 'text/html'});
    // 將HTTP響應的HTML內容寫入response:
    response.end('<h1>Hello world!</h1>');
});

// 讓服務器監聽8080端口:
server.listen(8080);

console.log('Server is running at http://127.0.0.1:8080/');

在命令提示符下運行該程序,可以看到以下輸出:

$ node hello.js 
Server is running at http://127.0.0.1:8080/

不要關閉命令提示符,直接打開瀏覽器輸入http://localhost:8080,即可看到服務器響應的內容:

 

 

 

 同時,在命令提示符窗口,可以看到程序打印的請求信息:

GET: /
GET: /favicon.ico

這就是我們編寫的第一個HTTP服務器程序!

文件服務器

讓我們繼續擴展一下上面的Web程序。我們可以設定一個目錄,然后讓Web程序變成一個文件服務器。要實現這一點,我們只需要解析request.url中的路徑,然后在本地找到對應的文件,把文件內容發送出去就可以了。

解析URL需要用到Node.js提供的url模塊,它使用起來非常簡單,通過parse()將一個字符串解析為一個Url對象:

'use strict';

var url = require('url');

console.log(url.parse('http://user:pass@host.com:8080/path/to/file?query=string#hash'));

結果如下:

Url {
  protocol: 'http:',
  slashes: true,
  auth: 'user:pass',
  host: 'host.com:8080',
  port: '8080',
  hostname: 'host.com',
  hash: '#hash',
  search: '?query=string',
  query: 'query=string',
  pathname: '/path/to/file',
  path: '/path/to/file?query=string',
  href: 'http://user:pass@host.com:8080/path/to/file?query=string#hash' }

 

處理本地文件目錄需要使用Node.js提供的path模塊,它可以方便地構造目錄:

'use strict';

var path = require('path');

// 解析當前目錄:
var workDir = path.resolve('.'); // '/Users/michael'

// 組合完整的文件路徑:當前目錄+'pub'+'index.html':
var filePath = path.join(workDir, 'pub', 'index.html');
// '/Users/michael/pub/index.html'

 

使用path模塊可以正確處理操作系統相關的文件路徑。在Windows系統下,返回的路徑類似於C:\Users\michael\static\index.html,這樣,我們就不關心怎么拼接路徑了。

最后,我們實現一個文件服務器file_server.js

'use strict';

var
    fs = require('fs'),
    url = require('url'),
    path = require('path'),
    http = require('http');

// 從命令行參數獲取root目錄,默認是當前目錄:
var root = path.resolve(process.argv[2] || '.');

console.log('Static root dir: ' + root);

// 創建服務器:
var server = http.createServer(function (request, response) {
    // 獲得URL的path,類似 '/css/bootstrap.css':
    var pathname = url.parse(request.url).pathname;
    // 獲得對應的本地文件路徑,類似 '/srv/www/css/bootstrap.css':
    var filepath = path.join(root, pathname);
    // 獲取文件狀態:
    fs.stat(filepath, function (err, stats) {
        if (!err && stats.isFile()) {
            // 沒有出錯並且文件存在:
            console.log('200 ' + request.url);
            // 發送200響應:
            response.writeHead(200);
            // 將文件流導向response:
            fs.createReadStream(filepath).pipe(response);
        } else {
            // 出錯了或者文件不存在:
            console.log('404 ' + request.url);
            // 發送404響應:
            response.writeHead(404);
            response.end('404 Not Found');
        }
    });
});

server.listen(8080);

console.log('Server is running at http://127.0.0.1:8080/');

 

沒有必要手動讀取文件內容。由於response對象本身是一個Writable Stream,直接用pipe()方法就實現了自動讀取文件內容並輸出到HTTP響應。

在命令行運行node file_server.js /path/to/dir,把/path/to/dir改成你本地的一個有效的目錄,然后在瀏覽器中輸入http://localhost:8080/index.html

 

 

只要當前目錄下存在文件index.html,服務器就可以把文件內容發送給瀏覽器。觀察控制台輸出:

200 /index.html
200 /css/uikit.min.css
200 /js/jquery.min.js
200 /fonts/fontawesome-webfont.woff2

 

第一個請求是瀏覽器請求index.html頁面,后續請求是瀏覽器解析HTML后發送的其它資源請求。

練習

在瀏覽器輸入http://localhost:8080/時,會返回404,原因是程序識別出HTTP請求的不是文件,而是目錄。請修改file_server.js,如果遇到請求的路徑是目錄,則自動在目錄下依次搜索index.htmldefault.html,如果找到了,就返回HTML文件的內容。

var fs = require('fs');

var url = require('url');

var http = require('http');

var path = require('path');

/** forecast.html所在目錄(C:\Users\1\Desktop\Prac\demoWeb_Temp\vue-learn\forecast) */

var root = path.resolve(process.argv[2] || '.'); 

var server = http.createServer(function (request, response){

    var pathname = url.parse(request.url).pathname;

    var filepath = path.join(root, pathname);

    var defaultPath = ['/default.html', '/forecast.html'];

    fs.stat(filepath, function (err, stat){

        if(!err && stat.isFile()){

            console.log("200" + request.url);

            response.writeHead(200);

            /** 將文件流導向response */

            /**沒有必要手動讀取文件內容。由於response對象本身是一個Writable Stream,

            * 直接用pipe()方法就實現了自動讀取文件內容並輸出到HTTP響應。 */

            fs.createReadStream(filepath).pipe(response);

        }else if(!err && stat.isDirectory()){

            for(var i = 0; i < defaultPath.length; i++){

                var tempPath = fs.existsSync(path.join(filepath, defaultPath[i]));

                if(tempPath){

                    console.log("200" + request.url);

                    response.writeHead(200);

                    fs.createReadStream(path.join(filepath, defaultPath[i])).pipe(response);

                    break;

                }

            }

        }else{

            console.log("404" + request.url);

            response.writeHead(404);

            response.end("404 Not Found!");

        }

    });

});

server.listen(8080);

console.log('Server is running at http://127.0.0.1:8080/');

 

參考源碼

http服務器代碼(含靜態網站)

 

四、crypto

crypto模塊的目的是為了提供通用的加密和哈希算法。用純JavaScript代碼實現這些功能不是不可能,但速度會非常慢。Nodejs用C/C++實現這些算法后,通過cypto這個模塊暴露為JavaScript接口,這樣用起來方便,運行速度也快。

MD5和SHA1

MD5是一種常用的哈希算法,用於給任意數據一個“簽名”。這個簽名通常用一個十六進制的字符串表示:

const crypto = require('crypto');

const hash = crypto.createHash('md5');

// 可任意多次調用update():
hash.update('Hello, world!');
hash.update('Hello, nodejs!');

console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544

update()方法默認字符串編碼為UTF-8,也可以傳入Buffer。

如果要計算SHA1,只需要把'md5'改成'sha1',就可以得到SHA1的結果1f32b9c9932c02227819a4151feed43e131aca40

還可以使用更安全的sha256sha512

Hmac

Hmac算法也是一種哈希算法,它可以利用MD5或SHA1等哈希算法。不同的是,Hmac還需要一個密鑰:

const crypto = require('crypto');

const hmac = crypto.createHmac('sha256', 'secret-key');

hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');

console.log(hmac.digest('hex')); // 80f7e22570...

只要密鑰發生了變化,那么同樣的輸入數據也會得到不同的簽名,因此,可以把Hmac理解為用隨機數“增強”的哈希算法。

AES

AES是一種常用的對稱加密算法,加解密都用同一個密鑰。crypto模塊提供了AES支持,但是需要自己封裝好函數,便於使用:

const crypto = require('crypto');

function aesEncrypt(data, key) {
    const cipher = crypto.createCipher('aes192', key);
    var crypted = cipher.update(data, 'utf8', 'hex');
    crypted += cipher.final('hex');
    return crypted;
}

function aesDecrypt(encrypted, key) {
    const decipher = crypto.createDecipher('aes192', key);
    var decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
}

var data = 'Hello, this is a secret message!';
var key = 'Password!';
var encrypted = aesEncrypt(data, key);
var decrypted = aesDecrypt(encrypted, key);

console.log('Plain text: ' + data);
console.log('Encrypted text: ' + encrypted);
console.log('Decrypted text: ' + decrypted);

 

運行結果如下:

Plain text: Hello, this is a secret message!
Encrypted text: 8a944d97bdabc157a5b7a40cb180e7...
Decrypted text: Hello, this is a secret message!

可以看出,加密后的字符串通過解密又得到了原始內容。

注意到AES有很多不同的算法,如aes192aes-128-ecbaes-256-cbc等,AES除了密鑰外還可以指定IV(Initial Vector),不同的系統只要IV不同,用相同的密鑰加密相同的數據得到的加密結果也是不同的。加密結果通常有兩種表示方法:hex和base64,這些功能Nodejs全部都支持,但是在應用中要注意,如果加解密雙方一方用Nodejs,另一方用Java、PHP等其它語言,需要仔細測試。如果無法正確解密,要確認雙方是否遵循同樣的AES算法,字符串密鑰和IV是否相同,加密后的數據是否統一為hex或base64格式。

Diffie-Hellman

DH算法是一種密鑰交換協議,它可以讓雙方在不泄漏密鑰的情況下協商出一個密鑰來。DH算法基於數學原理,比如小明和小紅想要協商一個密鑰,可以這么做:

  1. 小明先選一個素數和一個底數,例如,素數p=23,底數g=5(底數可以任選),再選擇一個秘密整數a=6,計算A=g^a mod p=8,然后大聲告訴小紅:p=23,g=5,A=8

  2. 小紅收到小明發來的pgA后,也選一個秘密整數b=15,然后計算B=g^b mod p=19,並大聲告訴小明:B=19

  3. 小明自己計算出s=B^a mod p=2,小紅也自己計算出s=A^b mod p=2,因此,最終協商的密鑰s2

在這個過程中,密鑰2並不是小明告訴小紅的,也不是小紅告訴小明的,而是雙方協商計算出來的。第三方只能知道p=23g=5A=8B=19,由於不知道雙方選的秘密整數a=6b=15,因此無法計算出密鑰2

用crypto模塊實現DH算法如下:

const crypto = require('crypto');

// xiaoming's keys:
var ming = crypto.createDiffieHellman(512);
var ming_keys = ming.generateKeys();

var prime = ming.getPrime();
var generator = ming.getGenerator();

console.log('Prime: ' + prime.toString('hex'));
console.log('Generator: ' + generator.toString('hex'));

// xiaohong's keys:
var hong = crypto.createDiffieHellman(prime, generator);
var hong_keys = hong.generateKeys();

// exchange and generate secret:
var ming_secret = ming.computeSecret(hong_keys);
var hong_secret = hong.computeSecret(ming_keys);

// print secret:
console.log('Secret of Xiao Ming: ' + ming_secret.toString('hex'));
console.log('Secret of Xiao Hong: ' + hong_secret.toString('hex'));

 

運行后,可以得到如下輸出:

$ node dh.js 
Prime: a8224c...deead3
Generator: 02
Secret of Xiao Ming: 695308...d519be
Secret of Xiao Hong: 695308...d519be

 

注意每次輸出都不一樣,因為素數的選擇是隨機的。

RSA

RSA算法是一種非對稱加密算法,即由一個私鑰和一個公鑰構成的密鑰對,通過私鑰加密,公鑰解密,或者通過公鑰加密,私鑰解密。其中,公鑰可以公開,私鑰必須保密。

RSA算法是1977年由Ron Rivest、Adi Shamir和Leonard Adleman共同提出的,所以以他們三人的姓氏的頭字母命名。

當小明給小紅發送信息時,可以用小明自己的私鑰加密,小紅用小明的公鑰解密,也可以用小紅的公鑰加密,小紅用她自己的私鑰解密,這就是非對稱加密。相比對稱加密,非對稱加密只需要每個人各自持有自己的私鑰,同時公開自己的公鑰,不需要像AES那樣由兩個人共享同一個密鑰。

在使用Node進行RSA加密前,我們先要准備好私鑰和公鑰。

首先,在命令行執行以下命令以生成一個RSA密鑰對:

openssl genrsa -aes256 -out rsa-key.pem 2048

 

根據提示輸入密碼,這個密碼是用來加密RSA密鑰的,加密方式指定為AES256,生成的RSA的密鑰長度是2048位。執行成功后,我們獲得了加密的rsa-key.pem文件。

第二步,通過上面的rsa-key.pem加密文件,我們可以導出原始的私鑰,命令如下:

openssl rsa -in rsa-key.pem -outform PEM -out rsa-prv.pem

輸入第一步的密碼,我們獲得了解密后的私鑰。

類似的,我們用下面的命令導出原始的公鑰:

openssl rsa -in rsa-key.pem -outform PEM -pubout -out rsa-pub.pem

 

這樣,我們就准備好了原始私鑰文件rsa-prv.pem和原始公鑰文件rsa-pub.pem,編碼格式均為PEM。

下面,使用crypto模塊提供的方法,即可實現非對稱加解密。

首先,我們用私鑰加密,公鑰解密:

const
    fs = require('fs'),
    crypto = require('crypto');

// 從文件加載key:
function loadKey(file) {
    // key實際上就是PEM編碼的字符串:
    return fs.readFileSync(file, 'utf8');
}

let
    prvKey = loadKey('./rsa-prv.pem'),
    pubKey = loadKey('./rsa-pub.pem'),
    message = 'Hello, world!';

// 使用私鑰加密:
let enc_by_prv = crypto.privateEncrypt(prvKey, Buffer.from(message, 'utf8'));
console.log('encrypted by private key: ' + enc_by_prv.toString('hex'));


let dec_by_pub = crypto.publicDecrypt(pubKey, enc_by_prv);
console.log('decrypted by public key: ' + dec_by_pub.toString('utf8'));

 

執行后,可以得到解密后的消息,與原始消息相同。

接下來我們使用公鑰加密,私鑰解密:

// 使用公鑰加密:
let enc_by_pub = crypto.publicEncrypt(pubKey, Buffer.from(message, 'utf8'));
console.log('encrypted by public key: ' + enc_by_pub.toString('hex'));

// 使用私鑰解密:
let dec_by_prv = crypto.privateDecrypt(prvKey, enc_by_pub);
console.log('decrypted by private key: ' + dec_by_prv.toString('utf8'));

 

執行得到的解密后的消息仍與原始消息相同。

如果我們把message字符串的長度增加到很長,例如1M,這時,執行RSA加密會得到一個類似這樣的錯誤:data too large for key size,這是因為RSA加密的原始信息必須小於Key的長度。那如何用RSA加密一個很長的消息呢?實際上,RSA並不適合加密大數據,而是先生成一個隨機的AES密碼,用AES加密原始信息,然后用RSA加密AES口令,這樣,實際使用RSA時,給對方傳的密文分兩部分,一部分是AES加密的密文,另一部分是RSA加密的AES口令。對方用RSA先解密出AES口令,再用AES解密密文,即可獲得明文。

證書

crypto模塊也可以處理數字證書。數字證書通常用在SSL連接,也就是Web的https連接。一般情況下,https連接只需要處理服務器端的單向認證,如無特殊需求(例如自己作為Root給客戶發認證證書),建議用反向代理服務器如Nginx等Web服務器去處理證書。

參考源碼

crypto常用算法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM