前情提要:ASP.NET Core 使用 JWT 搭建分布式无状态身份验证系统
文章超长预警(1万字以上),不想看全部实现过程的同学可以直接跳转到末尾查看成果或者一键安装相关的 nuget 包
自上一篇介绍如何在 ASP.NET Core 中集成 JWT 的博文发布接近一年后,我又想来分享一些使用 JWT 的经验了。过去的一年中,我每次遇到一些小的,垃圾的项目,就会按照去年的那片文章来进行配置,虽然代码不多,但是每次写这么一些模板代码,又感觉很枯燥、冗余,而且稍不注意就有可能配置的有问题,导致验证不成功。前几天,我继续写自己的垃圾毕设,写到集成 JWT 的时候,我终于忍受不了这种重复的配置工作了,于是便着手封装一个简单易用的 JWT 插件。
之前集成 JWT 的方法在 ConfigureServices
方法里面添加了太多细节上的东西,所以在新的实现里面,添加服务依赖的 API 一定要足够简单,其次,之前的实现里面,签发一个 Token 步骤太多且比较复杂,所以签发 Token 的步骤也要简化。最后,之前在 Cookie 中添加 JWT 支持也比较 hack,跟 ASP.NET Core 的集成也不是很好。带着这些痛点,我在网上经历了一番搜索,最终找到了这个仓库 ,本来都想直接用他的实现了,不过他的配置看起来还是有些麻烦的,所以没办法,只好自己手写一个了。
从设计配置 API 开始
其实不管是我之前写的实现还是 GitHub 上找到的那个仓库的实现,最让我不满意的地方就是配置,很多时候,我就只想快速地搭建一个项目,根本不想去研究“怎样配置”,所以我的第一步的目标就是设计一个简单的配置接口:
EasyJwtOption
是用来进行描述 EasyJwt
配置的类型,它的每个属性都是我们可以进行配置的地方,同时为了避免把 ASP.NET Core 自带的对 JWT 跟 Cookie 的配置项目重写一遍,我就定义了 CookieOptions
跟 JwtOptions
这两个属性,用来向微软的 AuthenticationBuilder
传递配置。
GenerateKey()
GenerateCredentials()
这两个抽象方法则跟加密算法相关,在 JWT 中,我们可以使用两类算法进行加密:对称加密与非对称加密,在我之前写的文章中,我使用的是非对称加密的 RSA 算法,将原先的配置写成新的 EasyJwtOption
就是:
由于 RSA 算法的私钥与密钥只能机器生成,所以我还是延续了以前的做法,把算法参数导出成 json 保存在本地,故 EasyRSAOptions
的构造函数接受一个存储位置作为必须参数。但是这种做法普适性不太好,更好的做法是把 RSA 私钥与公钥导出成标准格式的文本,这样其他的应用也可以导入,不过我比较懒,先这么凑活吧。
在 GitHub 找到的那个项目中,作者使用的是对称加密算法,把这个算法改成我的 EasyJwtOption
就是:
在非对称加密算法中,我们需要提供一个密钥供加密、解密使用,所以 EasySymmetricOptions
的构造函数接受一个任意的字符串作为参数,又因为 SymmetricSecurityKey
对安全性的要求,密钥的长度太短会报出异常,用户的输入的密钥字符串进行了一些转换,来满足密钥长度条件。
方便的签发 Token
为了能够让网站的各个组件能够方便的随时签发 Token ,我设计了下面这个类,它的构造函数接受一个 EasyJwtOption
作为参数:
只要我们在 Starpup.ConfigureServices
方法中把这个类添加进 IoC 容器,任何依赖 EasyJwt
的对象都可以非常简便的为用户生成 Token,调用方法大致如下:
claims
是 Identity 中的概念,表示用户的信息,例如:用户名、邮箱。签发 token 需要指定用户名、用户相关的信息以及 token 过期时间。我们 EasyJwt
得到了签发 token 所需要的参数后会创建一个 ClaimsIdentity
对象,这同样也是 Identity 中的概念,用来表示用户的一些身份信息的集合,我们可以把一个 Identity 对象想象成一张通行证,上面记录着用户的身份信息。一个用户可以有多张通行证,这些通行证既可以由我们自己的应用生成,也可以由第三方授权的应用生成,不过具体的细节就涉及到了 Identity 的身份认证设计,在此就不拓展来讲了。
为应用添加 JWT 认证支持
上面说了这么多还只是停留在签发 Token 的阶段,进行身份认证从这里才真正开始。微软早就已经提供了一个添加 JWT 认证支持的拓展,不过那个还不算特别的简单易用,所以我就在微软的 API 之上设计了一个新的拓展方法来结合之前的 EasyJwt
配置 JWT 认证:
这个拓展方法接受一个 EasyJwtOption
的子类实例作为参数,并通过这个参数初始化一个 EasyJwt
对象,并将其添加进 IoC 容器中。接着就是简单的调用微软的拓展方法,为应用程序添加 JwtBearer 认证。这里的 jwtParams
是由 EasyJwt
对象导出的,具体的导出代码实现可以在我的 GitHub 上看到,并不是很重要的代码,所以就不在这里贴出来了。
为 Cookie 添加 JWT 支持
为 Cookie 添加 Jwt 支持是最让人头疼的了,而且还要让我
们的 API 跟 ASP.NET Core 自己的机制能够较完美的结合起来,这里就需要比较多的代码了。
首先我们需要自定义一个 Cookie 中存储 Jwt Token 的格式,也就是下面这个 EasyJwtAuthTicketFormat
:
这个类我借鉴了前面提到的 Github 上面的那个项目的实现,并花了一些功夫对它做了一些改动。你可以看到,这个真的是非常大的一坨代码,不过我们还是先克服困难,从构造函数来阅读吧。
EasyJwtAuthTicketFormat
的构造函数接受三个参数,第一个我们已经见过了,是 EasyJwt
导出的 TokenValidationParameters
,用来对 Jwt token 进行验证、解密。另外两个参数与 ASP.NET Core 的安全机制有关,IDataSerializer<AuthenticationTicket> ticketSerializer
用来将要存入 Cookie 中的数据序列化或者从 Cookie 中反序列化我们需要读出来的数据。IDataProtector dataProtector
则是用来对 Cookie 进行加密、解密的工具。
据 Github 上那个项目的作者说,他的代码是从微软的默认实现里面魔改出来的,所以我个人认为其中有些东西对于 Jwt 来说其实不是必须的,理由我会在下面详细解释。
首先一起来看看 Unprotect
方法,他的 protectedText
参数就是存储在 Cookie 中的字符串,首先我们需要对他用 Base64 进行解码,然后接着要用之前的 dataProtector
进行解密,最后再用 ticketSerializer
反序列化出 AuthenticationTicket
对象,这个 AuthenticationTicket
中存储的就是一些跟身份认证相关的数据,在我们这里,主要就是存储着 jwt Token。当我们把 token 中的用户数据解密并提取出来之后,再跟 Cookie 中可能含有的其他的身份信息合并起来(虽然可能并不会有什么其他信息。。。),最终就把结果返回出去。
Protect
方法就很简单了,基本上就是 Unprotect
开头一部分的逆序,先把 AuthenticationTicket
序列化,然后使用 dataProtector
加密,最终 Base64 编码成字符串返回出去。
那么很有意思的事情就出现了,jwt 本身的设计就是可以直接在 HTTP 协议中直接传递的,一般来说,并不需要我们重新对其进行 Base64 编码,而且 JWT 本身的内容就是有加密校验的,也就是说信息可读但是不可被修改,那么使用 dataProtector
对其加密的过程也应该是可以省略的。不过由于我比较懒,而且对这里不太肯定,所以就没有移除这部分的代码。
向响应头 Cookie 中添加 JWT
你可能觉得这有啥意思,不就是直接 Cookies.Add()
就好了?然而这样做是没法让认证中间件正确的提取出 Token 的,我们需要用到 HttpContext.SignInAsync
这个方法。这个方法的一个重载是接受一个 ClaimsPrincipal
跟一个 AuthentifactionProperties
作为参数,而这两个东西就是上一节提到的 AuthenticationTicket
的重要组成。
所以,我们除了要让 EasyJwt
签发 token 之外,还要它能够生成 AuthTicket,方便我们跟自带的认证中间件结合使用,相关的实现代码如下:
这个方法跟签发 Token 的方法长得一个样,接受一个 Claims
集合,然后用这些 claims 构建出一张通行证(ClaimsIdentity),然后把这个 identity 对象扔进一个 ClaimsPrincipal
里面。同时,我们还需要把 token 塞进一个 AuthentifactionProperties
对象里面。最后,把这两个创建出来的东西返回出去。
为了能够简化这部分的调用,我又写了一个拓展方法把 SignInAsync
重新包装了一下:
这样,在用户登录的时候就可以非常的简单的同时把 token 显式的返回并设置在 Cookie 中了:
看起来我们终于能够正确的签发 token 了,然而事情并没有结束,我们还没有把 Cookie 认证及其相关依赖添加到 IoC 容器中,让我们直接修改一下前面的操作注册服务的拓展方法好了:
至此,我们终于能够完整的让 Jwt 的功能运行起来了。
成果展示
那么如何在一个空白的项目中使用 EasyJwt 认证呢?
1. 注册服务
2. 添加认证中间件
// Startup.Configureapp.UseAuthentication();
3. 编写自己的登录注册控制器
4. 使用 EasyJwtAuthorize
认证过滤器保护你的 API 或者 MVC 控制器
终于,经过我们一系列的魔改,我们可以非常快速的来构建一个使用 Jwt 来进行身份认证的网站了。
本文的全部代码您都可以在我的这个项目中找到,或者,如果您想在您的项目中试试我写的这个小拓展,可以直接使用 dotnet cli 来安装:
dotnet add package ZeekoUtilsPack.AspNetCore --source https://www.myget.org/F/zeekoget/api/v3/index.json
可以改进的地方
加入吊销 token 的功能
移除
EasyJwtAuthTicketFormat
中冗余的代码
原文地址:https://www.cnblogs.com/JacZhu/p/9388964.html
.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com