blog
blog copied to clipboard
Node.js 应用:Koa2 使用 JWT 进行鉴权
前言
在前后端分离的开发中,通过 Restful API 进行数据交互时,如果没有对 API 进行保护,那么别人就可以很容易地获取并调用这些 API 进行操作。那么服务器端要如何进行鉴权呢?
Json Web Token 简称为 JWT,它定义了一种用于简洁、自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。
说得好像跟真的一样,那么到底要怎么进行认证呢?
首先用户登录时,输入用户名和密码后请求服务器登录接口,服务器验证用户名密码正确后,生成token并返回给前端,前端存储token,并在后面的请求中把token带在请求头中传给服务器,服务器验证token有效,返回正确数据。
既然服务器端使用 Koa2 框架进行开发,除了要使用到 jsonwebtoken 库之外,还要使用一个 koa-jwt 中间件,该中间件针对 Koa 对 jsonwebtoken 进行了封装,使用起来更加方便。下面就来看看是如何使用的。
生成token
这里注册了个 /login
的路由,用于用户登录时获取token。
const router = require('koa-router')();
const jwt = require('jsonwebtoken');
const userModel = require('../models/userModel.js');
router.post('/login', async (ctx) => {
const data = ctx.request.body;
if(!data.name || !data.password){
return ctx.body = {
code: '000002',
data: null,
msg: '参数不合法'
}
}
const result = await userModel.findOne({
name: data.name,
password: data.password
})
if(result !== null){
const token = jwt.sign({
name: result.name,
_id: result._id
}, 'my_token', { expiresIn: '2h' });
return ctx.body = {
code: '000001',
data: token,
msg: '登录成功'
}
}else{
return ctx.body = {
code: '000002',
data: null,
msg: '用户名或密码错误'
}
}
});
module.exports = router;
在验证了用户名密码正确之后,调用 jsonwebtoken 的 sign() 方法来生成token,接收三个参数,第一个是载荷,用于编码后存储在 token 中的数据,也是验证 token 后可以拿到的数据;第二个是密钥,自己定义的,验证的时候也是要相同的密钥才能解码;第三个是options,可以设置 token 的过期时间。
获取token
接下来就是前端获取 token,这里是在 vue.js 中使用 axios 进行请求,请求成功之后拿到 token 保存到 localStorage 中。这里登录成功后,还把当前时间存了起来,除了判断 token 是否存在之外,还可以再简单的判断一下当前 token 是否过期,如果过期,则跳登录页面
submit(){
axios.post('/login', {
name: this.username,
password: this.password
}).then(res => {
if(res.code === '000001'){
localStorage.setItem('token', res.data);
localStorage.setItem('token_exp', new Date().getTime());
this.$router.push('/');
}else{
alert(res.msg);
}
})
}
然后请求服务器端API的时候,把 token 带在请求头中传给服务器进行验证。每次请求都要获取 localStorage 中的 token,这样很麻烦,这里使用了 axios 的请求拦截器,对每次请求都进行了取 token 放到 headers 中的操作。
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
config.headers.common['Authorization'] = 'Bearer ' + token;
return config;
})
验证token
通过 koa-jwt 中间件来进行验证,用法也非常简单
const koa = require('koa');
const koajwt = require('koa-jwt');
const app = new koa();
// 错误处理
app.use((ctx, next) => {
return next().catch((err) => {
if(err.status === 401){
ctx.status = 401;
ctx.body = 'Protected resource, use Authorization header to get access\n';
}else{
throw err;
}
})
})
app.use(koajwt({
secret: 'my_token'
}).unless({
path: [/\/user\/login/]
}));
通过 app.use 来调用该中间件,并传入密钥 {secret: 'my_token'}
,unless 可以指定哪些 URL 不需要进行 token 验证。token 验证失败的时候会抛出401错误,因此需要添加错误处理,而且要放在 app.use(koajwt()) 之前,否则不执行。
如果请求时没有token或者token过期,则会返回401。
解析koa-jwt
我们上面使用 jsonwebtoken 的 sign() 方法来生成 token 的,那么 koa-jwt 做了些什么帮我们来验证 token。
resolvers/auth-header.js
module.exports = function resolveAuthorizationHeader(ctx, opts) {
if (!ctx.header || !ctx.header.authorization) {
return;
}
const parts = ctx.header.authorization.split(' ');
if (parts.length === 2) {
const scheme = parts[0];
const credentials = parts[1];
if (/^Bearer$/i.test(scheme)) {
return credentials;
}
}
if (!opts.passthrough) {
ctx.throw(401, 'Bad Authorization header format. Format is "Authorization: Bearer <token>"');
}
};
在 auth-header.js 中,判断请求头中是否带了 authorization,如果有,将 token 从 authorization 中分离出来。如果没有 authorization,则代表了客户端没有传 token 到服务器,这时候就抛出 401 错误状态。
verify.js
const jwt = require('jsonwebtoken');
module.exports = (...args) => {
return new Promise((resolve, reject) => {
jwt.verify(...args, (error, decoded) => {
error ? reject(error) : resolve(decoded);
});
});
};
在 verify.js 中,使用 jsonwebtoken 提供的 verify() 方法进行验证返回结果。jsonwebtoken 的 sign() 方法来生成 token 的,而 verify() 方法则是用来认证和解析 token。如果 token 无效,则会在此方法被验证出来。
index.js
const decodedToken = await verify(token, secret, opts);
if (isRevoked) {
const tokenRevoked = await isRevoked(ctx, decodedToken, token);
if (tokenRevoked) {
throw new Error('Token revoked');
}
}
ctx.state[key] = decodedToken; // 这里的key = 'user'
if (tokenKey) {
ctx.state[tokenKey] = token;
}
在 index.js 中,调用 verify.js 的方法进行验证并解析 token,拿到上面进行 sign() 的数据 {name: result.name, _id: result._id}
,并赋值给 ctx.state.user
,在控制器中便可以直接通过 ctx.state.user
拿到 name
和 _id
。
安全性
- 如果 JWT 的加密密钥泄露的话,那么就可以通过密钥生成 token,随意的请求 API 了。因此密钥绝对不能存在前端代码中,不然很容易就能被找到。
- 在 HTTP 请求中,token 放在 header 中,中间者很容易可以通过抓包工具抓取到 header 里的数据。而 HTTPS 即使能被抓包,但是它是加密传输的,所以也拿不到 token,就会相对安全了。
总结
这上面就是 jwt 基本的流程,这或许不是最完美的,但在大多数登录中使用已经足够了。 上面的代码可能不够具体,这里使用 Koa + mongoose + vue.js 实现的一个例子 : jwt-demo,可以做为参考。
那么问题来了,服务端怎么单方面作废一个用户的登陆状态?
服务端是不能让jwt在有效期内变得无效的,但是可以让即使这个token有效,也可以让用户的登录状态失效:
-
- 需要给用户添加一个字段,通过这个字段和token一起验证登录状态
-
- 也可以把这个token加进黑名单
小白问几个问题:
- token放localStorage里面会有安全问题吗
2.例子中好像没有token验证的代码
@GitHdu
- 如果是在公共电脑登录,绝对不安全
- koa-jwt已经封装了,想看的话可以看node_modules里的代码,上面已经有解析koa-jwt的代码了,再具体的话还可以看 jsonwebtoken 里的代码
客户端发起请求的时候,是不是应该将 token 进行加密更安全点呢,还有登录的时候,password 也应该加密保证安全吧
@lin-xin
如果需在在jwt中加额外的字段来标明是否失效,那么意味着服务端也需要存储这个字段,这与jwt的设计初衷是违背的。
把这个token作废也需要服务端单独存在这个token,下次再通过这个token访问才知道到底作废没,还是违背了jwt的设计初衷
小白提问 如果当前我登录成功了页面要发生跳转,是url跳转并非ajax请求,这时要怎么带token给后台验证啊?
@Elity 是的,jwt的设计并没有让服务器去使token失效的方法,而是通过过期时间,而且不建议设置过期时间太久。不知你有其他更好的方法不?
@lin-xin jwt对这个貌似是无解的。 所以,如果业务有这样的需求,那就没办法使用jwt了
@Elity 虽然和 jwt 设计的初衷是违背的,但不代表就不能用这种方式,只能说你可以用这种方式去补充 jwt 的缺点。
可以数据库 存下这个token? 每次jwt 验证完,然后在查数据库,对比此token是否已变更?
那么问题来了,服务端怎么单方面作废一个用户的登陆状态?
secret 有两部分组成,一部分全局的,一部分包含在用户数据里的。
kow-jwt 可以验证token是否正确和解析的话,就不要用verify了吧,是吗
token过期后更换新的token 有什么好的实现方式吗,koa的插件貌似得自己实现这些
@MinWest https://github.com/Vibing/blog/issues/7
@lin-xin localStorage.setItem('token_exp', new Date().getTime());
中 token_exp 是用来做什么?
@MinWest Vibing/blog#7
@lin-xin
localStorage.setItem('token_exp', new Date().getTime());
中 token_exp 是用来做什么?
个人理解是记录存入 token 时的时间戳
直接存cookie不行吗
为啥还得存localstorage