随着技术的发展,分布式web应用的普及,通过session管理用户登录状态成本越来越高,因此慢慢发展成为token的方式做登录身份校验,然后通过token去取redis中的缓存的用户信息。随着之后jwt的出现,校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录更为简单
JSON Web Token由三部分组成,它们之间用圆点(.)连接,header.payload.signature
$ npm install jsonwebtoken --save
jsonwebtoken用于创建和验证token
使用默认的HMAC SHA256生成
var jwt = require('jsonwebtoken');
var token = jwt.sign({ foo: 'bar' }, 'secret_shhhhh');
设置过期时间(1h过期)
// 方式一
jwt.sign({
exp: Math.floor(Date.now() / 1000) + (60 * 60),
data: 'foobar'
}, 'secret');
// 方式二 推荐使用
jwt.sign({data: 'foobar'}, 'secret', { expiresIn: '1h' });
如果没有配置过期时间,则默认永不过期
eg:登录获取token
router.post('/token', async (ctx, next) => {
let columnNames = ['admin_id','role_id','user_name','real_name','nick_name','portrait']
let sql = `SELECT ${columnNames.join()} FROM admin WHERE user_name = ? AND password = ?`
const md5Password = md5(password).toString()
// 查不到数据时rows=[]
const [rows,fields] = await mysqlPool.execute(sql, [userName,md5Password]);
if(rows[0]){
console.log('login success')
// 创建token
const token = jwt.sign(rows[0], 'secret', { expiresIn: '2h' });
ctx.body = {success: true, data: token}
}else{
// 账号或密码错误
console.log('login failed')
ctx.body = {success: false, message: '账号或密码错误'}
}
});
校验的同时会返回解码后的payload
// verify a token symmetric - synchronous
var decoded = jwt.verify(token, 'secret_shhhhh');
console.log(decoded) // { foo: 'bar', iat: 1644312929 }
// 如果想要把3段都输出来,即{ header, payload, signature }
var decoded1 = jwt.verify(token, 'secret_shhhhh', {complete: true});
/* {
header: { alg: 'HS256', typ: 'JWT' },
payload: { foo: 'bar', iat: 1644312990 },
signature: 'hTf9bM3S4Y9XaoRRr9LVWecDnPHZ7n4Ma9CrnmjRqSE'
}*/
如果解码失败
// invalid token - synchronous
try {
var decoded = jwt.verify(token, 'wrong-secret');
} catch(err) {
// err
// 如果超时 err.message = 'jwt expired'
// 如果token解析不了 err.message = 'invalid token'
// 如果签名错误 err.message = 'invalid signature'
// 其他错误查看GitHub
// ctx.throw(401, 'token error');
}
github:https://github.com/auth0/node-jsonwebtoken
用于在所有请求的前置做token的校验,并自动解码token里的payload,然后放在ctx.state.user对象里。
当然也可以自己写个中间件,然后在中间件里使用jwt.verify来校验
npm install koa-jwt --save
GitHub:https://github.com/koajs/jwt
由于koa-jwt依赖了jsonwebtoken,所以安装了koa-jwt之后可以不用安装jsonwebtoken,可以直接var jwt = require(‘jsonwebtoken’);
eg:
var Koa = require('koa');
var jwt = require('koa-jwt');
var app = new Koa();
// 自定义401错误 start 【可以不要,必须写在前面;洋葱模型,发生错误后是不会继续往下执行的】
// 如果token没有经过验证中间件会返回401错误,可以通过下面的中间件自定义处理这个错误
// Custom 401 handling if you don't want to expose koa-jwt errors to users
app.use(function(ctx, next){
return next().catch((err) => {
if (401 == err.status) {
ctx.status = 401;
ctx.body = 'Protected resource, use Authorization header to get access\n';
// ctx.body = {error: err.originalError ? err.originalError.message : err.message};
} else {
throw err;
}
});
});
// 自定义401错误 end
// Middleware below this line is only reached if JWT token is valid
// 登录注册接口不验证'/api/login','/api/register'
// 验证失败会返回401错误
app.use(jwt({ secret: 'my-secret' }).unless({ path: [/^\/api\/login/, /^\/api\/register/]}));
// 不验证token的中间件 测试1
app.use(function(ctx, next){
if (ctx.url.match(/^\/api\/login/)) {
ctx.body = 'unprotected\n';
} else {
return next();
}
});
// 验证token的中间件 测试2
app.use(function(ctx){
if (ctx.url.match(/^\/api/)) {
ctx.body = 'protected\n';
}
});
app.listen(3000);
注意鉴权代码的位置,鉴权代码应该写在注册路由之前,写在koa-static
(用了的话)等中间件之后,否则,请求静态资源路径也被鉴权。
如果不校验token,始终放行,配置passthrough: true
// 不校验token是否有效
app.use(jwt({ secret: 'my-secret', passthrough: true }));
如果secret有多个
app.use(jwt({ secret: ['old-my-secret', 'new-my-secret'] }));
Headers:{Authorization:'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.sign'}
'Bearer’前缀是jwt标准要求的,参见:https://jwt.io/introduction/
// koa-jwt中间件校验token成功才会执行下面的方法
router.get('/detail/:code', async (ctx, next) => {
console.log('user:', ctx.state.user) // {name:'txj',iat: 1644314969}
})
如果token校验失败,直接返回401至前台
app.use(jwt({ secret: 'secret_shhhhh',key: 'jwtdata' }).unless({ path: [/^\/api\/login/, /^\/api\/register/]}));
后面使用ctx.state.jwtdata取payload里的值
app.js
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const router = require('./index.js')
const jwt = require('koa-jwt');
const app = new Koa();
// 用于cookie签名
app.keys = ['SHjUcKJS9TqcPWGk'];
// logger
app.use(async (ctx, next) => {
const start = new Date;
await next();
const ms = new Date - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// 配置koa-bodyparser模块解析body
app.use(bodyParser());
// 自定义401错误
// app.use(function(ctx, next){
// return next().catch((err) => {
// if (401 == err.status) {
// ctx.status = 401;
// // ctx.body = 'Protected resource, use Authorization header to get access\n';
// ctx.body = {error: err.originalError ? err.originalError.message : err.message};
// } else {
// throw err;
// }
// });
// });
// jwttoken鉴权
// 登录注册接口不验证'/api/login','/api/register'
// 验证失败会返回401错误
app.use(jwt({ secret: 'secret_shhhhh' }).unless({ path: [/^\/api\/login/, /^\/api\/register/]}));
// 配置路由中间件
app
.use(router.routes())
.use(router.allowedMethods());;
// 全局监听异常信息
app.on('error', err => {
console.error('server error', err)
});
app.listen(8910);
console.log('listening on port 8910');
index.js
const Router = require('@koa/router');
const imageTextRoutes = require('./imageText.js')
const indexService = require('../service/index.js')
const router = new Router({
prefix: '/api'
});
// 指定一个url匹配
router.get('/indexData', async (ctx) => {
// 这里可以直接获取token里的payload
console.log('cccc:', ctx.state.user)
// let body = ctx.request.body;
const data = await indexService.getIndexData({
current: ctx.query.current,
size: ctx.query.size
})
ctx.body = {
success: true,
data: data
}
})
router.use('/article', imageTextRoutes.routes(), imageTextRoutes.allowedMethods());
module.exports = router