基於JWT標准的用戶認證接口實現


前面的話

  實現用戶登錄認證的方式常見的有兩種:一種是基於 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的更多資源移步官網

 


免責聲明!

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



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