1. JSON Web Token
JWT是实现token技术的一种解决方案,是目前最流行的跨域身份验证解决方案。今天我们主要聊一聊基于nodeJS使用jsonwebtoken实现token。
1.1. JWT的基本组成
JWT对象是一个很长的字符串,看起来没有任何规律,通过“.”分隔符分为三个字串。字串之间不换行。如图所示,JWT Token包括三个部分。
1.1.1. Header
JWT的头部承载两部分信息
- 声明类型,这里是jwt
- 声明加密的算法
JWT元数据的JSON对象参考
{
"alg": "HS256",
"typ": "JWT"
}
1.1.2. Payload
和Header一样,Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段:
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
也可以自定义私有字段
1.1.3. signature
Signature 部分是对前两部分的签名,防止数据篡改。基本步骤如下:
- 指定一个密钥(secret)作为混淆。这个密钥只有服务器才知道,不能泄露给用户。
- 使用 Header 里面指定的签名算法(默认是 HMAC SHA256)按照一定的公式进行计算得到签名。
- 得到签名以后,把 Header、Payload、Signature 三个部分进行拼接,也就是前文讲到的长字符串
- 将该长字符串作为token返回给前端。
1.2. 用法
前端接收到token,可以将其存储在localStorage、sessionStorage中(注意两者区别),也可以存储在Cookie中。
注意:存储在cookie中可以自动发送,但是无法进行跨域
一般来讲,我们会将token放入请求头部的Authorization中,接下来我们会进行实验。
2. jsonwebtoken
实验的项目是基于nodeJS开发的后台,我们选用jsonwebtoken作为生成token的辅助库。
https://www.npmjs.com/package/jsonwebtoken
官方文档是个好东西,根据文档,基本配置如下:
/**
* 签名
* @param {*} rules 签名规则
* @param {*} secretOrKey 密钥
* @param {*} options token的属性
* @param {*} callback 回调函数
*/
jwt.sign(rules, secretOrKey, options, callback)
由此,我们根据项目需求,在登录确认账号密码无误之后,使用jsonwebtoken生成签名。
首先应该在相关文件导入jsonwebtoken。
// 导入依赖包
const jwt = require("jsonwebtoken")
登录验证核心代码如下,注意一点,生成的签名前面,必须加”Bearer “,这是作者的要求哦!
// 核心代码
if (doc.password === req.body.password) {
const rule = { id: doc._id, email: doc.email }
jwt.sign(rule, "secret", { expiresIn: 3600 }, (err, token) => {
if (err) {
res.json({
status: 500,
msg: "登录失败!"
})
} else {
res.json({
status: 200,
token: "Bearer " + token,
email: doc.email,
name: doc.name
})
}
})
}
推荐一款不错的插件,可以替代postman。
在VSCode使用REST Client进行测试,代码如下:
POST http://localhost:5000/api/user/login HTTP/1.1
content-type: application/json
{
"email": "xxxxxx@qq.com",
"password": "123456"
}
发起请求,结果如下:
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, PUT, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, Content-Length, X-Requested-With
Content-Type: application/json; charset=utf-8
Content-Length: 280
ETag: W/"118-3gBqMYUKarbx0KPF29gjiAi8UjI"
Date: Sun, 09 Feb 2020 11:20:11 GMT
Connection: close
{
"status": 200,
"token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlMjdiYzRhZjIwN2U5ZDc5OGUzN2RmMCIsImVtYWlsIjoiNzQxNTgxODc5QHFxLmNvbSIsImlhdCI6MTU4MTI0NzE4NiwiZXhwIjoxNTgxMjUwNzg2fQ.5hfAiTeSO9CEowEv9DW4vW9T2dh92awZEDkWiQqYXpg",
"email": "xxxxxx@qq.com",
"name": "Hans"
}
我们可以看到,后台成功给我们返回了token,将其存在sessionStorage中。为了让我们接下来每次请求都带上这个token,我们在前端写一个请求拦截,主要用于在请求头部增加一个字段Authorization,值为token,代码如下:
// axios请求拦截
axios.interceptors.request.use(config => {
NProgress.start()
config.headers.Authorization = window.sessionStorage.getItem('token')
return config
})
登录之后的请求都会带上token
3. passport-jwt
我们将使用这一辅助库对前端请求携带的token进行解析检验,如果检验通过,则放行,若不通过,则拦截,返回Unauthorized
https://www.npmjs.com/package/passport-jwt
官方文档有较为详细的说明,主要是要对JwtStrategy的实例化。
var JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
var opts = {}
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = 'secret';
opts.issuer = 'accounts.examplesoft.com';
opts.audience = 'yoursite.net';
passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
User.findOne({id: jwt_payload.sub}, function(err, user) {
if (err) {
return done(err, false);
}
if (user) {
return done(null, user);
} else {
return done(null, false);
// or you could create a new account
}
});
}));
我们首先应该在后台的主文件初始化passport,我的项目是server.js。
// 导入passport
const passport = require("passport");
// passport初始化
app.use(passport.initialize());
require("./config/passport")(passport);
config/passport.js文件代码如下:
var JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
//设置策略选项
let opts = {
// 密钥
secretOrKey: "secret",
// 定义从请求头的Authrization抽取token数据
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()
}
// 实例化策略对象
var jwtConfig = new JwtStrategy(opts, (jwt_payload, done) => {
// jwt_payload是经过解析的 token字符串,
console.log(jwt_payload);
User.findOne({ email: jwt_payload.email })
.exec((err, user) => {
if (err) {
// 若失败,则返回401,Authorization
return done(result.error, false);
} else {
if (user) {
// 成功则放行
return done(null, user);
} else {
// 若失败,则返回401,Authorization
return done(result.error, false);
}
}
});
});
module.exports = passport => {
passport.use(jwtConfig)
}
在server.js,应该利用passport.authenticate()进行验证拦截
passport.authenticate("加密算法策略", 验证条件,回调)
示例代码如下:
app.use("/api/records", passport.authenticate("jwt", { session: false }), recordsRouter)
到此为止,我们已经完成了整个JWT验证的所有配置,接下来进行测试。仍然使用REST Client进行测试,带上Authorization。
GET http://localhost:5000/api/records/histories?email=xxxxxx@qq.com HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlMjdiYzRhZjIwN2U5ZDc5OGUzN2RmMCIsImVtYWlsIjoiNzQxNTgxODc5QHFxLmNvbSIsImlhdCI6MTU4MTI0ODExNCwiZXhwIjoxNTgxMjUxNzE0fQ.ItfaSVJTWiENE9Hq9S7ygr-Fmiuk3WNXyVn85uprHdY
结果如下:
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, PUT, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, Content-Length, X-Requested-With
Content-Type: application/json; charset=utf-8
Content-Length: 2807
ETag: W/"af7-QTQanyH1ZISPRxlui9VxauIxQSA"
Date: Sun, 09 Feb 2020 11:57:15 GMT
Connection: close
{
"status": 200,
"msg": []
}
如果我们不在Header中加入Authorization,会发生说明呢?如下所示:
HTTP/1.1 401 Unauthorized
X-Powered-By: Express
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, PUT, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, Content-Length, X-Requested-With
Date: Sun, 09 Feb 2020 12:00:24 GMT
Connection: close
Content-Length: 12
Unauthorized
返回状态码401,以及Unauthorized,说明未验证通过。
3. 结语
花了一点时间,总结一下jwt,还是收获不少的。希望大家能够从中获益,能够帮助到大家,是一件非常开心的事情。如果有什么不对的地方,还望指正。