ASP.NET Core 7 源码阅读5:鉴权授权
2023-07-10 15:35:12从Http无状态协议---用户持久化的需求---最初是Cookie+Session---Token但是不管是哪种方式,有一段内容是不变的:
- 请求服务端,获取凭证
- 客户端再次请求,带上凭证
- 服务端识别凭证,判断是否允许访问
鉴权授权的本质是做啥的?
其实就是第3步解决用户识别和判断授权的问题被拆分成了2个动作:
- 鉴权Authentication:凭证识别解析,有没有登陆,凭证有没有过期,是张三还是李四
- 授权Authorization:基于解析来权限检测,判断下张三/李四有没有权限访问这个资源
一、实操
1. 没有任何权限要求实操
- 添加相关控制器, Cookie式登陆方法
- UseAuthentication + AddAuthentication + Cookie
- UseAuthorization + AddAuthorization(可不写)
- [Authorize]
- [AllowAnonymous]
校验流程:
- 未登陆,直接访问,跳转登录页
- AllowAnonymous可直接访问
- 登陆后跳转,正常访问
- 退出后访问,跳转登陆页
2. 授权
- Roles授权: [Authorize(Roles = "Admin")]
- Policy授权: [Authorize(Policy = "AdminPolicy")]
- 复杂Policy授权: [Authorize(Policy = "MutiPolicy")]
授权的设计,也是可以满足开发者各种这样的需求—目前还没演示完
如果是没有登陆,是跳转LoginPath---401
如果是有凭证,但没有权限,是跳转AccessDeniedPath--403
3. 发送了什么?
请求到达Action之前,即使没有标记,也会自动鉴权
- 主动鉴权:理解鉴权信息保存
- 主动授权:理解授权检测过程
4. 理解总结
鉴权授权是ASP.NET Core框架封装的2个中间件,目的在于请求进入具体的Filter-M-V-C之前,通过中间件完成用户权限检测包含2个步骤:
- 鉴权Authentication:鉴别有没有登陆,解析是张三还是李四(且将信息传递下去)---UseAuthentication配置Http管道,保证请求来了,都要做一次凭证的解析AddAuthentication配置IOC,告诉如何鉴权(凭证在那儿,啥格式的)
- 授权Authorization:判断下张三/李四有没有权限访问这个资源---UseAuthorization告诉框架要做授权检测, AddAuthorization告诉框架如何授权检测---[Authorize]标记显示声明该方法需要授权+需要什么样的授权
这里是基本过程,全是配置下就生效了,封装的挺厉害的。。然而框架是如何实现的? 各种封装又是怎么回事儿? 开始深入!
二、理解鉴权的设计
细化鉴权---深入鉴权---鉴权就是检测凭证,鉴别是张三,是如何解析?
- 凭证的位置:凭证是怎么传递的, Cookie/JWT就不一样
- 凭证的格式:凭证是什么格式的,加密/序列化
- 信息有效性:过期时间啥的, token
- 鉴权信息传递:解析得到的信息保存起来,后面使用--context.User
- 特殊情况处理:没登陆/没权限怎么跳转?
以上都是鉴权该完成的动作
1. 自定义鉴权
Cookie鉴权 JWT鉴权,抑或其他鉴权,本质应该都是一样的
这里先来个自定义的鉴权方式,一是帮助理解鉴权,二是方便后续调试学源码
- 实现个UrlTokenAuthenticationHandler---主要是实现IAuthenticationHandler
- 注册鉴权---AddScheme---告诉框架如何解析
- 标记+验证---带Token就ok 不带就跳转---自定义Scheme已完成!
2. 鉴权Authentication上帝视角
鉴权授权是紧密关联的,通过context.User通信的(授权期间其实也会调用鉴权)
先执行AddAuthentication:
- 设置默认Scheme+4个核心对象的IOC注册
- 支持配置AuthenticationOptions,里面支持了AddScheme,AuthenticationSchemeBuilder去组装Scheme(未Build)
- AddCookie完成校验规则配置---暂时不看Cookie细节, UrlToken是一样的
后执行UseAuthentication:
- 注册AuthenticationMiddleware中间件, Build-AuthenticationScheme
- 请求来了,执行Invoke方法,然后里面又是一套Provider--Handler--Service多对象绕一下,完成鉴权
- 只做基于默认Scheme解析,信息经过一波转换,保存到context.User
3. AddAuthentication
- 核心是AddAuthenticationCore,里面完成了4核心对象的注册---默认名字就是指定下DefaultScheme
- Options支持了AddScheme,其实是new一个AuthenticationSchemeBuilder保存起来(里面就是存了个那个handler的Type),且用Name作为key,存入字典----(没有Build,注册中间件的时候注入Provider,在构造函数里面才Build成AuthenticationScheme)
- AddCookie---AddJWT---AddUrlToken,都是一样的
最终是做了几个IOC注册,支持了不同的 字符串对应一个AuthenticationScheme (其实这里HandlerType就是将来的处理方式)
4. UseAuthentication
- Use中间件AuthenticationMiddleware时,构造函数注入AuthenticationSchemeProvider,其构造函数中先完成Options的初始化(AddSchme),然后把AuthenticationOptions的全部SchemeBuilder.Build(里面就namedisplayname-HandleType),得到全部的AuthenticationScheme,字典缓存起来---- (至此,启动环节完成—Add时把SchemeName跟Handler类型保存到字典,给到中间件)
- 请求来了---有个基于AuthenticationHandlerProvider的扩展点,直接自行处理掉请求(非常规,不管了) -----用AuthenticationSchemeProvider获取默认的AuthenticationScheme,然后准备鉴权
- context.AuthenticateAsync来鉴权,里面会调用AuthenticationService的AuthenticateAsync()方法----这里调用注入的AuthenticationHandlerProvider,找的是AuthenticationSchemeProvider---里面查找的AuthenticationScheme---然后是里面的HandlerType(之前存的),且完成实例化+初始化----然后调用handler的AuthenticateAsync()方法做鉴权---然后执行一次数据转换---转换后保存到context.User
5. Authentication核心对象
- AuthenticationSchemeProvider:包含了全部的Scheme,字典保存
- AuthenticationScheme: 2个名字+具体handler的类型
- AuthenticationService:鉴权服务, context.鉴权其实就是调用它的
- AuthenticationHandlerProvider:鉴权handler的提供程序,它会通过Scheme的Provider来获取Scheme,且完成Scheme的handler的实例化,且初始化,然后缓存
- NoopClaimsTransformation可以完成数据的转换
绕来绕去的价值,就是为了单一职责,为了扩展
6. 总结鉴权流程
- AddAuthentication注册最主要的三个对象注册
- Options把Scheme和Handler注册进去
- UseAuthentication注册认证中间件,完成AuthenticationScheme的初始化
- 请求来了,就是用默认scheme的名字去绕一圈,完成鉴权,保存到context.User
7. 3个鉴权扩展
- 自定义实现IAuthenticationHandler,也就是UrlTokenAuthenticationHandler完成鉴权来源、方式等全部扩展
- 简单扩展下IClaimsTransformation---CustomClaimsTransformation,然后IOC注册,可以将信息做一次转换
- 多Scheme问题---
- 程序是可以支持多个Scheme并存
- 鉴权中间件是默认只走默认Scheme,其他Scheme信息怎么获取?其实靠授权标记的
- 多Scheme,那授权怎么算?其实还很复杂!以后说!
三、Scheme
1. 多Scheme演练
一个站点支持多个渠道的鉴权,比如APP、 H5、微信端、 PC----Cookie+JWT+Ids4+UrlToken多Scheme鉴权配置和测试流程, 8次访问完成验证过程:
- 访问地址 http://localhost:5726/Auth/UrlCookieByDefault
- 访问地址 http://localhost:5726/Auth/UrlCookieByDefault?UrlToken=admin-123456
- 访问地址http://localhost:5726/Auth/UrlCookieByUrlToken?UrlToken=admin-123456
- 访问地址http://localhost:5726/Auth/UrlCookieByCookie
- 访问地址http://localhost:5726/Auth/Login?name=admin&password=123456
- 访问地址http://localhost:5726/Auth/UrlCookieByCookie
- 访问地址http://localhost:5726/Auth/UrlCookieByCookie?UrlToken=admin-123456
- 访问地址: http://localhost:5726/Auth/UrlCookieByDouble?UrlToken=admin-123456
2. 多Scheme鉴权结论
- 程序支持多套Scheme解析,是可以共存的
- 不标记Scheme等同于是标记默认Scheme
- 无论是否标记,或标记啥Scheme,默认鉴权都会走一遍,在鉴权中间件(它不管Filter特性)
- 可以通过标记来指定Scheme去鉴权
- 声明多个Scheme信息都保存context.User---标记的Scheme,解析后的信息是存入到context.User---默认、其他的、多个都一样,多个会合并--
- 如果标记的是其他Scheme---然后默认Scheme也传值了也解析了,但不会保存到context.User
标记相同的Scheme,会distinct—授权源码里面
四、理解授权的设计
1. 授权3属性
鉴权Authentication是解析出当下是张三,信息保存在context.User然后该授权Authorization了,判断下张三有没有权限访问这个资源(其实因为其他scheme的原因,授权环节会根据Scheme触发鉴权)
- 标记[Authorize] 走的是默认Policy,只要有合法凭证就行(有源码)
- Roles:要求用户角色信息满足(具体校验有源码)
- Policy:支持自定义各种规则(多API show)
- AuthenticationSchemes:表示用户信息的来源(已展示)
2. PolicyBuilder多API
- RequireRole
- RequireUserName
- RequireClaim
- RequireAssertion
- AddRequirements
- Requirements.Add
- Combine
上述一共7个API,一次性验证AddAuthenticationSchemes---其实就是指定Scheme这么多API,就是为了满足各种授权要求---但是最终是怎么设计
3. Authorization源码-上帝视角
AddAuthorization:
- 一系列IOC的注册,分在AddAuthorizationCore()和AddAuthorizationPolicyEvaluator()-评估,执行授权
- 支持Options里面组装各种Policy规则, Name-PolicyBuilder的缓存组合
- Role是Policy的特例, Policy的各种Require,都是转换成了Requirement,将来授权就是遍历Requirement做检验
UseAuthorization:
- 先注册中间件(实例化middleware时通过注入初始化了Policy)—然后请求来了
- 获取全部授权特性Authorize,拼装全部Requirement(Roles、 Policy)和Scheme,放入到AuthorizationPolicy
- 将全部的Scheme都鉴权一遍,信息保存到context.User
- 校验全部的Requirement(Roles、 Policy)
- 成功则继续 未登录就Challenge ,未授权就Forbid
4. AddAuthorization源码
- 各种IOC注册,主要是AddAuthorizationCore()和AddAuthorizationPolicyEvaluator(),里面就是一系列的IOC注册和一个services.Configure(configure)
- 启动过程中,某类(也许我们写的测试)初始化依赖于IOptions
,然后就执行了Configure委托---(之前AddAuthorization只是做了Configure未执行)
跟AddScheme一个套路---所以先理解为是UseAuthorization()使用中间件AuthorizationMiddleware,会注入IAuthorizationPolicyProvider,其依赖于IOptions
5. 授权策略的设计与实现
AuthorizationPolicy授权策略是如何设计的?各种Roles、 Policy里面N多方法,都是要放入到AuthorizationPolicy,最终形态是怎样的? AuthorizationPolicyBuilder是如何Build的?
- AddPolicy就是实例化AuthorizationPolicyBuilder(这里都没有Scheme的,后面授权检测时会有)
- 然后ConfigureBuilder,各种API了--实际特别简单,都转成AuthorizationRequirement存起来
- Build()就是把各种Requirement和Scheme传递过去,变成AuthorizationPolicy,而且保存到map缓存
- 校验时,就是遍历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扩展
- SingleEmailRequirement是实现接口+继承父类,然后直接Add
- CountryRequirement、 DateOfBirthRequirement-----拆开的需要做IOC注册才能校验---如果不注册,不报错,但不能校验通过
- 或条件: DoubleEmailRequirement,必须继承AuthorizationHandler,拆开接口----然后分别IOC注册----然后就是遍历多实现,支持或关系
8. 总结下AddAuthorization
- AddAuthorization各种IOC的注册
- 把各种规则Policy组装好,都是变成requirement,都放入Map缓存了下,里面是PolicyName+各种AuthorizationPolicy
- 关于Policy的设计:一个builder,支持7种方式去Add,最终都requirement,就是规则
- Requirement的3层验证: Role/Policy/动态校验
9. UseAuthorization上帝视角
AddAuthorization已经将Role、 Policy、 Requirement等规则,都转成了Requirement集合,然后把Scheme一起存起来,放入缓存Map了而UseAuthorization,自然是注册中间件+请求来了开始处理!
- 先注册中间件(实例化middleware时通过注入初始化了Policy,应该是这样设计)
- 然后是请求来了
- 3 获取全部授权特性Authorize,拼装全部Requirement(Roles、 Policy)和Scheme,放入到AuthorizationPolicy
- 将全部的Scheme都鉴权一遍,信息保存到context.User
- 校验全部的Requirement(Roles、 Policy)
- 由AuthorizationMiddlewareResultHandler中间件处理结果:成功则继续 未登录就Challenge ,未授权就Forbid
10. UseAuthorization源码
-
常规的UseMiddleware, Build时实例化中间件----注入IAuthorizationPolicyProvider---里面注入IOptions
options---就是上端配置的各种Policy规则初始化放入缓存(特殊说明: Scheme是这样设计,但授权不知道前面是哪里注入了Options)---保存到Options的PolicyMap(又是缓存了) -
请求来了-----检查endpoint---给context的Item增加个值,然后是从Metadata里面找下IAuthorizeData(其实就是AuthorizeAttribute)—包括Aciton、 Controller、全局、 ModelConversion
-
AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData)把Policy和授权特性的要求都组装起来----先看有没有authorizeData(授权要求,没有就啥也不干)---有授权特性,会把全部规则都组装起来:
- 遍历authorizeData(代表可以声明多个,效果叠加),实例化一个AuthorizationPolicyBuilder(负责包含全部规则)
- 先看Policy---标记特性AuthorizeData有Policy-----用Name去PolicyProvider去map缓存中获取AuthorizationPolicy----然后Combine就是将AuthenticationSchemes和Requirements保存在PolicyProvider(依旧是policy的scheme和requirement)--没有的话采用默认Policy,就是不允许匿名,也就是得有个合法凭证
- 再看Roles---标记特性AuthorizeData有Roles----先Split拆分,再每个Role都是RolesAuthorizationRequirement----再添加到PolicyBuilder的Requirements(其实也是转成了Requirement)
- 最后看Schemes---标记特性AuthorizeData有Schemes,就直接添加到PolicyBuilder的AuthenticationSchemes
- 最后再Builder下,把全部Requirements和去重后的AuthenticationSchemes,交给AuthorizationPolicy实例化
-
组装完了,就是一个AuthorizationPolicy,里面是全部的Schemes和Requirement,后面就是校验了,能得到的信息:
- Options还能设置DefaultPolicy,默认就是个DenyAnonymousAuthorizationRequirement,不允许匿名使用,也就是日常只标记个[Authorize],只要鉴权成功就能通过,其实是通过这个requirement
- Authorize特性可以标记多个, controller+action+全局,都是叠加规则
- Authorize特性的Policy只能一个名字, Role和Scheme可以逗号分隔的多个值
-
继续授权检测,这次是全面鉴权---先找IPolicyEvaluator(PolicyEvaluator),一堆注入不看,然后执行AuthenticateAsync()----执行鉴权
- 为啥又鉴权? 因为前面鉴权中间件只管默认Scheme,这里可能多Scheme
- 如何鉴权?如果没有AuthenticationSchemes,则查看context.User?.Identity?.IsAuthenticated?,也就是之前的默认Scheme鉴权结果
- 如果有指定Scheme,则去遍历执行全部Scheme,分别鉴权,并MergeUserPrincipal合并到ClaimsPrincipal属性(List
) - 都为空就NoResult了,失败了
收获:
- 支持多个Scheme鉴权,同名的去重了,各渠道都解析一遍,然后信息都保存到context.User了
- 不添加Authorization的Scheme,则是用默认的Scheme,就不用鉴权
- 添加了Scheme,这里会根据Scheme去鉴权一次的--即使就是默认Scheme
-
检查IAllowAnonymous特性,有就直接过去了---(为啥不早点检查,甚至在鉴权就检查?我没有答案)
-
继续授权检测----用policyEvaluator来做AuthorizeAsync根据信息检查授权,其实靠的DefaultAuthorizationService做授权检测具体动作:
- IAuthorizationHandlerContextFactory准备上下文,就是各种信息拼起来
- IAuthorizationHandlerProvider获取授权AuthorizationHandler,这个是IOC注册进来的各种IAuthorizationHandler合集,默认是CustomPassThroughAuthorizationHandler,可以支持多个注册
- 遍历执行AuthorizationHandler中的HandleAsync----其实就是遍历全部Requiredment去执行的HandleAsync(),在父类里面,遍历全部该Requiredment类型,执行HandleRequirementAsync(到了Requiredment里面的校验方法了)-----把角色都拿出来,任意一个只要在contextUser里面的某个Identity满足就行(确实支持多个scheme获取的多个用户信息)---任意一个校验通过就行
- 规则是全部成功就过去,一个Failed就失败了
- 最后转化个Succeed结果,成功---Forbid---Challenge,保存起来了
-
根据授权结果处理----找IAuthorizationMiddlewareResultHandler去HandleAsync(),根据不同的authorizeResult分别执行不同的逻辑
- Challenged:就先看Scheme,没指定就看默认Scheme。有指定,就遍历Schme,去AuthenticationHttpContextExtensions执行Scheme的Challenged ,实际上又跑到AuthenticationService去Challenged ,又找到那个AuthenticateHandler(自定义各种处理方式的)去Challenged--回到鉴权的结果--return
- Forbidden 同Challenged
- 成功就继续咯 然后这里执行的next()下个中间件
11. 总结Use
也就是几个步骤,比较好理解,只是其中很多细节:
- 根据特性标记,找到全部的授权检测要求Requirement和Scheme
- 用全部的Scheme全部鉴权一下,都存起来
- 匿名特性支持
- 用全部的requirement分别检测下
- 成功--未登录--未授权分别执行对应的动作
细节还是特别多,只能靠测试。。。出问题了能定位
12. 扩展定制
- 自定义IAuthorizeData和AllowAnonymous,代替默认的---很简单,自己来。。
- 单Requirement扩展+多Requirement扩展
- 多Scheme,多Policy,多演练,会有蛮多细节
13. 授权验证总结
- 多个Role是并列关系,多个Policy会报错
- 单个的Authorize声明, Policy和Role需要同时满足
- 多Scheme时,信息合集满足约束就行
- 多Policy同时满足---标记多个Authorize特性
- 多Policy的或关系? ---只能多Requirement
14 . 鉴权授权流程总结
- 鉴权授权的时机,在Routing后,在EndPoint前
- 二者的交互,通过一个context.User来完成---其实在授权环节,又调用了鉴权
- 鉴权的Scheme设计和授权的Policy设计,几乎一样-----通过Options去Add、里面都是用的Builder、通过注入Options完成初始化、提供key-value式缓存、中间件里面再通过Provider来获取、找XXService、提供的各种Handler,非常典型的设计
- 多Scheme多Policy的设计的有点巧妙有点复杂
- 细节太多了,不可能都记住,但是理解很重要,很有底!
五、 JWT
1. 4W学习法
学习JWT,还是What Why When How
要讲Why,就得从起源说起:
- Http无状态&轻量级,请求--响应式--传输是文本
- Cookie/Session阶段
- Session共享阶段
- 分布式的去中心化需求---Token
- 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. 加密算法
- HS256对称可逆加密算法:双方之间仅共享一个 密钥。由于使用相同的密钥生成签名和验证签名, 因此必须注意确保密钥不被泄密---加密速度快,字符串短点,性能高
- RS256非对称可逆加密算法:使用公共/私钥对: 标识提供方采用私钥生成签名, 使用方获取公钥以验证签名。公钥通常通过一个元数据URL提供----安全性好,但性能低
- HMACSHA256不可逆加密算法:将不同长度字符串转成固定长度且唯一的字符串,不可逆向解密---通常用来做签名防篡改
4. JWT令牌结构
- Header头--{ “alg”: “HS256”, “typ”: “JWT”}—描述下加密算法
- Payload有效载荷---没加密,只是序列化,任何人都可以轻松读取(前端轻松可用)
- Signature签名---防止抵赖,防止篡改
签名原理:
- 颁发时:账号密码验证通过后,获取用户信息---然后先base64UrlEncode(header)+“.”+ base64UrlEncode(payload), 得到一个字符串xx.yyyyy
- 用HMACSHA256不可逆算法对xx.yyyyy加密, (还有个key参数,就用是公开的解密钥),得到固定长度字符串b---(目的是把内容变短)
- 然后用加密钥把字符串b加密,得到字符串zzzz,然后JWT就是xx.yyyyy.zzzz
- 客户端验证时:先拿到公钥,对zzzz进行解密得到字符串c---能解开证明秘钥是对的, zzzz来自服务器
- 然后对xx.yyyyy进行HMACSHA256加密,得到字符串d,比对字符串c和d---相同则证明Token信息没有被篡改
- 至于有效期啥的,解析后再根据业务要求进阶检测:有效期、其他信息等等
5. 实操JWT
建立独立鉴权授权中心, Core WebAPI,完成JWT生成:
- XXXX.NET7.AuthenticationCenter鉴权授权服务器(验证用户,颁发token)
- 配置基础信息(对称+不对称)
- Swagger发起请求,完成验证,创建Token
来个客户端集成JWT鉴权,就用AuthenticationCenter,增加个AuthController:
- 添加鉴权授权中间件
- 配置鉴权方式
- 新增AuthController,标记授权
- 配置下Swagger配置接受BearerToken
Web使用JWT
完整的Web页面登陆+验证使用:
- 浏览器请求服务端,服务端调用鉴权中心获取Token
- 前端用localStorage存储, ajax请求时放到Header
- Get/Post要求基础鉴权授权
6. RSA算法来一遍
HS256是对称可逆加密,是同一个秘钥,只能用在公司内部 RSA256非对称可逆加密,是一组密钥对,支持外部第三方使用
- 鉴权授权中心升级,使用RSA算法
- 启动时生成密钥对,提供公钥获取方法(没用上)
- 客户端URL获取,或手动拷贝到本地指定目录
- 客户端升级验证方式
- 流程验证是一样的---完全不需要改动
7. JWT与鉴权授权
JWT的使用过程,基本上也是一入一出: 一入:鉴权授权中心写入Token 一出:客户端使用Token信息
然后, JWT+鉴权+授权是怎么协同的?
- JWT只是个数据传输格式,基于加密算法完成信任---然后其信息是公开的,配置一下后自动解析,其实也能自定义解析,前端js也能解析----JWTTokenDeserialize.AnalysisToken(token)
- 鉴权,就是在通过指定位置读取Token信息,然后解析,然后写入到context.User
- 授权,这是根据这个context.User完成权限检测
鉴权需要的几个信息:
- 数据来源:基于Scheme知悉
- 数据格式:基于Scheme知悉
- 检测要求: TokenValidationParameters,是否检测、值、检测方式
- 处理事件: JwtBearerEvents—也是扩展处理动作---观察者模式扩展
- 数据传递: base.HttpContext.User.Identities---数据最好是CliamType类型的
JWT+授权,跟Cookie UrlToken没啥区别,因为授权跟Scheme没啥关联:
- 角色授权,注意CliamType
- 策略授权
- 各种自定义授权
授权跟鉴权的方式是没有任何关系的 鉴权是获取用户信息 授权是检测用户信息是否合格
8. AddCookie源码
AddCookie的源码看看,其实跟AddJWT差别不大,在ASP.NET Core源码里:
- 通过AuthenticationBuilder来AddScheme,还是Add到Option—就是AddScheme---CookieAuthenticationHandler
- CookieAuthenticationHandler多层父类,其实落脚点在AuthenticationHandler
- 几个核心方法在父类,子类override一下
InitializeAsync:完成初始化 ForbidAsync:搞个虚方法, 403--给子类覆写 ChallengeAsync:搞个虚方法, 401--给子类覆写 AuthenticateAsync:去子类的看HandleAuthenticateAsync, EnsureCookieTicket就是去读取cookie值---CheckForRefreshAsync 做滑动过期---然后做检验---然后发现了
Event的执行Event在AuthenticationHandler就初始化对象时完成初始化,用Options的Event---然后在整个生命周期中,就直接调用该事件
9. AddJWT源码
AddJWT是几乎一样的
- 通过AuthenticationBuilder来AddScheme,还是Add到Option
- 靠的是JwtBearerHandler来解析
- 几个核心方法在父类,子类override一下
- 植入多个Event动作,观察者,用来扩展执行流程中的各种动作
InitializeAsync:完成初始化 ForbidAsync:搞个虚方法, 403--给子类覆写 ChallengeAsync:搞个虚方法, 401--给子类覆写 AuthenticateAsync:去子类的看HandleAuthenticateAsync--Events.MessageReceived- --然后按规则读取Token---然后根据TokenValidationParameters遍历校验---一系列的Event扩展支持
10. JWT鉴权授权扩展
- 多属性验证以及验证逻辑扩展TokenValidationParameters
- JWT鉴权多事件扩展, Event
- JWT灵活授权扩展
- 多Scheme多Policy,跟之前一样
六、Token
1. JWT局限性
稍微回顾下Token的机制:通过加密算法来保证Token的来源,以及准确性因此可以做到去中心化验证
既然去中心化了,那就有几个问题无法解决:
- Token泄露,安全问题
- 修改密码/删除用户, Token失效问题
- Token能做到滑动过期吗?
其实都是去中心化带来的问题,或者说是JWT机制带来的问题
2. Token泄露
关乎Token泄露问题:
- SSL通信,传输中途不会窃取;但是到了浏览器确实没办法,别人能拿到浏览器,那真的没办法---(Cookie或者其他方案都没区别)
- 额外验证点东西, IP地址,浏览器类型(写在Validator---Event,或者自己额外写在授权环节)---做的少,非常规
重放攻击:无止境的重复请求----请求表单带个随机数---随机数搞个Redis---执行前先Redis一下(随机数不是在token)
3. Token过期问题
修改密码、删除用户,希望让Token立即失效---目前是做不到,有以下思路:
-
修改密码/删除用户,就更新秘钥,所有的token都失效—而且不断变证书—不可取
-
要想及时过期,客户端和鉴权授权中心必须有通信!
- 生成token时—除了生成token(含guid)—还生成个guid+用户id—写入redis
- 验证token时---拿guid去redis校验
- 改密码---redis那一项数据—之前的给删掉/过期/无效
- 验证旧token—发现过期
- 验证新token就没事儿
有企业在用,一般是局域网式—但是比较有限制,不太JWT
-
减少有效期---降低伤害,这也算是一个思路
4. 滑动过期
不能用着用着,突然说我过期了,要重新登陆---就是要修改token的有效期? --不行! Token是不会变的,而且只能鉴权授权中心发的
不能默认检测有效期,可以扩展自定义校验(或者写在授权环节)---第一次校验成功后把token写一下Redis(有效期30分钟)—再往后的检测就是:要么Redis有,可以直接用-用了修改有效期;没有的话就检测token有效期----能做到滑动过期,也是依赖于Redis
其实,更推荐用的叫刷新Token
5. 刷新Token设计
更推荐的是刷新Token设计,业界主流方式:
- 也叫双Token,一个AccessToken,一个RefreshToken,有效期不一样, AccessToken有效期60分钟, RefreshToken有效期7x24小时
- 常规请求是AccessToken,超时后程序自动用用RefreshToken去授权中心获取新的AccessToken(不是账号密码---是为了自动请求,不需要用户操作,无感)
- 颁发token时,把RefreshToken缓存一下,再次请求直接看缓存,颁发新的Token,一直到RefreshToken过期,就结束了,才重新登陆
既能长时间有效,又能定期去验证一下,而且用户无感
6. 刷新Token全流程
鉴权授权中心升级:
- 支持双Token获取,和缓存
- 支持Token刷新获取
- refreshToken管理: A 有效期校验 B 清除
基于swagger完成测试:
- 调用LoginWithRefresh获取双Token
- 基于refreshToken去调用RefreshToken
客户端支持刷新Token,包括前端和后端:
- 后端得支持双Token,支持刷新Token
- 前端支持, N多前端代码细节
- 后端JWT校验事件支持
每隔1分钟,其实accesstoken都要过期---就会跟授权服务器通信一次---全过程是无感的
流程校验:
- 先登陆---然后点击,是正常的
- 过期后,再点击---过期+刷新Token+自动点击,通了
7. 总结刷新Token
- 其实就是JWT,只是加了端编程逻辑
- 客户端定期跟服务端通信一次,频率可控---防止数据长期滞后
- 做好封装,全程自动化的
安全----------没用 Token失效---非实时,但可控 滑动过期-----直接长有效期