本文是此文的缩略翻译版, 更详细的内容请参考原文. 本文在原文基础上更正了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.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, 它可以监听文件变化, 自动重启服务器, 启动服务器的命令为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;
创建测试用的用户数组:
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是一些预定义的方法, 它们会在请求抵达真正的路由之前执行. 如果你定义的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 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
创建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字符串.
测试秘密路由
可以创建一个测试用的秘密路由用于打印接收到的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
的第二个参数, 比如向其中写入一些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...蛋疼.
网友评论