http是無狀態的請求,上一次請求和下一次請求之間沒有什麼必然的聯繫,且是明文的協議,,每一次http請求,都至關於拿着喇叭在網絡上喊,個人帳號密碼是什麼。爲了解決這個問題,引入了cookie和session機制node
用戶每一次的請求都會帶有cookie,當咱們拿到用戶的cookie以後,再去數據庫裏或者內存裏邊看看,這個用戶是否已經登錄過了,這個cookie是否是我給他的,若是是我給的,他就有權限使用一些功能git
使用cookie可以訪問咱們用戶相應的內容,這樣看起來和帳號密碼功能差很少了。可是和帳號密碼的區別是,cookie是由後端加密的,信息雖然能被攻擊者拿到,可是不能被篡改(若是在客戶端作加密,也等因而透明瞭,差很少也等因而廣播)github
本文使用express
框架,使用import語法由於我是在typescript項目內作的,看成require來看就行,不影響食用web
下面使用setCookie
更改cookieredis
// 這裏是路由
app.get('/auth', authController.auth);
app.get('/hello', authController.sayHello);
// 如下是controller
// 這裏存儲用戶信息
const users = [];
// 設置cookie
export const auth = async function(req, res, next) {
const { username } = req.query;
if (!users.find(u => u.username === username)) {
res.set('Set-Cookie', `username=${username}`);
users.push({ username });
}
res.send();
};
export const sayHello = async function(req, res, next) {
const { username } = req.cookies;
if (users.find(u => u.username === username)) {
res.send(req.cookies.username);
} else {
res.send('please login');
}
};
複製代碼
用戶端訪問/auth?username="你的用戶名",返回header的cookie中會有username值是你輸入的名字,用戶再訪問/hello會獲得後端返回的值,也就是訪問/auth時輸入的username的值算法
會話管理表示一個用戶和服務器的交互的過程當中要求用戶先登錄,才能進行操做(例如敏感操做,已登錄用戶才能訪問的頁面),如何判斷用戶信息是不是真實,這時候就涉及到會話管理,會話管理有幾種形式,上邊代碼也是其中一種,它是最初級的一種會話管理,它沒有正常cookie過時時間,當服務器重啓後用戶的會話信息就會丟掉。更好的解決方案是把用戶的登陸信息放到數據庫裏邊mongodb
cookiesession是一個會話管理解決方案,express官網上有推薦typescript
下邊使用cookieSession來改造上邊的例子數據庫
// 安裝庫
npm install cookie-session
// 引入cookie-session
const cookieSession = require('cookie-session')
//使用中間件
app.use(
cookieSession({
name: 'testCookieSession', // cookie名稱
keys: ['nuo', 'blog'], // 祕鑰
// Cookie Options
maxAge: 24 * 60 * 60 * 1000 // cookie過時時間
})
);
// 改造的controller
export const auth = async function(req, res, next) {
const { username } = req.query;
req.session.user = { username };
res.send('success');
};
export const sayHello = async function(req, res, next) {
const { username } = req.session.user;
res.send(username);
};
複製代碼
此時訪問/auth?username=nuo返回的cookie就和以前明文返回的cookie有區別了,返回內容header的cookie部分以下express
cookie名稱: testCookieSession
cookie值: eyJ1c2VyIjp7InVzZXJuYW1lIjoibnVvIn19 // base64簡單加密的傳輸內容
cookie名稱: testCookieSession.sig // 這裏是cookie的簽名,根據上面設置的密鑰和解密出來的內容獲得的
cookie值: gTty0Cinxf8r0FwhmS8lz-TdM7I
複製代碼
這是一個通過簡單加密
的cookie,再訪問/hello,依然返回咱們設置的username,值是nuo
cookieSession這個中間件使用的是一套成熟的體系,幫咱們解決了最開始代碼咱們處理的那些細節。在這裏的體現:拿着同一套cookie的瀏覽器發過來的請求,自動把session給掛到request上。
Jsonwebtoken
是經常使用的會話管理,它的存儲方式不像setcookie方法那麼簡單
import JWT from 'jsonwebtoken';
export const auth = async function(req, res, next) {
const { username } = req.query;
const user = { username };
// 使用jsonwebtoken生成token
const token = JWT.sign(user, 'signKey_18902379108701');
res.send(token);
};
複製代碼
此時訪問/auth?username=nuo會返回一串字符串,以.
做爲分割,有三個部分
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im51byIsImlhdCI6MTU0NTc0NTcyNH0._hCXHeZBrd0uZYhvQxbQylDtC_1UC2hcXHBOK1rR0Kc
// 第一部分裏邊包含了加密信息
new Buffer('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9','base64').toString()
//'{"alg":"HS256","typ":"JWT"}' // 加密算法,加密方式
// 第二部分包含了傳遞的內容和生成時間
new Buffer('eyJ1c2VybmFtZSI6Im51byIsImlhdCI6MTU0NTc0NTcyNH0','base64').toString()
// {"username":"nuo","iat":1545745724} // 加密內容,加密時間
複製代碼
第三部分是沒法解密的,相似於cookieSession中的簽名,下面咱們來看如何在服務端解析jsonwebtoken的信息
import JWT from 'jsonwebtoken';
export const auth = async function(req, res, next) {
const { username } = req.query;
const user = { username };
const token = JWT.sign(user, 'signKey_18902379108701');
res.send(token);
};
export const sayHello = async function(req, res, next) {
const auth = req.get('Authorization');
if (!auth) return res.send('no auth');
if (auth.indexOf('Bearer') < 0) return res.send('no auth');
const token = auth.split('Bearer ')[1];
const user = JWT.verify(token, 'signKey_18902379108701');
res.send(user);
};
複製代碼
這裏請求/auth記錄用戶信息以後,須要在postman進行一下操做
在postman,Headers中輸入key:Authorization,value爲Bearer+以前返回的token,以下
key: Authorization
value: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im51byIsImlhdCI6MTU0NTc0NTcyNH0._hCXHeZBrd0uZYhvQxbQylDtC_1UC2hcXHBOK1rR0Kc
複製代碼
此時發送請求,返回的是上邊解密出來的值
把數據存到會話管理系統裏邊的緣由是由於,無狀態
的http請求,對於分佈式管理的系統很是不友好,若是登陸信息存在內存裏,服務器重啓,用戶須要從新登錄。或者有多臺服務器,用戶在一臺服務器上登陸後,想訪問另外一臺服務器上的內容,又須要進行從新登陸,也會很是的麻煩。解決這種狀況有幾種方式:
經過集羣的數據庫,好比redis,mongodb,把session統一的管理起來,或者每一個服務器存儲惟一的用戶信息,其餘服務器須要的時候來這臺服務器上取(分片式
存儲)
把用戶的不敏感
信息存放到cookie裏邊或發送到客戶端的token裏,客戶端能夠在任什麼時候候,經過token作一些不敏感操做,由於這些信息是明文的,當須要作敏感操做
的時候,須要加入其餘驗證方式,如交叉驗證
,當用戶作如更改密碼這些操做,給用戶一個更高級,過時時間更短的token來進行相應的處理
import JWT from 'jsonwebtoken';
export const auth = async function(req, res, next) {
const { username } = req.query;
const user = {
username,
// 設置token過時時間
expireAt: `${Date.now().valueOf() + 20 * 60 * 1000}`
};
const token = JWT.sign(user, 'signKey_18902379108701');
res.send(token);
};
export const sayHello = async function(req, res, next) {
const auth = req.get('Authorization');
if (!auth) return res.send('no auth');
if (auth.indexOf('Bearer') < 0) return res.send('no auth');
const token = auth.split('Bearer ')[1];
const user = JWT.verify(token, 'signKey_18902379108701');
// 判斷過時時間
if (user.expireAt < Date.now().valueOf()) return res.send('time out');
res.send(user);
};
複製代碼
token雖然不能保證被泄密,可是能夠保證攻擊者只能拿到token,並不能修改token。
簡單的帳號密碼存儲
export const login = async function(req, res, next) {
const { username, password } = req.body;
user.push({ username, password });
};
複製代碼
上面的示例是很是危險的,只要數據庫被攻擊,大量用戶名和密碼會被泄漏,或有數據庫訪問權限的人拿到數據庫的信息就拿到了全部的信息
node的密碼管理最佳實踐之一
import crypto from 'crypto'; // crypto加密
import blueBird from 'bluebird'
// pbkdf2加密是一個很是難破解的加密方式
const pbkdf2Async = blueBird.promisify(crypto.pbkdf2)
// 模擬user,實際開發中使用數據庫
const user = [];
export const login = async function(req, res, next) {
const { username, password } = req.query;
const cipher = await pbkdf2Async(
password,
'test_key_akjsdhaksjdhakjsdh',
10000,
512,
'sha256'
);
user.push({ username, cipher });
res.send(cipher);
};
複製代碼
此時cipher就是一個加密的串,固然除了密碼的加密以外,咱們還須要作密碼和用戶名的分表分庫,防止被脫褲。
那麼如何進行crypto加密狀況下的密碼驗證呢:
import crypto from 'crypto';
import blueBird from 'bluebird';
const pbkdf2Async = blueBird.promisify(crypto.pbkdf2);
// 模擬user
const user = [];
export const login = async function(req, res, next) {
const { username, password } = req.query;
const cipher = await pbkdf2Async(
password,
'test_key_akjsdhaksjdhakjsdh',
10000,
512,
'sha256'
);
user.push({ username, password: cipher });
res.send(cipher);
};
export const getUserByNamePass = async function(req, res, next) {
const { username, password } = req.query;
const cipher = await pbkdf2Async(
password,
'test_key_akjsdhaksjdhakjsdh',
10000,
512,
'sha256'
);
// 經過加密後的密碼進行對比,來驗證,增強了密碼的安全
const truePassword = user.find(k => {
return k.username === username && k.password.toString() === cipher.toString();
});
if (truePassword) {
res.send('login');
} else {
res.send('wrong password');
}
};
複製代碼
此時,只須要把加密配置的文件從一個安全的文件夾引入,就能安全的保護你的用戶密碼數據了