前言
现在很多Web项目都是前后端分离的形式,现在浏览器的功能也是越来越强大,基本上大部分主流的浏览器都有调试模式,也有很多抓包工具,可以很轻松的看到前端请求的URL和发送的数据信息。如果不增加安全验证的话,这种形式的前后端交互时候是很不安全的。
相信很多开发小程序的开发者也不一定都是大神,能够精通前后端,作为小程序的初学者不少人也是根据官方的文档去学习开发的。我自己最开始接触小程序也是从wafer2开始的,那时候腾讯云提供的SDK包含PHP和Node.js,因为对于一直做前端的人来说,Node.js的学习成本比较低,只要会JS基本能看懂,也是从那时候才开始接触Node.js,所以本文主要是基于wafer2的服务端基于Koa2的后端来说(其实这个不重要,Node.js基本都差不多)。
什么是JWT?
根据维基百科的定义,JSON WEB Token,是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。
为什么使用JWT?
首先,这不是一个必选方案。有时候我们的API是其它服务端和小程序公用的,那么就涉及到安全验证的问题了。
微信官方不鼓励小程序一打开就要求必须登陆的方式去获取用户信息,因此我们也不能去校验这个用户是否有权限访问这个接口,但是有的接口又不能让任何人随便去看或者被随意采集。
基于token(令牌)的用户认证
用户输入其登录信息
服务器验证信息是否正确,并返回已签名的token
token储在客户端,例如存在local storage或cookie中
之后的HTTP请求都将token添加到请求头里
服务器解码JWT,并且如果令牌有效,则接受请求
一旦用户注销,令牌将在客户端被销毁,不需要与服务器进行交互一个关键是,令牌是无状态的。后端服务器不需要保存令牌或当前session的记录。
关于JWT的详细介绍网上有很多,这里也就不说了,下面介绍在Koa2框架里的添加方法。
安装依赖
npm install jsonwebtoken
npm install koa-jwt
app.js
引用
const jwtKoa = require('koa-jwt');
设置不需要JWT验证的目录或者文件
const secret = '设置密钥';
app.use(jwtKoa({secret}).unless({
path: ['/','\/favicon.ico',/^\demo/]
}))
数组中的路径不需要通过jwt验证。
授权
小程序 wx.request 发送网络请求的 referer header 不可设置。
其格式固定为 https://servicewechat.com/{appid}/{version}/page-frame.html,其中 {appid} 为小程序的 appid,{version} 为小程序的版本号,版本号为 0 表示为开发版、体验版以及审核版本,版本号为 devtools 表示为开发者工具,其余为正式版本。
那么我们就可以根据 ctx.header 里的 referer 进行初步的限制,比如指定的 appid 才能生成令牌。
我们在生成令牌的时候可以把简单的信息加入进去,如:
const userToken = {
referer: refererArray[2],
appid: refererArray[3],
version: refererArray[4],
data: '此处可传入用户的信息'
}
生成令牌:
const jwt = require('jsonwebtoken');
const secret = '设置密钥';
jwt.sign(userToken, secret, {expiresIn: '2h'});
expiresIn:为令牌的有效期
这样简单的JWT令牌就生成好了,再通过接口返回给小程序端。
小程序前端如何使用JWT?
很简单,在header里加入下面属性即可。
authorization: 'Bearer 获取到的令牌'
JWT优点
- 可扩展性好
应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而JWT不需要。
- 无状态
JWT不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外JWT的载荷中可以存储一些常用信息,用于交换信息,有效地使用JWT,可以降低服务器查询数据库的次数。
JWT缺点
- 安全性
由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。
- 性能
JWT太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面,http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用JWT的http请求比使用session的开销大得多。
- 一次性
无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT。
(1)无法废弃
通过上面JWT的验证机制可以看出来,一旦签发一个 JWT,在到期之前就会始终有效,无法中途废弃。例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但是由于旧的JWT还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的JWT,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。
(2)续签
如果你使用jwt做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变JWT的有效时间,就要签发新的JWT。最简单的一种方式是每次请求刷新JWT,即每个http请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。另一种方法是在redis中单独为每个JWT设置过期时间,每次访问时刷新JWT的过期时间。