Spiga

ASP.NET Core 7 源码阅读5:鉴权授权

2023-07-10 15:35:12

从Http无状态协议---用户持久化的需求---最初是Cookie+Session---Token但是不管是哪种方式,有一段内容是不变的:

  1. 请求服务端,获取凭证
  2. 客户端再次请求,带上凭证
  3. 服务端识别凭证,判断是否允许访问

鉴权授权的本质是做啥的?

其实就是第3步解决用户识别和判断授权的问题被拆分成了2个动作:

  1. 鉴权Authentication:凭证识别解析,有没有登陆,凭证有没有过期,是张三还是李四
  2. 授权Authorization:基于解析来权限检测,判断下张三/李四有没有权限访问这个资源

一、实操

1. 没有任何权限要求实操

  1. 添加相关控制器, Cookie式登陆方法
  2. UseAuthentication + AddAuthentication + Cookie
  3. UseAuthorization + AddAuthorization(可不写)
  4. [Authorize]
  5. [AllowAnonymous]

校验流程:

  1. 未登陆,直接访问,跳转登录页
  2. AllowAnonymous可直接访问
  3. 登陆后跳转,正常访问
  4. 退出后访问,跳转登陆页

2. 授权

  1. Roles授权: [Authorize(Roles = "Admin")]
  2. Policy授权: [Authorize(Policy = "AdminPolicy")]
  3. 复杂Policy授权: [Authorize(Policy = "MutiPolicy")]

授权的设计,也是可以满足开发者各种这样的需求—目前还没演示完

如果是没有登陆,是跳转LoginPath---401

如果是有凭证,但没有权限,是跳转AccessDeniedPath--403

3. 发送了什么?

请求到达Action之前,即使没有标记,也会自动鉴权

  1. 主动鉴权:理解鉴权信息保存
  2. 主动授权:理解授权检测过程

4. 理解总结

鉴权授权是ASP.NET Core框架封装的2个中间件,目的在于请求进入具体的Filter-M-V-C之前,通过中间件完成用户权限检测包含2个步骤:

  1. 鉴权Authentication:鉴别有没有登陆,解析是张三还是李四(且将信息传递下去)---UseAuthentication配置Http管道,保证请求来了,都要做一次凭证的解析AddAuthentication配置IOC,告诉如何鉴权(凭证在那儿,啥格式的)
  2. 授权Authorization:判断下张三/李四有没有权限访问这个资源---UseAuthorization告诉框架要做授权检测, AddAuthorization告诉框架如何授权检测---[Authorize]标记显示声明该方法需要授权+需要什么样的授权

这里是基本过程,全是配置下就生效了,封装的挺厉害的。。然而框架是如何实现的? 各种封装又是怎么回事儿? 开始深入!

二、理解鉴权的设计

细化鉴权---深入鉴权---鉴权就是检测凭证,鉴别是张三,是如何解析?

  1. 凭证的位置:凭证是怎么传递的, Cookie/JWT就不一样
  2. 凭证的格式:凭证是什么格式的,加密/序列化
  3. 信息有效性:过期时间啥的, token
  4. 鉴权信息传递:解析得到的信息保存起来,后面使用--context.User
  5. 特殊情况处理:没登陆/没权限怎么跳转?

以上都是鉴权该完成的动作

1. 自定义鉴权

Cookie鉴权 JWT鉴权,抑或其他鉴权,本质应该都是一样的

这里先来个自定义的鉴权方式,一是帮助理解鉴权,二是方便后续调试学源码

  1. 实现个UrlTokenAuthenticationHandler---主要是实现IAuthenticationHandler
  2. 注册鉴权---AddScheme---告诉框架如何解析
  3. 标记+验证---带Token就ok 不带就跳转---自定义Scheme已完成!

2. 鉴权Authentication上帝视角

鉴权授权是紧密关联的,通过context.User通信的(授权期间其实也会调用鉴权)

先执行AddAuthentication:

  1. 设置默认Scheme+4个核心对象的IOC注册
  2. 支持配置AuthenticationOptions,里面支持了AddScheme,AuthenticationSchemeBuilder去组装Scheme(未Build)
  3. AddCookie完成校验规则配置---暂时不看Cookie细节, UrlToken是一样的

后执行UseAuthentication:

  1. 注册AuthenticationMiddleware中间件, Build-AuthenticationScheme
  2. 请求来了,执行Invoke方法,然后里面又是一套Provider--Handler--Service多对象绕一下,完成鉴权
  3. 只做基于默认Scheme解析,信息经过一波转换,保存到context.User

3. AddAuthentication

  1. 核心是AddAuthenticationCore,里面完成了4核心对象的注册---默认名字就是指定下DefaultScheme
  2. Options支持了AddScheme,其实是new一个AuthenticationSchemeBuilder保存起来(里面就是存了个那个handler的Type),且用Name作为key,存入字典----(没有Build,注册中间件的时候注入Provider,在构造函数里面才Build成AuthenticationScheme)
  3. AddCookie---AddJWT---AddUrlToken,都是一样的

最终是做了几个IOC注册,支持了不同的 字符串对应一个AuthenticationScheme (其实这里HandlerType就是将来的处理方式)

4. UseAuthentication

  1. Use中间件AuthenticationMiddleware时,构造函数注入AuthenticationSchemeProvider,其构造函数中先完成Options的初始化(AddSchme),然后把AuthenticationOptions的全部SchemeBuilder.Build(里面就namedisplayname-HandleType),得到全部的AuthenticationScheme,字典缓存起来---- (至此,启动环节完成—Add时把SchemeName跟Handler类型保存到字典,给到中间件)
  2. 请求来了---有个基于AuthenticationHandlerProvider的扩展点,直接自行处理掉请求(非常规,不管了) -----用AuthenticationSchemeProvider获取默认的AuthenticationScheme,然后准备鉴权
  3. context.AuthenticateAsync来鉴权,里面会调用AuthenticationService的AuthenticateAsync()方法----这里调用注入的AuthenticationHandlerProvider,找的是AuthenticationSchemeProvider---里面查找的AuthenticationScheme---然后是里面的HandlerType(之前存的),且完成实例化+初始化----然后调用handler的AuthenticateAsync()方法做鉴权---然后执行一次数据转换---转换后保存到context.User

5. Authentication核心对象

  1. AuthenticationSchemeProvider:包含了全部的Scheme,字典保存
  2. AuthenticationScheme: 2个名字+具体handler的类型
  3. AuthenticationService:鉴权服务, context.鉴权其实就是调用它的
  4. AuthenticationHandlerProvider:鉴权handler的提供程序,它会通过Scheme的Provider来获取Scheme,且完成Scheme的handler的实例化,且初始化,然后缓存
  5. NoopClaimsTransformation可以完成数据的转换

绕来绕去的价值,就是为了单一职责,为了扩展

6. 总结鉴权流程

  1. AddAuthentication注册最主要的三个对象注册
  2. Options把Scheme和Handler注册进去
  3. UseAuthentication注册认证中间件,完成AuthenticationScheme的初始化
  4. 请求来了,就是用默认scheme的名字去绕一圈,完成鉴权,保存到context.User

7. 3个鉴权扩展

  1. 自定义实现IAuthenticationHandler,也就是UrlTokenAuthenticationHandler完成鉴权来源、方式等全部扩展
  2. 简单扩展下IClaimsTransformation---CustomClaimsTransformation,然后IOC注册,可以将信息做一次转换
  3. 多Scheme问题---
    • 程序是可以支持多个Scheme并存
    • 鉴权中间件是默认只走默认Scheme,其他Scheme信息怎么获取?其实靠授权标记的
    • 多Scheme,那授权怎么算?其实还很复杂!以后说!

三、Scheme

1. 多Scheme演练

一个站点支持多个渠道的鉴权,比如APP、 H5、微信端、 PC----Cookie+JWT+Ids4+UrlToken多Scheme鉴权配置和测试流程, 8次访问完成验证过程:

  1. 访问地址 http://localhost:5726/Auth/UrlCookieByDefault
  2. 访问地址 http://localhost:5726/Auth/UrlCookieByDefault?UrlToken=admin-123456
  3. 访问地址http://localhost:5726/Auth/UrlCookieByUrlToken?UrlToken=admin-123456
  4. 访问地址http://localhost:5726/Auth/UrlCookieByCookie
  5. 访问地址http://localhost:5726/Auth/Login?name=admin&password=123456
  6. 访问地址http://localhost:5726/Auth/UrlCookieByCookie
  7. 访问地址http://localhost:5726/Auth/UrlCookieByCookie?UrlToken=admin-123456
  8. 访问地址: http://localhost:5726/Auth/UrlCookieByDouble?UrlToken=admin-123456

2. 多Scheme鉴权结论

  1. 程序支持多套Scheme解析,是可以共存的
  2. 不标记Scheme等同于是标记默认Scheme
  3. 无论是否标记,或标记啥Scheme,默认鉴权都会走一遍,在鉴权中间件(它不管Filter特性)
  4. 可以通过标记来指定Scheme去鉴权
  5. 声明多个Scheme信息都保存context.User---标记的Scheme,解析后的信息是存入到context.User---默认、其他的、多个都一样,多个会合并--
  6. 如果标记的是其他Scheme---然后默认Scheme也传值了也解析了,但不会保存到context.User

标记相同的Scheme,会distinct—授权源码里面

四、理解授权的设计

1. 授权3属性

鉴权Authentication是解析出当下是张三,信息保存在context.User然后该授权Authorization了,判断下张三有没有权限访问这个资源(其实因为其他scheme的原因,授权环节会根据Scheme触发鉴权)

  1. 标记[Authorize] 走的是默认Policy,只要有合法凭证就行(有源码)
  2. Roles:要求用户角色信息满足(具体校验有源码)
  3. Policy:支持自定义各种规则(多API show)
  4. AuthenticationSchemes:表示用户信息的来源(已展示)

2. PolicyBuilder多API

  1. RequireRole
  2. RequireUserName
  3. RequireClaim
  4. RequireAssertion
  5. AddRequirements
  6. Requirements.Add
  7. Combine

上述一共7个API,一次性验证AddAuthenticationSchemes---其实就是指定Scheme这么多API,就是为了满足各种授权要求---但是最终是怎么设计

3. Authorization源码-上帝视角

AddAuthorization:

  1. 一系列IOC的注册,分在AddAuthorizationCore()和AddAuthorizationPolicyEvaluator()-评估,执行授权
  2. 支持Options里面组装各种Policy规则, Name-PolicyBuilder的缓存组合
  3. Role是Policy的特例, Policy的各种Require,都是转换成了Requirement,将来授权就是遍历Requirement做检验

UseAuthorization:

  1. 先注册中间件(实例化middleware时通过注入初始化了Policy)—然后请求来了
  2. 获取全部授权特性Authorize,拼装全部Requirement(Roles、 Policy)和Scheme,放入到AuthorizationPolicy
  3. 将全部的Scheme都鉴权一遍,信息保存到context.User
  4. 校验全部的Requirement(Roles、 Policy)
  5. 成功则继续 未登录就Challenge ,未授权就Forbid

4. AddAuthorization源码

  1. 各种IOC注册,主要是AddAuthorizationCore()和AddAuthorizationPolicyEvaluator(),里面就是一系列的IOC注册和一个services.Configure(configure)
  2. 启动过程中,某类(也许我们写的测试)初始化依赖于IOptions,然后就执行了Configure委托---(之前AddAuthorization只是做了Configure未执行)

跟AddScheme一个套路---所以先理解为是UseAuthorization()使用中间件AuthorizationMiddleware,会注入IAuthorizationPolicyProvider,其依赖于IOptions就是会完成Configure委托---(之前AddAuthorization只是做了Configure

5. 授权策略的设计与实现

AuthorizationPolicy授权策略是如何设计的?各种Roles、 Policy里面N多方法,都是要放入到AuthorizationPolicy,最终形态是怎样的? AuthorizationPolicyBuilder是如何Build的?

  1. AddPolicy就是实例化AuthorizationPolicyBuilder(这里都没有Scheme的,后面授权检测时会有)
  2. 然后ConfigureBuilder,各种API了--实际特别简单,都转成AuthorizationRequirement存起来
  3. Build()就是把各种Requirement和Scheme传递过去,变成AuthorizationPolicy,而且保存到map缓存
  4. 校验时,就是遍历Requirement的HandleRequirementAsync

支持AuthorizationOptions配置,然后里面是AddPolicy---写入AuthorizationPolicyBuilder----保存在PolicyMap---当下并未初始化------等注入这个Options时,各种规则转成一个个AuthorizationPolicy,等着校验时使用

6. 关于Requirement

Policy最终其实都是转成了Requirement---执行校验(猜),就是看RequirementRoles

AuthorizationRequirement源码 AssertionRequirement源码

Requirement 就是实现了IAuthorizationHandler和IAuthorizationRequirement(空接口),核心内容就是构造函数初始化+验证处理HandleAsync----成功就context.Succeed(this); 失败了不管------调用的地方,会检测全部Requirement是否设置成功

通过AuthorizationHandler会找出全部满足这个Trequirement然后遍历执行---其实支持多个(DoubleEmailRequirement)----这种就可以遍历执行2个Requirement---支持或规则----成功设置succeed,失败不管---

7. Requirement扩展

  1. SingleEmailRequirement是实现接口+继承父类,然后直接Add
  2. CountryRequirement、 DateOfBirthRequirement-----拆开的需要做IOC注册才能校验---如果不注册,不报错,但不能校验通过
  3. 或条件: DoubleEmailRequirement,必须继承AuthorizationHandler,拆开接口----然后分别IOC注册----然后就是遍历多实现,支持或关系

8. 总结下AddAuthorization

  1. AddAuthorization各种IOC的注册
  2. 把各种规则Policy组装好,都是变成requirement,都放入Map缓存了下,里面是PolicyName+各种AuthorizationPolicy
  3. 关于Policy的设计:一个builder,支持7种方式去Add,最终都requirement,就是规则
  4. Requirement的3层验证: Role/Policy/动态校验

9. UseAuthorization上帝视角

AddAuthorization已经将Role、 Policy、 Requirement等规则,都转成了Requirement集合,然后把Scheme一起存起来,放入缓存Map了而UseAuthorization,自然是注册中间件+请求来了开始处理!

  1. 先注册中间件(实例化middleware时通过注入初始化了Policy,应该是这样设计)
  2. 然后是请求来了
  3. 3 获取全部授权特性Authorize,拼装全部Requirement(Roles、 Policy)和Scheme,放入到AuthorizationPolicy
  4. 将全部的Scheme都鉴权一遍,信息保存到context.User
  5. 校验全部的Requirement(Roles、 Policy)
  6. 由AuthorizationMiddlewareResultHandler中间件处理结果:成功则继续 未登录就Challenge ,未授权就Forbid

10. UseAuthorization源码

  1. 常规的UseMiddleware, Build时实例化中间件----注入IAuthorizationPolicyProvider---里面注入IOptions options---就是上端配置的各种Policy规则初始化放入缓存(特殊说明: Scheme是这样设计,但授权不知道前面是哪里注入了Options)---保存到Options的PolicyMap(又是缓存了)

  2. 请求来了-----检查endpoint---给context的Item增加个值,然后是从Metadata里面找下IAuthorizeData(其实就是AuthorizeAttribute)—包括Aciton、 Controller、全局、 ModelConversion

  3. AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData)把Policy和授权特性的要求都组装起来----先看有没有authorizeData(授权要求,没有就啥也不干)---有授权特性,会把全部规则都组装起来:

    1. 遍历authorizeData(代表可以声明多个,效果叠加),实例化一个AuthorizationPolicyBuilder(负责包含全部规则)
    2. 先看Policy---标记特性AuthorizeData有Policy-----用Name去PolicyProvider去map缓存中获取AuthorizationPolicy----然后Combine就是将AuthenticationSchemes和Requirements保存在PolicyProvider(依旧是policy的scheme和requirement)--没有的话采用默认Policy,就是不允许匿名,也就是得有个合法凭证
    3. 再看Roles---标记特性AuthorizeData有Roles----先Split拆分,再每个Role都是RolesAuthorizationRequirement----再添加到PolicyBuilder的Requirements(其实也是转成了Requirement)
    4. 最后看Schemes---标记特性AuthorizeData有Schemes,就直接添加到PolicyBuilder的AuthenticationSchemes
    5. 最后再Builder下,把全部Requirements和去重后的AuthenticationSchemes,交给AuthorizationPolicy实例化
  4. 组装完了,就是一个AuthorizationPolicy,里面是全部的Schemes和Requirement,后面就是校验了,能得到的信息:

    1. Options还能设置DefaultPolicy,默认就是个DenyAnonymousAuthorizationRequirement,不允许匿名使用,也就是日常只标记个[Authorize],只要鉴权成功就能通过,其实是通过这个requirement
    2. Authorize特性可以标记多个, controller+action+全局,都是叠加规则
    3. Authorize特性的Policy只能一个名字, Role和Scheme可以逗号分隔的多个值
  5. 继续授权检测,这次是全面鉴权---先找IPolicyEvaluator(PolicyEvaluator),一堆注入不看,然后执行AuthenticateAsync()----执行鉴权

    1. 为啥又鉴权? 因为前面鉴权中间件只管默认Scheme,这里可能多Scheme
    2. 如何鉴权?如果没有AuthenticationSchemes,则查看context.User?.Identity?.IsAuthenticated?,也就是之前的默认Scheme鉴权结果
    3. 如果有指定Scheme,则去遍历执行全部Scheme,分别鉴权,并MergeUserPrincipal合并到ClaimsPrincipal属性(List)
    4. 都为空就NoResult了,失败了

    收获:

    1. 支持多个Scheme鉴权,同名的去重了,各渠道都解析一遍,然后信息都保存到context.User了
    2. 不添加Authorization的Scheme,则是用默认的Scheme,就不用鉴权
    3. 添加了Scheme,这里会根据Scheme去鉴权一次的--即使就是默认Scheme
  6. 检查IAllowAnonymous特性,有就直接过去了---(为啥不早点检查,甚至在鉴权就检查?我没有答案)

  7. 继续授权检测----用policyEvaluator来做AuthorizeAsync根据信息检查授权,其实靠的DefaultAuthorizationService做授权检测具体动作:

    1. IAuthorizationHandlerContextFactory准备上下文,就是各种信息拼起来
    2. IAuthorizationHandlerProvider获取授权AuthorizationHandler,这个是IOC注册进来的各种IAuthorizationHandler合集,默认是CustomPassThroughAuthorizationHandler,可以支持多个注册
    3. 遍历执行AuthorizationHandler中的HandleAsync----其实就是遍历全部Requiredment去执行的HandleAsync(),在父类里面,遍历全部该Requiredment类型,执行HandleRequirementAsync(到了Requiredment里面的校验方法了)-----把角色都拿出来,任意一个只要在contextUser里面的某个Identity满足就行(确实支持多个scheme获取的多个用户信息)---任意一个校验通过就行
    4. 规则是全部成功就过去,一个Failed就失败了
    5. 最后转化个Succeed结果,成功---Forbid---Challenge,保存起来了
  8. 根据授权结果处理----找IAuthorizationMiddlewareResultHandler去HandleAsync(),根据不同的authorizeResult分别执行不同的逻辑

    1. Challenged:就先看Scheme,没指定就看默认Scheme。有指定,就遍历Schme,去AuthenticationHttpContextExtensions执行Scheme的Challenged ,实际上又跑到AuthenticationService去Challenged ,又找到那个AuthenticateHandler(自定义各种处理方式的)去Challenged--回到鉴权的结果--return
    2. Forbidden 同Challenged
    3. 成功就继续咯 然后这里执行的next()下个中间件

11. 总结Use

也就是几个步骤,比较好理解,只是其中很多细节:

  1. 根据特性标记,找到全部的授权检测要求Requirement和Scheme
  2. 用全部的Scheme全部鉴权一下,都存起来
  3. 匿名特性支持
  4. 用全部的requirement分别检测下
  5. 成功--未登录--未授权分别执行对应的动作

细节还是特别多,只能靠测试。。。出问题了能定位

12. 扩展定制

  1. 自定义IAuthorizeData和AllowAnonymous,代替默认的---很简单,自己来。。
  2. 单Requirement扩展+多Requirement扩展
  3. 多Scheme,多Policy,多演练,会有蛮多细节

13. 授权验证总结

  1. 多个Role是并列关系,多个Policy会报错
  2. 单个的Authorize声明, Policy和Role需要同时满足
  3. 多Scheme时,信息合集满足约束就行
  4. 多Policy同时满足---标记多个Authorize特性
  5. 多Policy的或关系? ---只能多Requirement

14 . 鉴权授权流程总结

  1. 鉴权授权的时机,在Routing后,在EndPoint前
  2. 二者的交互,通过一个context.User来完成---其实在授权环节,又调用了鉴权
  3. 鉴权的Scheme设计和授权的Policy设计,几乎一样-----通过Options去Add、里面都是用的Builder、通过注入Options完成初始化、提供key-value式缓存、中间件里面再通过Provider来获取、找XXService、提供的各种Handler,非常典型的设计
  4. 多Scheme多Policy的设计的有点巧妙有点复杂
  5. 细节太多了,不可能都记住,但是理解很重要,很有底!

五、 JWT

1. 4W学习法

学习JWT,还是What Why When How

要讲Why,就得从起源说起:

  1. Http无状态&轻量级,请求--响应式--传输是文本
  2. Cookie/Session阶段
  3. Session共享阶段
  4. 分布式的去中心化需求---Token
  5. JWT: Json Web Token,是token的一种, json通用格式,语言无关的

2. JWT信任问题

A验证账号密码,颁发个Token给U

U带着Token去访问W---W都不跟A通信,直接认可Token? U带着Token去访问T-----T都不跟A通信,直接认可Token? T带着Token去访问W----W都不跟A通信,直接认可Token?

为啥可以这样做?信任基础在哪里?加密算法!

解决了信任问题,那去中心化就实现了而且特别棒,没有瓶颈

3. 加密算法

  1. HS256对称可逆加密算法:双方之间仅共享一个 密钥。由于使用相同的密钥生成签名和验证签名, 因此必须注意确保密钥不被泄密---加密速度快,字符串短点,性能高
  2. RS256非对称可逆加密算法:使用公共/私钥对: 标识提供方采用私钥生成签名, 使用方获取公钥以验证签名。公钥通常通过一个元数据URL提供----安全性好,但性能低
  3. HMACSHA256不可逆加密算法:将不同长度字符串转成固定长度且唯一的字符串,不可逆向解密---通常用来做签名防篡改

4. JWT令牌结构

  1. Header头--{ “alg”: “HS256”, “typ”: “JWT”}—描述下加密算法
  2. Payload有效载荷---没加密,只是序列化,任何人都可以轻松读取(前端轻松可用)
  3. Signature签名---防止抵赖,防止篡改

签名原理:

  1. 颁发时:账号密码验证通过后,获取用户信息---然后先base64UrlEncode(header)+“.”+ base64UrlEncode(payload), 得到一个字符串xx.yyyyy
  2. 用HMACSHA256不可逆算法对xx.yyyyy加密, (还有个key参数,就用是公开的解密钥),得到固定长度字符串b---(目的是把内容变短)
  3. 然后用加密钥把字符串b加密,得到字符串zzzz,然后JWT就是xx.yyyyy.zzzz
  4. 客户端验证时:先拿到公钥,对zzzz进行解密得到字符串c---能解开证明秘钥是对的, zzzz来自服务器
  5. 然后对xx.yyyyy进行HMACSHA256加密,得到字符串d,比对字符串c和d---相同则证明Token信息没有被篡改
  6. 至于有效期啥的,解析后再根据业务要求进阶检测:有效期、其他信息等等

5. 实操JWT

建立独立鉴权授权中心, Core WebAPI,完成JWT生成:

  1. XXXX.NET7.AuthenticationCenter鉴权授权服务器(验证用户,颁发token)
  2. 配置基础信息(对称+不对称)
  3. Swagger发起请求,完成验证,创建Token

来个客户端集成JWT鉴权,就用AuthenticationCenter,增加个AuthController:

  1. 添加鉴权授权中间件
  2. 配置鉴权方式
  3. 新增AuthController,标记授权
  4. 配置下Swagger配置接受BearerToken

Web使用JWT

完整的Web页面登陆+验证使用:

  1. 浏览器请求服务端,服务端调用鉴权中心获取Token
  2. 前端用localStorage存储, ajax请求时放到Header
  3. Get/Post要求基础鉴权授权

6. RSA算法来一遍

HS256是对称可逆加密,是同一个秘钥,只能用在公司内部 RSA256非对称可逆加密,是一组密钥对,支持外部第三方使用

  1. 鉴权授权中心升级,使用RSA算法
  2. 启动时生成密钥对,提供公钥获取方法(没用上)
  3. 客户端URL获取,或手动拷贝到本地指定目录
  4. 客户端升级验证方式
  5. 流程验证是一样的---完全不需要改动

7. JWT与鉴权授权

JWT的使用过程,基本上也是一入一出: 一入:鉴权授权中心写入Token 一出:客户端使用Token信息

然后, JWT+鉴权+授权是怎么协同的?

  1. JWT只是个数据传输格式,基于加密算法完成信任---然后其信息是公开的,配置一下后自动解析,其实也能自定义解析,前端js也能解析----JWTTokenDeserialize.AnalysisToken(token)
  2. 鉴权,就是在通过指定位置读取Token信息,然后解析,然后写入到context.User
  3. 授权,这是根据这个context.User完成权限检测

鉴权需要的几个信息:

  1. 数据来源:基于Scheme知悉
  2. 数据格式:基于Scheme知悉
  3. 检测要求: TokenValidationParameters,是否检测、值、检测方式
  4. 处理事件: JwtBearerEvents—也是扩展处理动作---观察者模式扩展
  5. 数据传递: base.HttpContext.User.Identities---数据最好是CliamType类型的

JWT+授权,跟Cookie UrlToken没啥区别,因为授权跟Scheme没啥关联:

  1. 角色授权,注意CliamType
  2. 策略授权
  3. 各种自定义授权

授权跟鉴权的方式是没有任何关系的 鉴权是获取用户信息 授权是检测用户信息是否合格

8. AddCookie源码

AddCookie的源码看看,其实跟AddJWT差别不大,在ASP.NET Core源码里:

  1. 通过AuthenticationBuilder来AddScheme,还是Add到Option—就是AddScheme---CookieAuthenticationHandler
  2. CookieAuthenticationHandler多层父类,其实落脚点在AuthenticationHandler
  3. 几个核心方法在父类,子类override一下

InitializeAsync:完成初始化 ForbidAsync:搞个虚方法, 403--给子类覆写 ChallengeAsync:搞个虚方法, 401--给子类覆写 AuthenticateAsync:去子类的看HandleAuthenticateAsync, EnsureCookieTicket就是去读取cookie值---CheckForRefreshAsync 做滑动过期---然后做检验---然后发现了

Event的执行Event在AuthenticationHandler就初始化对象时完成初始化,用Options的Event---然后在整个生命周期中,就直接调用该事件

9. AddJWT源码

AddJWT是几乎一样的

  1. 通过AuthenticationBuilder来AddScheme,还是Add到Option
  2. 靠的是JwtBearerHandler来解析
  3. 几个核心方法在父类,子类override一下
  4. 植入多个Event动作,观察者,用来扩展执行流程中的各种动作

InitializeAsync:完成初始化 ForbidAsync:搞个虚方法, 403--给子类覆写 ChallengeAsync:搞个虚方法, 401--给子类覆写 AuthenticateAsync:去子类的看HandleAuthenticateAsync--Events.MessageReceived- --然后按规则读取Token---然后根据TokenValidationParameters遍历校验---一系列的Event扩展支持

10. JWT鉴权授权扩展

  1. 多属性验证以及验证逻辑扩展TokenValidationParameters
  2. JWT鉴权多事件扩展, Event
  3. JWT灵活授权扩展
  4. 多Scheme多Policy,跟之前一样

六、Token

1. JWT局限性

稍微回顾下Token的机制:通过加密算法来保证Token的来源,以及准确性因此可以做到去中心化验证

既然去中心化了,那就有几个问题无法解决:

  1. Token泄露,安全问题
  2. 修改密码/删除用户, Token失效问题
  3. Token能做到滑动过期吗?

其实都是去中心化带来的问题,或者说是JWT机制带来的问题

2. Token泄露

关乎Token泄露问题:

  1. SSL通信,传输中途不会窃取;但是到了浏览器确实没办法,别人能拿到浏览器,那真的没办法---(Cookie或者其他方案都没区别)
  2. 额外验证点东西, IP地址,浏览器类型(写在Validator---Event,或者自己额外写在授权环节)---做的少,非常规

重放攻击:无止境的重复请求----请求表单带个随机数---随机数搞个Redis---执行前先Redis一下(随机数不是在token)

3. Token过期问题

修改密码、删除用户,希望让Token立即失效---目前是做不到,有以下思路:

  1. 修改密码/删除用户,就更新秘钥,所有的token都失效—而且不断变证书—不可取

  2. 要想及时过期,客户端和鉴权授权中心必须有通信!

    • 生成token时—除了生成token(含guid)—还生成个guid+用户id—写入redis
    • 验证token时---拿guid去redis校验
    • 改密码---redis那一项数据—之前的给删掉/过期/无效
    • 验证旧token—发现过期
    • 验证新token就没事儿

    有企业在用,一般是局域网式—但是比较有限制,不太JWT

  3. 减少有效期---降低伤害,这也算是一个思路

4. 滑动过期

不能用着用着,突然说我过期了,要重新登陆---就是要修改token的有效期? --不行! Token是不会变的,而且只能鉴权授权中心发的

不能默认检测有效期,可以扩展自定义校验(或者写在授权环节)---第一次校验成功后把token写一下Redis(有效期30分钟)—再往后的检测就是:要么Redis有,可以直接用-用了修改有效期;没有的话就检测token有效期----能做到滑动过期,也是依赖于Redis

其实,更推荐用的叫刷新Token

5. 刷新Token设计

更推荐的是刷新Token设计,业界主流方式:

  1. 也叫双Token,一个AccessToken,一个RefreshToken,有效期不一样, AccessToken有效期60分钟, RefreshToken有效期7x24小时
  2. 常规请求是AccessToken,超时后程序自动用用RefreshToken去授权中心获取新的AccessToken(不是账号密码---是为了自动请求,不需要用户操作,无感)
  3. 颁发token时,把RefreshToken缓存一下,再次请求直接看缓存,颁发新的Token,一直到RefreshToken过期,就结束了,才重新登陆

既能长时间有效,又能定期去验证一下,而且用户无感

6. 刷新Token全流程

鉴权授权中心升级:

  1. 支持双Token获取,和缓存
  2. 支持Token刷新获取
  3. refreshToken管理: A 有效期校验 B 清除

基于swagger完成测试:

  1. 调用LoginWithRefresh获取双Token
  2. 基于refreshToken去调用RefreshToken

客户端支持刷新Token,包括前端和后端:

  1. 后端得支持双Token,支持刷新Token
  2. 前端支持, N多前端代码细节
  3. 后端JWT校验事件支持

每隔1分钟,其实accesstoken都要过期---就会跟授权服务器通信一次---全过程是无感的

流程校验:

  1. 先登陆---然后点击,是正常的
  2. 过期后,再点击---过期+刷新Token+自动点击,通了

7. 总结刷新Token

  1. 其实就是JWT,只是加了端编程逻辑
  2. 客户端定期跟服务端通信一次,频率可控---防止数据长期滞后
  3. 做好封装,全程自动化的

安全----------没用 Token失效---非实时,但可控 滑动过期-----直接长有效期