基於JWT標準的使用者認證介面實現

小火柴的藍色理想發表於2018-04-11

前面的話

  實現使用者登入認證的方式常見的有兩種:一種是基於 cookie 的認證,另外一種是基於 token 的認證 。本文以基於cookie的認證為參照,詳細介紹JWT標準,並實現基於該標籤的使用者認證介面

 

cookie認證

  傳統的基於 cookie 的認證方式基本有下面幾個步驟:

  1、使用者輸入使用者名稱和密碼,傳送給伺服器

  2、伺服器驗證使用者名稱和密碼,正確的話就建立一個會話( session ),同時會把這個會話的 ID 儲存到客戶端瀏覽器中,因為儲存的地方是瀏覽器的 cookie ,所以這種認證方式叫做基於 cookie 的認證方式

  3、後續的請求中,瀏覽器會傳送會話 ID 到伺服器,伺服器上如果能找到對應 ID 的會話,那麼伺服器就會返回需要的資料給瀏覽器

  4、當使用者退出登入,會話會同時在客戶端和伺服器端被銷燬

  這種認證方式的不足之處有兩點

  1、伺服器端要為每個使用者保留 session 資訊,連線使用者多了,伺服器記憶體壓力巨大

  2、適合單一域名,不適合第三方請求

  cookie認證的後端典型程式碼如下所示

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: false }));

const session = require('express-session')
const pug = require('pug');

app.set('view engine', 'pug');

app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true
}))


app.get('/', function(req, res){
  let currentUser = req.session.username;
  res.render('index', {currentUser});
})

app.get('/login', function(req, res){
  res.sendFile('login.html', {root: 'public'});
})

app.post('/login', function(req, res){
  let username = req.body.username;
  req.session.username = username;
  res.redirect('/');
})

app.get('/logout', function(req, res){
  req.session.destroy();
  res.redirect('/');
})

app.listen(3006, function(){
  console.log('running on port 3006...');
})

 

token認證

  下面來介紹token認證。詳細認證過程如下

  1、使用者輸入使用者名稱密碼,傳送給伺服器

  2、伺服器驗證使用者名稱和密碼,正確的話就返回一個簽名過的 token( token 可以認為就是個長長的字串),客戶端瀏覽器拿到這個 token

  3、後續每次請求中,瀏覽器會把 token 作為 http header 傳送給伺服器,伺服器可以驗證一下簽名是否有效,如果有效那麼認證就成功了,可以返回客戶端需要的資料

  4、一旦使用者退出登入,只需要客戶端銷燬一下 token 即可,伺服器端不需要有任何操作

  這種方式的特點就是客戶端的 token 中自己保留有大量資訊,伺服器沒有儲存這些資訊,而只負責驗證,不必進行資料庫查詢,執行效率大大提高

 

JWT

  上面介紹的token-based 認證過程是通過 JWT 標準來完成的

  JWT 是 JSON Web Token 的簡寫,它定義了一種在客戶端和伺服器端安全傳輸資料的規範。通過 JSON 格式 來傳遞資訊

  讓我們來假想一下一個場景。在A使用者關注了B使用者的時候,系統發郵件給B使用者,並且附有一個連結“點此關注A使用者”。連結的地址可以是這樣的

https://your.awesome-app.com/make-friend/?from_user=B&target_user=A

  上面這樣做有一個弊端,那就是要求使用者B一定要先登入。可不可以簡化這個流程,讓B使用者不用登入就可以完成這個操作。JWT允許我們做到這點

【組成】

  一個JWT實際上就是一個字串,它由三部分組成,第一段是 header (頭部),第二段是 payload (主體資訊或稱為載荷),第三段是 signature(數字簽名)

aaaaaaaaaa.bbbbbbbbbbb.cccccccccccc

  頭部用於描述關於該JWT的最基本的資訊,例如其型別以及簽名所用的演算法等。這可以被表示成一個JSON物件

{
  "typ": "JWT",
  "alg": "HS256"
}

  將上面的新增好友的操作描述成一個JSON物件。其中新增了一些其他的資訊,幫助今後收到這個JWT的伺服器理解這個JWT

{
    "iss": "John Wu JWT",
    "iat": 1441593502,
    "exp": 1441594722,
    "aud": "www.example.com",
    "sub": "jrocket@example.com",
    "from_user": "B",
    "target_user": "A"
}

  將上面的JSON物件進行[base64編碼]可以得到下面的字串。這個字串稱作JWT的Payload(載荷)

eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9

  將上面的兩個編碼後的字串都用句號.連線在一起(頭部在前)

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0

  最後,我們將上面拼接完的字串用HS256演算法進行加密。在加密的時候,我們還需要提供一個金鑰(secret)。如果我們用mystar作為金鑰的話,那麼就可以得到我們加密後的內容。這一部分叫做簽名

rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

  最後將這一部分簽名也拼接在被簽名的字串後面,我們就得到了完整的JWT

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

  於是,我們就可以將郵件中的URL改成

https://your.awesome-app.com/make-friend/?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

  再強調一下數字簽名的運算過程

var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);

HMACSHA256(encodedString, 'secret');

  簽名是由伺服器完成的,secret 是伺服器上儲存的金鑰,資訊簽名後整個 token 會傳送給瀏覽器,每次瀏覽器 傳送請求中都包含 secret。所以可以跟伺服器達成互信,完成認證過程

認證介面

  新建 server/routes.js 檔案,匯入 User 模型並賦值給 User 變數:

let User = require('./models/user')

  接下來定義使用者認證介面,將實現的介面名稱為 /auth/login:

module.exports = app => {
  app.post('/auth/login', (req, res) => {
    User.findOne({ username: req.body.username }, (err, user) => {
      if (err) return console.log(err)
      if (!user) return res.status(403).json({ error: '使用者名稱不存在!' })
      user.comparePassword(req.body.password, (err, isMatch) => {
        if (err) return console.log(err)
        if (!isMatch) return res.status(403).json({ error: '密碼無效!' })
        return res.json({
          token: generateToken({ name: user.username }),
          user: { name: user.username }
        })
      })
    })
  })
}

  使用者從客戶端向伺服器提交使用者名稱和密碼,伺服器端通過body-parser中介軟體把客戶端傳送過的資料抽取出來並存放到 req.body 中,這樣就可以通過 req.body.username 獲取到使用者名稱。然後在 MongoDB 資料庫中查詢這個使用者,若查詢過程中出錯,則列印錯誤資訊到終端;若資料庫中不存在這個使用者,則向客戶端響應錯誤資訊;若資料庫中存在這個使用者,則驗證客戶端提交的密碼 req.body.password 是否與使用者儲存在資料庫中的密碼匹配。若密碼不匹配,則向客戶端返回錯誤資訊;若密碼匹配,則給客戶端返回使用者資訊

  使用NPM安裝jsonwebtoken包,jsonwebtoken 包可以生成、驗證和解碼 JWT 認證碼

npm install --save jsonwebtoken

  開啟 server/routes.js 檔案,匯入 jsonwebtoken 模組:

let jwt = require('jsonwebtoken')

  然後,定義生成 JWT 的 generateToken 方法

let generateToken = (user) => {
  return jwt.sign(user, 'xiaohuochai', { expiresIn: 3000 })
}

  呼叫 jsonwebtoken 模組提供的 sign() 介面生成 JWT。 其中,xiaohuochai 是生成 JWT 認證碼的祕鑰,為了安全,最好把祕鑰放到配置檔案中。 user 是要傳遞給前端的資訊,前端可以利用工具解碼 JWT 認證碼,從而得到 user 資料。 expiresIn 選項用來指定認證碼自生成到失效的時間間隔(過期間隔),上述程式碼中數字 3000 的單位是秒,意思說這個認證碼自生成後,再過50分鐘就失效了。認證碼失效之後,客戶端就不能使用失效的認證碼訪問伺服器端的受保護資源了

  完整程式碼如下

let User = require('./models/user')
let jwt = require('jsonwebtoken')
let secret = require('./config.js').secret
let generateToken = (user) => {
  return jwt.sign(user, secret, { expiresIn: 3000 })
}
module.exports = app => {
  app.post('/auth/login', (req, res) => {
    User.findOne({ username: req.body.username }, (err, user) => {
      if (err) return console.log(err)
      if (!user) return res.status(403).json({ error: '使用者名稱不存在!' })
      user.comparePassword(req.body.password, (err, isMatch) => {
        if (err) return console.log(err)
        if (!isMatch) return res.status(403).json({ error: '密碼無效!' })
        return res.json({
          token: generateToken({ name: user.username }),
          user: { name: user.username }
        })
      })
    })
  })
}

  最後在index.js中引入並使用routes

let routes = require('./routes.js')
routes(app)

  使用postman來測試介面,已經在資料庫中存了使用者名稱為admin,密碼為123456的使用者。測試結果如下

 

最後

  JWT適合於應用在『無狀態的REST API』,也就是說適用於Android/iOS等移動端,或前後端分離的WEB前端。關於JWT的更多資源移步官網

 

相關文章