node專案的鑑權和密碼管理

li_nuo發表於2018-12-25

http是無狀態的請求,上一次請求和下一次請求之間沒有什麼必然的聯絡,且是明文的協議,,每一次http請求,都相當於拿著喇叭在網路上喊,我的賬號密碼是什麼。為了解決這個問題,引入了cookie和session機制

使用者每一次的請求都會帶有cookie,當我們拿到使用者的cookie之後,再去資料庫裡或者記憶體裡邊看看,這個使用者是否已經登陸過了,這個cookie是不是我給他的,如果是我給的,他就有許可權使用一些功能

使用cookie能夠訪問我們使用者相應的內容,這樣看起來和賬號密碼功能差不多了。但是和賬號密碼的區別是,cookie是由後端加密的,資訊雖然能被攻擊者拿到,但是不能被篡改(如果在客戶端做加密,也等於是透明瞭,差不多也等於是廣播)

本文使用express框架,使用import語法因為我是在typescript專案內做的,當作require來看就行,不影響食用

cookie

cookie使用

下面使用setCookie更改cookie

// 這裡是路由
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過期時間,當伺服器重啟後使用者的會話資訊就會丟掉。更好的解決方案是把使用者的登入資訊放到資料庫裡邊

cookieSession

cookiesession是一個會話管理解決方案,express官網上有推薦

下邊使用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部分如下

cookie名稱: testCookieSession
cookie值: eyJ1c2VyIjp7InVzZXJuYW1lIjoibnVvIn19	// base64簡單加密的傳輸內容

cookie名稱: testCookieSession.sig	// 這裡是cookie的簽名,根據上面設定的金鑰和解密出來的內容得到的
cookie值: gTty0Cinxf8r0FwhmS8lz-TdM7I
複製程式碼

這是一個經過簡單加密的cookie,再訪問/hello,依然返回我們設定的username,值是nuo

cookieSession這個中介軟體使用的是一套成熟的體系,幫我們解決了最開始程式碼我們處理的那些細節。在這裡的體現:拿著同一套cookie的瀏覽器發過來的請求,自動把session給掛到request上。

jsonWebToken

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
複製程式碼

此時傳送請求,返回的是上邊解密出來的值

為什麼要使用cookieSession或JWT

把資料存到會話管理系統裡邊的原因是因為,無狀態的http請求,對於分散式管理的系統非常不友好,如果登入資訊存在記憶體裡,伺服器重啟,使用者需要重新登陸。或者有多臺伺服器,使用者在一臺伺服器上登入後,想訪問另一臺伺服器上的內容,又需要進行重新登入,也會非常的麻煩。解決這種情況有幾種方式:

  1. 通過叢集的資料庫,比如redis,mongodb,把session統一的管理起來,或者每個伺服器儲存唯一的使用者資訊,其他伺服器需要的時候來這臺伺服器上取(分片式儲存)

  2. 把使用者的不敏感資訊存放到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);
    };
    複製程式碼
  3. 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');
  }
};
複製程式碼

此時,只需要把加密配置的檔案從一個安全的資料夾引入,就能安全的保護你的使用者密碼資料了

歡迎訪問我的部落格

相關文章