共计 4162 个字符,预计需要花费 11 分钟才能阅读完成。
一、jwt 是什么
JWT 全称,JSON Web Token,是一个以 JSON 为基准的标准规范。
举例:服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样
{
"姓名": "brook",
"角色": "前端攻城狮",
"帅气指数": "5 颗星"
}
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
我们先看看 jwt 的真实面目
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibmFtZSI6ImFkbWluIiwidXNlcm5hbWUiOiJhZG1pbiIsInBvc2l0aW9uIjoiIiwicGhvbmUiOm51bGwsImVtYWlsIjpudWxsLCJyb2xlIjpbImFkbWluIl0sImF2YXRhciI6Imh0dHA6Ly9pbWcuZG9uZ3FpdWRpLmNvbS91cGxvYWRzL2F2YXRhci8yMDE1LzA3LzI1L1FNMzg3bmg3QXNfdGh1bWJfMTQzNzc5MDY3MjMxOC5qcGciLCJpbnRyb2R1Y3Rpb24iOiIiLCJjcmVhdGVfdGltZSI6IjIwMTctMTEtMDJUMTg6MTU6NDguMDAwWiIsInVwZGF0ZV90aW1lIjoiMjAxNy0xMS0yNlQwNjozMzoxNy4wMDBaIiwiaWF0IjoxNTM5MjQ0NjQ1fQ.cRg7ZAQ-1ZBiJUPDx6naQupUMK2BLHmIusMQZrnqVpG
它是一个很长的字符串,中间用点(.)分隔成三个部分。三个部分依次为
Header(头部)Payload(负载)Signature(签名)
即 header.payload.sign。
Header 部分是一个 JSON 对象,描述 JWT 的元数据。
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了 7 个官方字段,供选用。
Signature 部分是对前两部分的签名,防止数据篡改。
关于这三部分的详解,可以具体参考阮一峰老师的文章:http://www.ruanyifeng.com/blo…
在使用 jwt 的时候建议放在 HTTP 请求的头信息 Authorization 字段里面,如下
Authorization: Bearer <token>
二、jwt 的好处
- 前后端分离:使用 JWT 作为接口鉴权不需要前端代码发布到后端指定目录下,可以完全跨域,前端项目可以单独部署
- 减轻服务端内存负担:比起使用 session 来保存 cookie,JWT 自身包含了所有信息,通过解密即可验证(当然啦,这个通过吧 session 存在 redis 来避免)
- 安全性:防止 CSRF 攻击
- 移动端:对于无法使用 cookie 的一些移动端,JWT 能够正常使用
- 部署:服务器不需要保存 session 数据,无状态,容易扩展
PS:为什么不写为什么使用 jwt 呢,因为它其实还是存在不少缺点的,需要根据使用业务场景确定,不是所有的场景都适合使用 jwt,甚至网上有些帖子都是在评论 jwt 比较鸡肋的。具体可以看分析
https://juejin.im/entry/59748…
三、jwt 怎么使用
直接上图,流程如下
我们以 Eggjs 项目为例,使用 koa-jwt 这个库
https://github.com/koajs/jwt
后端 (以 Eggjs 项目为例)
1、在 config.default.js 中以中间件的方式使用 koa-jwt
config.middleware = ['compress', 'errorHandler','jwt'];
// 加上配置
config.jwt = {
match: '/api',
secret: 'abiao',
unless: ['/api/user/login'],
};
- match 指 egg 路由匹配到相应前缀,则会使用当前的中间件。可以使用正则表达式去匹配,推荐 api 前缀定为 api。
- secret 指 jwt 的加密密钥。
- unless 指指定的路由不经过此中间件,一般为 login 接口
PS:egg 中使用插件有全局模式和中间件模式。全局模式应该使用 egg 的插件,中间件模式可以使用第三方 koa 的插件
2、在 middlewares 文件夹目录下增加 jwt 中间件
jwt.js
const jwt = require('koa-jwt');
module.exports = (options, app) => {
let jwtMiddlerware = jwt(
{secret: options.secret,}
);
if (options.unless) {jwtMiddlerware = jwtMiddlerware.unless({path: options.unless})
}
return jwtMiddlerware
};
egg 会自动往 middleware 的中间件里注入配置。在中间件里就可以使用 koa-jwt 对我们的接口进行保护
3、在登录接口里做好 jwt 发放,剔除密码等敏感信息
* login() {
const params = this.ctx.request.body;
const rule = {
username: 'string',
password: 'string'
};
this.ctx.validate(rule, params);
// 调用 service 进行业务处理
const res = yield this.service.user.login(params);
// 获取 jwt 的配置
let {jwt:jwtConf} = app.config;
// 使用密钥对用户数据进行加密,生成 jwt
let token = jwt.sign(res,jwtConf.secret);
res.token = token;
this.ctx.body = this.ctx.helper.success(res);
}
前端
1、登录时使用用户名和密码请求登录接口,拿到接口返回的 jwt,把 jwt 存储到 localStorage 里
LoginByUsername({commit}, userInfo) {const username = userInfo.username.trim()
return new Promise((resolve, reject) => {loginByUsername(username, userInfo.password).then(response => {
const data = response.data
// 把 jwt 存储到 localStorage 里
LocalStorage.setItem('token', data.payload.token)
resolve(data)
}).catch(error => {reject(error)
})
})
}
PS:网上关于 jwt 应该存储到哪里有一篇分析,推荐是存到 cookie 里,因为可以避免 XSS 攻击,链接如下:
https://blog.csdn.net/loveyou…
经过实践,假如在服务端把 token 写入 cookie,并设置为 httpOnly,本地前端是获取不到 cookie 里的 token 的,也就没办法在 header 里带上 token 给后端校验,不可行;所以 token 还是需要让前端自己存储,而前端把 token 存储在 cookie 是没办法设置 httpOnly 的,所以规避不了 XSS 攻击。不管是在放 localStorage 和 cookie 里都会遇到 XSS 的问题,这个只能通过对用户输入进行转码来防范;而放在 localStorage 里会比放在 cookie 里好,因为每次请求不会在 cookie 里又带上 token,减少了请求体的大小。
综上所述,我认为 token 应该让前端存储在 localStorage 里,同时做好 XSS 防范。
2、前端在后续请求的时候在 header 里带上 jwt
推荐的做法是使用请求拦截器,推荐使用 axios
import axios from 'axios'
// 创建 axios 实例
const service = axios.create({
baseURL: '/',
timeout: 5000
})
// request 拦截器
service.interceptors.request.use(config => {if (LocalStorage.getItem('token')) {config.headers['authorization'] = 'Bearer' + LocalStorage.getItem('token')
}
return config
}, error => {Promise.reject(error)
})
写完基本流程之后我们带上 jwt 请求一个接口看看效果
返回结果正常。同时,我们也验证一下没有 token 的情况下。我们手动清除了 cookie 再请求一次接口
接口会返回 unauthorizeError,这个是我们期待的返回结果。当然啦,我们也可以在后端 catch 这个错误,返回更加友好的信息,例如 401,让前端提示会话过期并自动跳转到登录页。我们简单演示这一步就先跳过了。
到这里,基于 jwt 的前后端分离实现方案就搞定啦!
四、关于 jwt 的一些思考
实际上,jwt 在使用的过程中有一个比较致命的缺点,就是一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。这对于要临时禁止某个用户的操作,修改某个用户权限并马上生效的业务场景,是满足不了的,而对于做得比较完善的业务系统来讲都会有类似的需求,所以是否使用 jwt,还需要谨慎评估。
JWT 的最佳用途是「一次性授权 Token」,这种场景下的 Token 的特性如下:
有效期短,只希望被使用一次。
例如分享一个文件给朋友,在指定 1 小时打开有效。
结语
以上是关于基于 jwt 的前后端分离实现方案的总结和思考。此外分享一个 accesstoken 的方案,可以作为 jwt 的替代方案,详情可以查看 loopback 框架的 Authorization,可以满足大部分的业务场景。
https://loopback.io/doc/en/lb…