[1]cookie認證 [2]token認證 [3]JWT [4]認證介面 ...
前面的話
實現用戶登錄認證的方式常見的有兩種:一種是基於 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": "[email protected]", "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的更多資源移步官網