Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node.js 应用:Koa2 使用 JWT 进行鉴权 #28

Open
lin-xin opened this issue Aug 16, 2018 · 18 comments
Open

Node.js 应用:Koa2 使用 JWT 进行鉴权 #28

lin-xin opened this issue Aug 16, 2018 · 18 comments
Labels

Comments

@lin-xin
Copy link
Owner

lin-xin commented Aug 16, 2018

前言

在前后端分离的开发中,通过 Restful API 进行数据交互时,如果没有对 API 进行保护,那么别人就可以很容易地获取并调用这些 API 进行操作。那么服务器端要如何进行鉴权呢?

Json Web Token 简称为 JWT,它定义了一种用于简洁、自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。

说得好像跟真的一样,那么到底要怎么进行认证呢?

jwt流程图

首先用户登录时,输入用户名和密码后请求服务器登录接口,服务器验证用户名密码正确后,生成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,可以做为参考。

@lin-xin lin-xin added the node label Aug 16, 2018
@Elity
Copy link

Elity commented Aug 17, 2018

那么问题来了,服务端怎么单方面作废一个用户的登陆状态?

@lin-xin
Copy link
Owner Author

lin-xin commented Aug 17, 2018

服务端是不能让jwt在有效期内变得无效的,但是可以让即使这个token有效,也可以让用户的登录状态失效:

    1. 需要给用户添加一个字段,通过这个字段和token一起验证登录状态
    1. 也可以把这个token加进黑名单

@GitHdu
Copy link

GitHdu commented Aug 17, 2018

小白问几个问题:

  1. token放localStorage里面会有安全问题吗

2.例子中好像没有token验证的代码

@lin-xin
Copy link
Owner Author

lin-xin commented Aug 17, 2018

@GitHdu

  1. 如果是在公共电脑登录,绝对不安全
  2. koa-jwt已经封装了,想看的话可以看node_modules里的代码,上面已经有解析koa-jwt的代码了,再具体的话还可以看 jsonwebtoken 里的代码

@webproblem
Copy link

客户端发起请求的时候,是不是应该将 token 进行加密更安全点呢,还有登录的时候,password 也应该加密保证安全吧

@Elity
Copy link

Elity commented Aug 18, 2018

@lin-xin
如果需在在jwt中加额外的字段来标明是否失效,那么意味着服务端也需要存储这个字段,这与jwt的设计初衷是违背的。
把这个token作废也需要服务端单独存在这个token,下次再通过这个token访问才知道到底作废没,还是违背了jwt的设计初衷

@samfung09
Copy link

小白提问
如果当前我登录成功了页面要发生跳转,是url跳转并非ajax请求,这时要怎么带token给后台验证啊?

@lin-xin
Copy link
Owner Author

lin-xin commented Aug 20, 2018

@Elity 是的,jwt的设计并没有让服务器去使token失效的方法,而是通过过期时间,而且不建议设置过期时间太久。不知你有其他更好的方法不?

@Elity
Copy link

Elity commented Aug 22, 2018

@lin-xin jwt对这个貌似是无解的。 所以,如果业务有这样的需求,那就没办法使用jwt了

@dingjb
Copy link

dingjb commented Aug 23, 2018

@Elity 虽然和 jwt 设计的初衷是违背的,但不代表就不能用这种方式,只能说你可以用这种方式去补充 jwt 的缺点。

@cppfuns
Copy link

cppfuns commented Nov 20, 2018

可以数据库 存下这个token? 每次jwt 验证完,然后在查数据库,对比此token是否已变更?

@heiliuer
Copy link

那么问题来了,服务端怎么单方面作废一个用户的登陆状态?

secret 有两部分组成,一部分全局的,一部分包含在用户数据里的。

@grace618
Copy link

kow-jwt 可以验证token是否正确和解析的话,就不要用verify了吧,是吗

@MinWest
Copy link

MinWest commented May 22, 2019

token过期后更换新的token 有什么好的实现方式吗,koa的插件貌似得自己实现这些

@Vibing
Copy link

Vibing commented Jun 22, 2019

@MinWest Vibing/blog#7

@lin-xin localStorage.setItem('token_exp', new Date().getTime());中 token_exp 是用来做什么?

@EPSON-LEE
Copy link

@MinWest Vibing/blog#7

@lin-xin localStorage.setItem('token_exp', new Date().getTime());中 token_exp 是用来做什么?

个人理解是记录存入 token 时的时间戳

@plh97
Copy link

plh97 commented Dec 2, 2020

直接存cookie不行吗

@plh97
Copy link

plh97 commented Dec 2, 2020

为啥还得存localstorage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests