本文是此文的縮略翻譯版, 更詳細的內(nèi)容請參考原文. 本文在原文基礎上更正了Bearer的問題, 還有自己的一些更新.
本文講解下如何在express環(huán)境下, 使用passport進行JWT身份驗證.
目標:
-
/login用于登錄獲取token -
/secret僅對有合法token的用戶可訪問
工具:
- Postman用于測試發(fā)送請求
- Node, npm
準備工作
創(chuàng)建項目文件夾. 運行以下命令初始化及安裝必要的包.
npm init -y
npm install --save express body-parser passport passport-jwt jsonwebtoken
創(chuàng)建如下index.js
// file: index.js
var express = require("express");
var app = express();
app.get("/", function(req, res) {
res.json({message: "Express is up!"});
});
app.listen(3000, function() {
console.log("Express running");
});
運行node index.js即可啟動服務器. 強烈建議安裝使用nodemon, 它可以監(jiān)聽文件變化, 自動重啟服務器, 啟動服務器的命令為nodemon index.js.

登錄
// file: index.js
var express = require("express");
var bodyParser = require("body-parser");
var jwt = require('jsonwebtoken');
var passport = require("passport");
var passportJWT = require("passport-jwt");
var ExtractJwt = passportJWT.ExtractJwt;
var JwtStrategy = passportJWT.Strategy;
創(chuàng)建測試用的用戶數(shù)組:
var users = [
{
id: 1,
name: 'jonathanmh',
password: '%2yx4'
},
{
id: 2,
name: 'test',
password: 'test'
}
];
注意, 實際應用中絕對不要明文保存密碼.
- 使用bcrypt加密
- 讀一讀The OWASP wikis Password Storage Cheat Sheet
passport.js有策略(strategy)的概念. strategy是一些預定義的方法, 它們會在請求抵達真正的路由之前執(zhí)行. 如果你定義的strategy認定某個請求非法, 則該路由不會被執(zhí)行, 而是返回401 Unauthorized.
var jwtOptions = {}
jwtOptions.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
jwtOptions.secretOrKey = 'tasmanianDevil';
var strategy = new JwtStrategy(jwtOptions, function(jwt_payload, next) {
console.log('payload received', jwt_payload);
// usually this would be a database call:
var user = users.find(user => user.id === jwt_payload.id);
if (user) {
next(null, user);
} else {
next(null, false);
}
});
passport.use(strategy);
接下來添加登錄路由:
var app = express();
app.use(passport.initialize());
// parse application/x-www-form-urlencoded
// for easier testing with Postman or plain HTML forms
app.use(bodyParser.urlencoded({
extended: true
}));
// parse application/json
app.use(bodyParser.json())
app.post("/login", function(req, res) {
if(req.body.name && req.body.password){
var name = req.body.name;
var password = req.body.password;
}
// usually this would be a database call:
var user = users.find(user => user.name === name);
if( ! user ){
res.status(401).json({message:"no such user found"});
}
if(user.password === req.body.password) {
// from now on we'll identify the user by the id and the id is the only personalized value that goes into our token
var payload = {id: user.id};
var token = jwt.sign(payload, jwtOptions.secretOrKey);
res.json({message: "ok", token: token});
} else {
res.status(401).json({message:"passwords did not match"});
}
});
我們定義的payload中只有id一個claim.
打開Postman:
- method: POST
- URL: http://localhost:3000
- type: x-www-form-urlencoded


創(chuàng)建JWT驗證的秘密路由
app.get("/secret", passport.authenticate('jwt', { session: false }), function(req, res){
res.json("Success! You can not see this without a token");
});
打開Postman:
- method: GET
- URL: http://localhost:3000/secret
- inside Headers: 添加一項, Key為Authorization, 字段為
Bearer {token}. 其中{token}代表前面獲得的token字符串.

測試秘密路由
可以創(chuàng)建一個測試用的秘密路由用于打印接收到的JWT token.
app.get("/secretDebug",
function(req, res, next){
console.log(req.get('Authorization'));
next();
}, function(req, res){
res.json("debugging");
});
[nodemon] restarting due to changes...
[nodemon] starting `node index.js`
Express running
JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNDc3MTM0NzM4fQ.Ky3iKYcguIstYPDbMbIbDR5s7e_UF0PI1gal6VX5eyI
更新
如何在自定義的路由中訪問payload?
自己定義的strategy中的next(null, user);會將這個user的信息寫入req.user
因此, 你可以自定義next的第二個參數(shù), 比如向其中寫入一些payload的數(shù)據(jù), 然后通過req.user訪問那些數(shù)據(jù).
注意文中直接返回了user, 也就是將所有user信息, 包含password都返回給了req.user. 自己實現(xiàn)的時候返回必要的信息就行了.
如何在browser訪問userId?
login方法返回了{ message: 'ok', token: token }, 其中token包含了userId的信息.
token的結構是header.payload.signature.
Token中的header和payload是base64url編碼的, 它本身是用HMACSHA256算法進行簽名的.
所以對payload進行base64解壓縮即可, 瀏覽器有相應的atob解壓, btoa壓縮, 詳見Base64 encoding and decoding.
function getToken() {
let token = localStorage.getJson('token');
if (!token) {
return undefined;
}
let parts = token.split('.');
if (parts.length !== 3) {
return undefined;
}
let payload = parts[1];
return JSON.parse(base64url.decode(payload));
}
更新
注意! 踩了個坑!! header和payload是base64url編碼的(詳見rfc7519), 不是base64! 它們之間有一些細微的差別, 比如base64中的+和/在base64url中需要被轉換為-和_!
總之, 有一個包叫做base64url, 用這個庫解壓payload就對了! 別用base64!
我還整理了一個base64編解碼的文章, 結果搞了半天不是用base64...蛋疼.