本文是此文的缩略翻译版, 更详细的内容请参考原文. 本文在原文基础上更正了Bearer
的问题, 还有自己的一些更新.
本文讲解下如何在express环境下, 使用passport进行JWT身份验证.
目标:
/login
用于登录获取token/secret
仅对有合法token的用户可访问
工具:
Postman用于测试发送请求
Node, npm
准备工作
创建项目文件夹. 运行以下命令初始化及安装必要的包.
npm init -y npm install --save express body-parser passport passport-jwt jsonwebtoken
创建如下index.js
// file: index.jsvar 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, 它可以监听文件变化, 自动重启服务器, 启动服务器的命令为nodemon index.js
.
index
登录
// file: index.jsvar 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;
创建测试用的用户数组:
var users = [ { id: 1, name: 'jonathanmh', password: '%2yx4' }, { id: 2, name: 'test', password: 'test' } ];
注意, 实际应用中绝对不要明文保存密码.
使用bcrypt加密
passport.js有策略(strategy)的概念. strategy是一些预定义的方法, 它们会在请求抵达真正的路由之前执行. 如果你定义的strategy认定某个请求非法, 则该路由不会被执行, 而是返回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 formsapp.use(bodyParser.urlencoded({ extended: true}));// parse application/jsonapp.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
type: x-www-form-urlencoded
login failed
login success
创建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
inside Headers: 添加一项, Key为Authorization, 字段为
Bearer {token}
. 其中{token}
代表前面获得的token字符串.
secret
测试秘密路由
可以创建一个测试用的秘密路由用于打印接收到的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 runningJWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNDc3MTM0NzM4fQ.Ky3iKYcguIstYPDbMbIbDR5s7e_UF0PI1gal6VX5eyI
更新
如何在自定义的路由中访问payload?
自己定义的strategy中的next(null, user);
会将这个user
的信息写入req.user
因此, 你可以自定义next
的第二个参数, 比如向其中写入一些payload的数据, 然后通过req.user
访问那些数据.
注意文中直接返回了user
, 也就是将所有user
信息, 包含password都返回给了req.user
. 自己实现的时候返回必要的信息就行了.
如何在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...蛋疼.
参考
作者:柳正来
链接:https://www.jianshu.com/p/dc9a3302b92a