目录
Session认证机制存在的问题
- Session是基于Cookie工作的,所以当浏览器禁用cookie,或者发生跨站请求时,Session就无法工作了。
- Session如果存储在服务器内存中,则会占用大量服务器内存,并且当项目是分布式部署到多个服务器时,多个服务器内存中session的互相共享与同步成为一个问题。
- Session如果存储的数据库中,则可以解决分布式部署多个服务器间session共享和同步问题,但是如果数据库挂了,则所有分布式服务器的session认证都会失败,所以数据库也要集群部署,这意味着一份session需要被复制到多个数据库中。
- Session认证机制,本质是基于用户名和密码的认证机制,服务器需要取出session中保存的用户名和密码,去和数据库中的用户名和密码进行对比,对比一致才能认证成功,而这意味着每次session认证都需要进行一次数据库查询。
问题1:sessionid可以选择保存在浏览器的webStorage中,具体操作方案是,服务器将sessionid放在自定义响应头(非Set-Cookie)中,或者放在响应体中,浏览器收到响应后,从自定义响应头或响应体中取出sessionid,保存到浏览器webStorage中。当浏览器再次请求服务器时,需要手动从浏览器webStorage中取出sessionid加入自定义请求头(非Cookie)或请求体中,服务器收到请求后,从自定义请求头或请求体中解析出sessionid。
对于问题3:目前已有成熟的session数据库集群管理方案,但是任需要服务端提供大量硬盘进行session存储
对于问题4,主要需要优化的是:为了频繁重复的验证用户名和密码而去查询数据库,但是这是Session认证机制的安全性保障,无法优化。
可以分析出:Session认证机制会消耗服务端大量硬盘资源,以及会进行频繁重复的数据库查询。
这是Session认证机制的缺点。
Token令牌
在古代影视剧中,你会看到主帅在调兵遣将时,会拿起桌上的令牌给对应的将领,比如三国演义火烧博望坡一节中,诸葛亮依次给了张飞,赵云,关羽令牌,而张飞,赵云,关羽拿到令牌后就可以调动军队了。我们默认军队只听从主帅诸葛亮的调遣,也就是说军队需要认证调用者的身份为诸葛亮才能听命。那么拿着令牌的关羽为何也能调用军队呢?
其实很好理解,令牌是一种授权,它可以授予持有者某种权限,比如令牌赋予关羽调用军队的权限。
所以军队支持两种认证机制:1、主帅认证 2、令牌认证
为什么需要令牌认证呢?假设只有主帅认证,则关羽,张飞,赵云想要调动军队,则都需要带上诸葛亮,但是诸葛亮只有一个,所以诸葛亮会很忙,军队调用效率也不高。
诸葛亮很聪明,引入了令牌机制,并且和军队灌输了“见令牌如主帅亲临”,所以军队看到了令牌就像看到了主帅,也会服从持有者调遣。
而这种古老的认证机制也被设计者们引入到了网络身份认证中。
上面例子中,存在如下要素并且可以类比为:
- 诸葛亮:服务器端注册过的用户名和密码
- 军队:服务器端接口,该接口支持主帅认证和令牌认证
- 关羽:调用接口方式
并且存在如下场景:
- 关羽带诸葛亮调动军队:带上用户名密码调用接口,接口认证用户名和密码
- 关羽带着令牌调用军队:带上令牌调用接口,接口认证令牌
Token令牌认证的特点
令牌中包含用户名,“见令牌如主帅亲临”
令牌中不包含密码,“令牌不是主帅本人”
所以服务器拿到令牌无法解析出密码,也就无法基于用户名和密码进行身份认证。
令牌认证,其实就是基于令牌本身进行认证。也就是令牌既要支持表明身份,也要支持防伪和自验证。
Json Web Token
JWT是一种成熟的token令牌生成方案,JWT生成的token令牌既能包含用户身份信息,也具有很好的防伪和自验证能力。
JWtoken 由三部分组成 header,payload,signature,三部分之间用"."连接。
header是一个经过base64URL编码的JSON对象,未编码前如下
{
"alg": "HS256", // 签名算法
"typ": "JWT" // token类型
}
header用于设置当前token的类型,一般是“JWT”,以及jwt的signature部分的加密算法。
payload也是一个经过base64URL编码的JSON对象,未编码前如下
{
"name": "John Doe", // 用户身份
"exp": 1516239022 // jwt失效时间
}
payload主要用于设置用户身份数据,以及jwt的失效时间。
signature是基于header和payload的签名数据,计算公式如下
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
其中HMACSHA256是根据header.alg选择的加密算法,secret是用户自定义的加密密钥。
signature的作用是防止jwt中header和payload被篡改,因为基于被篡改的header和payload,使用相同的加密算法和加密密钥是无法生成相同的jwt中signartue部分的。signarture既是jwt防伪的保障,也是jwt进行自验证的关键。
另外需要注意的是base64URL编码并不是一种加密技术,它很容易被解码,所以在payload中我们最好不要添加用户隐私数据,比如密码。
node第三方模块jsonwebtoken的使用
jsonwebtoken是基于nodejs平台开发一个模块,基于该模块可以生成jwt,以及验证解析jwt。
jsonwebtoken模块对外暴露一个对象,该对象下主要有两个异步方法:sign和verify
sign方法
sign方法用于生成一个jwt字符串,该方法接收四个参数,依次如下:
- payload:接收一个对象,用于传入用户身份信息
- secret:接收一个字符串,作为jwt.signature的加密密钥
- options:配置对象,介绍如下
- callback(err, token):生成jwt结束时执行的回调函数,遵循nodejs的错误优先原则,第一个参数是生成jwt过程抛出的异常信息,第二个参数是成功生成的jwt字符串值。
options配置对象具有如下配置属性
algorithm | 签名加密算法,默认为HS256 |
expiresIn | jwt失效时间,默认单位为ms |
notBefore | jwt生效时间,默认单位为ms |
audience | 受众 |
issuer | 签发人 |
jwtid | 编号 |
subject | 主题 |
noTimestamp | |
header | |
keyid | |
mutatePayload |
比较重要的是expiresIn,用于设定jwt的失效时间。
sign方法的返回值就是sign方法生成的jwt字符串。
但是需要注意的是,一旦指定了sign方法的第四个参数回调函数,则sign方法变为异步方法,jwt字符串只能通过回调函数获取。
如果不指定sign方法的第四个参数回调函数,则sign方法为同步方法,jwt字符串可以通过sign方法返回值获取。
所以sign方法的第四个入参callback本质不是用来获取jwt字符串,而是指定jwt字符串的后续处理逻辑,为了防止处理逻辑阻塞主流程,所以才将callback变为异步执行。
verify方法
verify方法用于校验和解析jwt字符串,该方法入参四个参数:
- token:jwt字符串
- secret:加密密钥
- options:配置对象
- callback(err, decode):回调函数,遵循nodejs错误优先原则,第一个参数是异常信息,第二个参数是根据jwt解码出来的数据
options配置对象具有如下配置属性
algorithms | 签名加密算法,默认为HS256 |
audience | 受众 |
complete | 默认为false,表示只返回payload解密数据,若为true表示返回{ payload, header, signature }解密数据 |
issuer | 签发人 |
ignoreExpiration | if true do not validate the expiration of the token. |
ignoreNotBefore | if true do not validate the not before of the token. |
subject | 主题 |
clockTolerance | number of seconds to tolerate when checking the nbf and exp claims, to deal with small clock differences among different servers |
maxAge | the maximum allowed age for tokens to still be valid. |
clockTimestamp | the time in seconds that should be used as the current time for all necessary comparisons. |
nonce | if you want to check nonce claim, provide a string value here. It is used on Open ID for the ID Tokens. |
需要注意其中complete属性,该属性为true,则根据jwt字符串解码出来完整数据,即包含header,payload,signature,否则只包含payload
verify方法返回值也是jwt字符串解码出来的数据。
另外一旦verify传入callback,verify方法不再同步返回解码数据。
但是verify入参callback并不是异步执行的,而是同步执行的。
verify校验异常情况
NotBeforeError
该异常发生在jwt尚未生效时使用
TokenExpiredError
该异常发生在jwt字符串校验成功,但是已失效
JsonWebTokenError
该异常发生在jwt字符串校验失败
基于jsonwebtoken模块完成Token身份认证
实现步骤
- 登录接口验证用户名密码成功,服务器基于jsonwebtoken生成jwt字符串,作为cookie响应头返回
- 浏览器自动保存cookie:jwt,当再次请求服务器时自动携带,服务器解析出cookie:jwt进行校验解析
实现代码
nodejs平台基于jsonwebtoken的登录认证-Node.js文档类资源-优快云文库
登录成功,则生成jwt,并在jwt中加入用户身份信息name,并作为cookie响应给浏览器
浏览器携带cookie:jwt请求服务器,则服务器对jwt进行校验解析,若校验成功无异常,则说明jwt有效正确,然后从jwt解码出用户身份信息name。
上面jwt token是放在cookie中,这样在浏览器禁用cookie或跨站请求时,jwt token就会失效,所以我们可以将jwt token存储到浏览器端webStorage中。
Token身份认证的优缺点
优点
- 服务器无需保存用户登录状态,节省了服务器内存和硬盘资源,以及节省了服务端对于登录状态的管理
- 服务器端无需基于用户名密码验证,省去了频繁重复的查询数据库的动作
缺点
服务器一旦签发token,则在token有效期时间内,token始终有效,服务器端无法强制让token失效。
这将导致一个问题:即使用户已经退出登录,理论上应该让token失效,但是实际上,服务器端无法让还在有效期内的token失效,token的失效只能等它有效期过了。
原因是:token本质是一段支持自验证的字符串,而自验证的规则已经固化了,服务器端只能按照固化的验证规则对token进行验证,所以一个未失效的token总是可以通过固定规则的验证。
前面代码案例中,用户退出登录的实现都是清除了浏览器端保存的token信息,本质上并不是让token失效,如果用户将token刻意复制下来,然后再导入浏览器中,依旧可以实现免登录。
Token强制失效的方案
1、在服务器端建立黑名单,保存强制失效的token,这样服务器就可以通过查询黑名单来禁止强制失效token正常工作了。
该方案违背了token的初衷,token初衷是实现服务器端无状态保存,现在虽然不保存登录状态,但是却要保存黑名单token,依旧会给集群服务器带来管理压力。
2、减少token有效期的长度,降为20分钟到30分钟,这样可以减少token强制失效后的存活时间,降低影响。
该方案可以就是五十步笑百步的典型,依旧存在很大安全风险。
目前来看,没有太好的让Token强制失效的方案。
Token续签问题
前面让token强制失效,考虑的是token有效时长未用完,用户就退出登录,造成有效token可能被黑客盗用的风险。
但是还有一个实际生活中常见的问题:token有效时长用完了,用户还在继续正常访问网站,这时候会造成一个尴尬的问题,用户并没有退出登录,但是却要重新登录。
所以token续签成为一个需要思考的问题。
Token续签实现方案
服务器给用户发放两个token,分别是access_token和refresh_token。
access_token就是验证用户身份信息的token,它的有效期设置为短时间的,比如20分钟。
refresh_token用于更新access_token,即当access_token失效后,服务器可以验证refresh_token来后生成一个新的access_token。所以refresh_token的有效期一般设置为长时间跨度的,比如半个月。
需要注意的是refresh_token的secret和access_token的secret不能是同一个,否则refresh_token就可以被用来登录了。
基于acess_token和refresh_token实现token续签-Node.js文档类资源-优快云文库