0%

midway jwt token 探索

以前做登录授权的时候,流程是:

  1. 用户在登录页面输入用户名和密码
  2. 服务器验证提交过来的信息,通过后,在session中保存相关数据,比如用户ID
  3. 服务器返回一个session_id,写入用户的Cookie
  4. 用户的每一次请求,都会通过Cookiesession_id传递给服务器。
  5. 服务器收到session_id,找到前面保存的数据,得知用户的身份。

这次正好也用midway.js来做到登录授权了,换种方式,采用JWT(JSON Web Token)。

使用egg-jwt插件,插件地址:okoale/egg

JWT原理

jwt的原理是,在服务器认证通过后,将数据保存在一个JSON对象中,然后生成一个字符串TOKEN,发回给用户。用户在请求的时候,头部带着这个TOKEN,服务器就可以解析出前面保存的JSON对象,然后识别用户身份。

JWT的数据结构由三部分组成:

  • Header 头部
  • Payload 负载
  • Signature 签名

Header部分是用来描述JWT的元数据,Paylod 部分是用来存放实际需要传递的数据,我们可以在这个里面定义私有字段。Signature是对前面两部分的签名,防止数据篡改。

服务器中会设定一个密钥,然后使用Header里面指定的签名算法(默认是HMAC SHA256),按照下面的公式产生签名:

1
2
3
4
5
HMACSHA256(
base64UrlEncode(header) + '.'
base64UrlEncode(payload),
secret
);

算出签名后,把Header、Payload、Signature三个部分使用.拼成一个字符串,返回给用户。

客户端在收到JWT后,可以存储在Cookie里面,也可以存储在localStorage里面,以后每次与服务器通信时,都需携带JWT,为了解决跨域问题,更好的做法是将JWT放在HTTP请求头信息的Authorization字段里面:

1
Authorization: Bearer 你的JWT

也就是说,这个Header的名字叫做Authorization,值为Bearer加一个空格再加上你的JWT。

配置插件

文件src/config/plugin.ts

1
2
3
4
5
6
7
8
9
10
import {EggPlugin} from 'egg';

const plugin: EggPlugin = {
jwt: {
enable: true,
package: 'egg-jwt',
}
};

export default plugin;

然后修改配置文件src/config/config.default.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14

export = (appInfo: any) => {
const config: PowerPartial<EggAppConfig> = {};
// use for cookie sign key, should change to your own and keep security
config.keys = 'api_tcs';

config.jwt = {
enable: true,
secret: 'test123456',
ignore: ['/sign'],
}
return config;
};

使用

该插件提供了app.jwt对象,而@plugin属性装饰器,则是类似于直接从app对象上拿属性。所以要使用app['jwt'],可以使用@plugin

1
2
3
4
5
export class testController {
@plugin()
jwt;

}

生成token

1
2
3
4
@get('/')
async index(ctx) {
ctx.body = { token: this.jwt.sign({name: 'tony'})}
}

解token获得数据

插件会自动解出数据,并放在ctx.state.user上,可以直接访问。

携带token访问

需要在header里面加上参数:

Authorization,然后值为:Bearea You-token,使用curl示例:

1
2
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0OTAwMTU0MTN9.ehQ38YsRlM8hDpUMKYq1rHt-YjBPSU11dFm0NOroPEg" 127.0.0.1:7001/test

很好,这时候,没有特别设置的路由进来的请求的话会抛出401的异常,当携带了token并有效的时候才会返回正常的接口结果。

但是,万一没有jwt或者jwt过期了,不能直接抛出异常啊,我们需要手动处理异常,然后以json的形式返回。

错误处理

我们需要添加一个中间件来处理这个错误,error_handle.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = () => {
return async function errorHandle(ctx, next) {
console.log('error handle');
try {
await next();
} catch (err) {
if (err.name === 'UnauthorizedError') {
ctx.body = {
code: 401,
mssage: '请登录后再进行操作',
}
}
}
}
}

尝试把中间件加入到config.default.ts中:

1
2
3
config.middleware = [
'errorHandle'
];

但是感觉没起作用,没有进入中间件就抛出了异常。

搜了好久,才看到eggjs的官网文档上有解释:

应用层定义的中间件(app.config.appMiddleware)和框架默认中间件(app.config.coreMiddleware)都会被加载器加载,并挂载到 app.middleware 上。

所以,需要把这个错误拦截的处理中间件挂载在光甲默认中间件上, 修改app.ts

1
2
3
module.exports = (app) => {
app.config.coreMiddleware.unshift('errorHandle');
};

再次请求尝试,发现UnauthorizedError被拦截了,当没有token或token过期的时候就会返回我们的errcode。

再谈插件的ignore

我在打通公众号的时候,将接口的域名配置到了公众号那边,但是公众号那边发起的验证请求是不带token的,这要求我在配置的时候忽略不带路径的请求,我尝试了下面两种方式:

1
2
3
4
5
6
ignore: [
'/',
];
igonre: [
'',
]

尝试的结果是,这两种方式都是错的,这导致我的jwt插件会忽略所有的请求。

我以为做这个操作的是插件,我查看了egg-jwt插件的源码,以及内部使用的koa-jwt2甚至连node-jsonwebtoken都看了,没有我想要的配置,后续一查,原来配置这个中间件是否起作用是中间件的配置。有matchigonre两个, 他们的参数都一样:

  • 字符串:配置是一个url的路径前缀。也可以是字符串数组
  • 正则:直接匹配满足正则验证的url的路径。
  • 函数:将请求上下文传递给这个函数,最终由函数的返回结果truefalse来判断是否匹配。

我们可以使用函数和数组结合:

1
2
3
4
ignore: [
(ctx: IApplicationLocals) => ctx.path === '/',
'/user/login',
],

OK,满足需求。

参考文章:JSON Web Token 入门教程

码字辛苦,打赏个咖啡☕️可好?💘