目录
一、为什么不用Session用JWT
Session在用户登陆验证成功后,服务器端生成唯一标识SessionId,服务器端不仅会把SessionId返回给浏览器端,还会把SessionId和登陆用户的信息的对应关系保存到服务器的内存中;当浏览器端再次向服务器端发送请求的时候,浏览器端就在HTTP请求中携带SessionId,服务器端就可以根据SessionId从服务器的内存中取到用户的信息,这样就实现了用户登陆的功能。
我们一般把SessionId保存在Cookie中,而Session的数据默认是保存在服务器内存中,对于分布式集群环境,Session数据保存在服务器内存中就不合适了,应该保存到一个供所有集群实例访问的共用的状态服务器上。
Session缺点
1、如果Session 数据保存到内存中,当登录用户量很大的时候,Session 数据就会占用非常多的内存,而且无法支持分布式集群环境。
2、如果Session数据保存到 Redis 等状态服务器中,它可以支持分布式集群环境,但是每遇到一次客户端请求都要向状态服务器获取一次 Session 数据,这会导致请求的响应速度变慢。特别是对于一些跨多数据中心的分布式环境,跨数据中心的状态传递更是一件棘手的事情。
在现在的项目开发中,我们倾向于采用JWT代替 Session 实现登录。JWT 是使用JSON格式来保存令牌信息的。JWT 机制不是把用户的登录信息保存在服务器端,而是把登录信息(也叫作令牌)保存在客户端。为了防止客户端的数据造假,保存在客户端的令牌经过了签名处理,而签名的密钥只有服务器端才知道,每次服务器端收到客户端提交的令牌的时候都要检查一下签名,如果发现数据被篡改,则拒绝接收客户端提交的令牌。
二、JWT结构
头部:保存加密算法的说明
负载:保存的是用户的ID、用户名、角色等信息
签名:根据头部和负载一起算出来的值
三、JWT实现登陆的流程
1、客户端向服务器端发送用户名、密码等请求登录。
2、服务器端校验用户名、密码,如果校验成功,则从数据库中取出这个用户的ID、角色等用户相关信息。
3、服务器端采用只有服务器才知道的密钥来对用户信息的JSON字符串进行签名,形成签名数据。
4、服务器端把用户信息的JSON字符串和签名拼接到一起形成JWT,然后发送给客户端。
5、客户端保存服务器端返回的JWT,并且在客户端每次向服务器端发送请求的时候都带上这个JWT。
6、每次服务器端收到浏览器请求中携带的JWT后,服务器端用密钥对JWT的签名进行校验,如果校验成功,服务器端则从JWT中的JSON字符串中读取用户的信息。这样服务器端就知道这个请求对应的用户了,也就实现了登陆的功能。
四、ASP.NET Core对于JWT的封装
第一步:配置系统中配置一个名字叫JWT的节点,在节点下创建SigningKey、ExpireSeconds两个配置项,分别代表JWT的密钥和过期时间。我们再创建一个对应JWT节点的配置类JWTOptions,类中包含SigningKey、ExpireSeconds两个属性。
第二步:NuGet引用程序集Microsoft.AspNetCore.Authentication.JwtBearer包
第三部:编写配置,放到Program.cs的builder.Build之前
第四步:在Program.cs的app.UseAuthorization之前添加app.UseAuthentication
第五步:在TextController类中增加登陆并且创建JWT的操作方法Login
第六步:在需要登陆才能访问的控制器类上添加[Authorize]这个ASP.NET Core内置的Attribute
五、[Authorize]的注意事项
[Authorizel这个Attribute既可以被添加到控制器类上,也可以被添加到操作方法上。我们可以在控制器类上标注[Authorize],那么这个控制器类中的所有操作方法都会被进行身份验证和授权验证:对于标注了[Authorize]的控制器类,如果其中某个操作方法不想被验证,我们可以在这个操作方法上添加[AllowAnonymous]。如果没有在控制器类上标注[Authorize],那么这个控制器类中的所有操作方法都允许被自由地访问:对于没有标注[Authorize]的控制器类,如果其中某个操作方法需要被验证,我们也可以在操作方法上添加[Authorize]。
六、解决JWT无法提前撤回的难题
JWT 把用户信息保存到客户端,而不像 Session 那样在服务器端保存状态,因此更加适合分布式系统及前后端分离项目,但是任何技术都不是完美的,JWT 的缺点是:一旦JWT 被发放给客户端,在有效期内这个令牌就一直有效,令牌是无法被提前撤回的。哪些场景会需要在JWT过期之间提前撤回令牌呢?比如,用户被删除了,那么针对这个用户的令牌就要被撤回,否则会出现客户端使用已经被删除的用户身份的问题:再如,某个JWT 被恶意攻击者拿到并用来发送恶意请求,我们也要撤回针对这个用户的令牌,以便阻断攻击者: 再如,用户在 A设备上登录了,稍后又在 B 设备上登录了,我们就需要把用户在 A 设备上登录获得的JWT 撤回,否则就会出现用户同时在多个设备上登录的问题。上面提到的这些需求其实用传统的 Session 实现更合适
思路:在用户表中增加一个整数类型的列 JWTVersion,它代表最后次发放出去的今牌的版本号:每次登录、发放令牌的时候,我们都让JWTVersion 的值自增同时将JWTVersion 的值也放到JWT 的负载中:当执行用用户、撤回用户的令牌等操作的时候,我们让这个用户对应的JWTVersion的值自增:当服务器端收到客户端提交的JWT 后,把JWT 中的JWTVersion 值和数据中的JWTVersion 值做比较,如果JWT中JWTVersion的值小于数据库中JWTVersion 的值,就说明这个JWT 过期了,这样我们就实现了JWT 的撤回机制。由于我们在用户表中保存了JWTVersion 值,因此这种方案本质上仍然是在服务器端保存状态,这是绕不过去的,只不过这种方案是一种缺点比较少的妥协方案。