OAuth&OpenIDConnect是什么?
最近因为工作的原因,大概有两个月时间没写博客了,本来今年给自己的目标是每个月写一篇,或许记录工作中踩过的一些坑,或许学习一些新的技术框架。说实话IDS4的源码我是在几天前才开始看的,因为工作需要实现一个认证授权服务中心,临时抱佛脚研究了几天源码和查了一些资料,我个人的习惯是三方的开源框架,如果不了解源码实现,一般情况下不敢贸然使用。以下内容如果有说的不对的地方,希望大家及时指出。接下来我就结合OAuth&OIDC以及IDS4的源码跟大家剖析一下Identityserver4(以下简称IDS4)的实现原理。
在介绍IDS4之前,我们先来看下OAuth和OpenIDConnect(以下简称OIDC)协议的一些概念,因为只有在理解这两个协议规范的基础之上才能理解IDS4。
OAuth是什么?最直接的方式,我们可以通过查看OAuth2的官方文档,地址是 https://oauth.net/2/。官方文档是英文版的,建议大家在阅读IDS4的源码之前多少看看这个文档,我个人的理解是OAuth是一个开放式授权委托协议,是一种规范,它可以让资源所有者委托应用软件(注意是委托),从IDP(IDP是一种服务提供商,NETCORE平台下的实现是IDS4)获取授权AccessToken访问资源所有者受保护的资源。接下来我们一起看看OAuth的授权流程。
OAuth2的授权流程
在官方文档里边大概有这么一张图片,描述了OAuth2的整个授权流程。
![](https://img2020.cnblogs.com/blog/542901/202109/542901-20210905232035523-1250396797.png)
在介绍授权流程之前,我想先简单解释以下OAuth2授权里边涉及的这么4个角色,我用红圈标注了。1.Client我们可以把它理解为客户端应用;2.ResourceOwner也就是前面描述的资源所有者;3.AuthorizationServer就是授权服务提供商简称IDP,在本篇文章里面就是IDS4;4.ResourceServer资源服务器,资源所有者受保护的资源就在这里。下边我们一起看看授权流程。
我们以WEB-ASPNETCORE应用为例,ResourceOwner(用户)操作Client(WEB应用)去访问ResourceServer(资源服务器)上面的某个受保护的资源,这时候因为ASPNETCORE内置的认证系统会重定向到AuthorizationServer(授权服务器),当然在授权之前还需要身份认证,OAuth2它不管认证,只管授权,一般情况下在认证通过之后,会跳转到一个授权页面,里面一般包含了Scope也就是你的权限范围,最后完成授权。关于整个授权流程里面涉及的参数以及返回值我在后续实例会详细说。下面我们来看下OAuth2里边支持哪些授权类型?
OAuth2的授权类型(模式)
从官方文档的定义来看,OAuth2的授权类型主要包含6种,但是我们常见的只有4种。1.AuthorizationCode授权码类型,也是我们常用的授权模式,一般是通过授权服务器中介完成,典型的场景就是我上面的例子WEB-ASPNETCORE应用,该模式主要是通过授权码获取AccessToken访问令牌,而且授权码是一次性的;2.Implicit简化授权类型,有些朋友称之为隐式授权类型,它应该是code类型的简化版本,没有授权码这个过程,授权服务器直接返回AccessToken到浏览器,安全相一般,但是使用方便,它的应用场景主要是浏览器内的客户端应用,比如Angular等;3.ResourceOwnerPasswordCredentials密码授权类型,名字比较长,安全系数不高,需要用户与应用具有高度的信任的环境;4.ClientCredentials客户端授权类型,一般应用场景就是资源不属于某个所有者但是客户端又需要获取。授权类型概念也差不多简单介绍完了,下面我们再看看OAuth2端点这个概念。
OAuth2端点(EndPoint)
OAuth2端点主要分为3个AuthorizationEndPoint(授权端点)和TokenPoint(令牌端点)以及RedirectionEndpoint(重定向端点)。授权端点一般是在浏览器里面进行交互和处理,通俗一点说就是资源所有者委托客户端从授权服务器那登录以及授权,可以使用Post|Get请求,必须支持Get请求。令牌端点一般是客户端应用与授权服务器进行交互,通俗来说就是客户端和授权服务器交换令牌token的过程都是需要tokenendpoint参与完成,必须使用Post请求。以上介绍的4种授权类型只有Implicit简化授权类型没有采用令牌端点,其他均访问了令牌端点。OAuth2授权协议就介绍到这吧,下边我们看看OIDC。
什么是OIDC?
OIDC它是基于OAuth2的协议之上构建的一个简单身份层,通俗的说就是身份认证协议,简单理解就是OIDC = Authentication + Authorization + OAuth2。在OAuth2 in Action 这本书里面把授权比喻成巧克力,这个巧克力不是最终的产品还是一种原料,身份认证被比喻成软糖,按照这本书的理解,我们要制作巧克力软糖,也就是需要一个基于OAuth2的身份认证协议,而OpenIDConnect就是这样协议规范,它可以工作在不同的身份认证供应商之间。关于OIDC和OAuth2角色映射自己查下资料吧,就是叫法不同,没什么暗箱操作,OIDC协议需要注意的是它定义了3个流程,AuthorizationCodeFlow、HybridFlow、ImplicitFlow,其实这3个流程对应的就是OAuth2的几种授权类型,比如HybridFlow它叫混合流程,包含OAuth2授权类型里面的code & implicit,概念就到这吧。
什么是IDS4?
IdentityServer4 是ASPNETCore2.X系列量身打造的一款基于OpenIDConnect和OAuth2认证框架,也就是OIDC认证服务提供商。它实现了OpenIDConnect和OAuth2的大部分协议,同时遵循Apache2的这么一个开源协议,github地址是https://github.com/IdentityServer/IdentityServer4,大家如果下载建议先Star以下。上面说了这么多,还不如一张图来的实在。
![](https://img2020.cnblogs.com/blog/542901/202109/542901-20210905232035170-1916276665.png)
这张图不是我画的,是从IDS4官网扣的。引用官方的描述IDS4大概可以为我们做这么几件事。
1.保护你的资源
2.使用本地帐户或通过外部身份提供程序对用户进行身份验证
3.提供会话管理和单点登录
4.管理和验证客户机
5.向客户发出标识和访问令牌
6.验证令牌
接下来我们具体看看IDS4是如何实现OAuth2和OIDC完成我们的统一身份认证和授权的,我会从OAuth2常见的授权类型里面抽取两个授权类型实例加以讲解,并且结合IDS4框架源码加以分析,我们先从简单的开始吧。
ClientCredentials
实例代码是IDS4源码里面Samples文件下的Demo工程,大家可以自行下载,GITHUB地址我在上面已经贴出来了。
ClientCredentials实例解决方案下面有3个工程文件夹,分别是API对应我们OAuth2里面的受保护资源,Client对应我们OAuth2里面的客户端应用,在这里是控制台程序,IdentityServer不用多说就是IDS4,认证授权服务。
请求流程
![](https://img2020.cnblogs.com/blog/542901/202109/542901-20210905232034653-1284542423.png)
ClientCredentials授权类型的请求流程很简单,自己看吧。下面我们一起看看实例代码。
Client端代码
1 private static async Task Main() 2 { 3 // 其他代码... 4 var client = new HttpClient(); 5 // 没啥说的,OIDC协议规范定义的端点发现机制,当然你也可以绕过这步,感兴趣的可以看看这个方法的实现 6 var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000"); 7 8 // 通过clientid secret tokenendpoint等信息获取accesstoken访问令牌 9 var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest 10 { 11 Address = disco.TokenEndpoint, 12 ClientId = "client", 13 ClientSecret = "secret", 14 Scope = "api1" 15 }); 16 Console.WriteLine(tokenResponse.Json); 17 // 通过accesstoken 调用 受保护的api 18 var apiClient = new HttpClient(); 19 apiClient.SetBearerToken(tokenResponse.AccessToken); 20 var response = await apiClient.GetAsync("http://localhost:5001/identity"); 21 if (!response.IsSuccessStatusCode) 22 { 23 Console.WriteLine(response.StatusCode); 24 } 25 else 26 { 27 var content = await response.Content.ReadAsStringAsync(); 28 Console.WriteLine(JArray.Parse(content)); 29 } 30 }
client代码很简单,我也大概做了注释和删掉一些验证的代码,相信大家一看就明白了。接下来我们继续看IDS4这边的代码。
IdentityServer代码
1 // 注入服务 2 public void ConfigureServices(IServiceCollection services) 3 { 4 // 注入IDS4的基础服务 5 var builder = services.AddIdentityServer() 6 // 注入resource资源到内存,该资源我们可以理解为ids4本地资源,比如用户信息等 7 .AddInMemoryIdentityResources(Config.GetIdentityResources()) 8 // 注入api资源,也就是那些受保护的api 9 .AddInMemoryApiResources(Config.GetApis()) 10 // 注入client客户端资源 11 .AddInMemoryClients(Config.GetClients()); 12 if (Environment.IsDevelopment()) 13 { 14 // 临时开发环境下的证书 15 builder.AddDeveloperSigningCredential(); 16 } 17 } 18 // 注入中间件 19 public void Configure(IApplicationBuilder app) 20 { 21 // 注册ids4中间件 22 app.UseIdentityServer(); 23 }
以上代码注释的比较详细,就不加以说明了,需要注意的一个地方就是这些资源都是从config这个类里面加载出来的,我们下面看看资源具体是什么样。题外话,上边这两方法来源于Starpup启动类的两个方法,关于Starpup的加载执行机制可以看我上一篇文章。
Config代码
1 public static class Config 2 { 3 public static IEnumerable<IdentityResource> GetIdentityResources() 4 { 5 return new IdentityResource[] 6 { 7 new IdentityResources.OpenId() 8 }; 9 } 10 public static IEnumerable<ApiResource> GetApis() 11 { 12 return new List<ApiResource> 13 { 14 // 注意api1这个名称,需要跟api资源Audience的名称一样,包括下边的scope里面的名称 15 new ApiResource("api1", "My API") 16 }; 17 } 18 public static IEnumerable<Client> GetClients() 19 { 20 return new List<Client> 21 { 22 new Client 23 { 24 // 客户端id 25 ClientId = "client", 26 // 授权类型 27 AllowedGrantTypes = GrantTypes.ClientCredentials, 28 // secret 29 ClientSecrets = 30 { 31 new Secret("secret".Sha256()) 32 }, 33 // scope授权范围 34 AllowedScopes = { "api1" } 35 } 36 }; 37 } 38 }
由于是示例代码,所以资源都是以硬编码的方式写死在了config类里边,代码简单不多说,生产环境下建议用数据库或者redis缓存,关于efcore的版本github上面有开源代码,有兴趣的朋友可以自行查看。最后我们一起看下api这个工程里边的代码。
API代码
1 public void ConfigureServices(IServiceCollection services) 2 { 3 // 注入mvc的核心服务 4 services.AddMvcCore() 5 // 注入aspnetcore认证服务 6 .AddAuthorization() 7 .AddJsonFormatters(); 8 // 注入jwtbearer基础服务 9 services.AddAuthentication("Bearer") 10 .AddJwtBearer("Bearer", options => 11 { 12 // ids4地址,用于验证accesstoken 13 options.Authority = "http://localhost:5000"; 14 // 非必须使用https 15 options.RequireHttpsMetadata = false; 16 // api资源名称 17 options.Audience = "api1"; 18 }); 19 } 20 public void Configure(IApplicationBuilder app) 21 { 22 // 注入aspnetcore认证中间件 23 app.UseAuthentication(); 24 app.UseMvc(); 25 } 26 27 [Route("identity")] 28 [Authorize] // 需要认证应用携带正确的accesstoken才能访问 29 public class IdentityController : ControllerBase 30 { 31 public IActionResult Get() 32 { 33 return new JsonResult(from c in User.Claims select new { c.Type, c.Value }); 34 } 35 }
api代码的风格类似ids4认证服务,我注释的也很详细,关于aspnetcore的认证机制可以查看我上一篇文章。以上代码就是IDS4的ClientCredentials的授权,下边我们重点看看IDS4的源代码实现。
ClientCredentials授权IDS4源码分析
在分析ids4源码之前,我们先看看OAuth协议针对ClientCredentials客户端授权的一些参数&返回值规范。
Request: POST /token HTTP/1.1 Host: authorization-server.com grant_type=client_credentials // 必须 &client_id=xxxxxxxxxx // 必须 &client_secret=xxxxxxxxxx // 必须
Response:
1 HTTP/1.1 200 OK 2 Content-Type: application/json 3 Cache-Control: no-store 4 Pragma: no-cache 5 { 6 "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", // 必须 7 "token_type":"bearer", // 必须 8 "expires_in":3600, // 9 "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk", 10 "scope":"create" 11 }
Request&Response我是从OAuth官方文档上面扒下来的,可能有朋友会好奇,我为什么要贴这一部分东西?我个人觉得读ids4的源码和aspnetcore或者其他开源代码有一些区别,ids4的源代码代码量并不多,也没有很多逻辑,但是它是遵循OAuth2和OIDC这两个协议规范实现的这么一个框架,换句话说如果你完全不了解这两个协议规范的情况下去阅读ids4,你会被它跳晕。即便你能看懂里面的逻辑,你也不知道它为什么要这么干。下面我们通过Fiddler工具查看ClientCredentials授权类型的请求和响应报文。
![](https://img2020.cnblogs.com/blog/542901/202109/542901-20210905232035332-1284245218.png)
从Fiddler上的请求监控来看,ClientCredentials授权类型一共有6个请求,其中前面4个是client发起的,后面2个是api资源向ids4发起的请求。下面我们具体看看这些请求涉及的报文。
openid-configuration&openid-configuration/jwks
configuration响应报文
![](https://img2020.cnblogs.com/blog/542901/202109/542901-20210905232035522-516109166.png)
从响应内容来看,这个请求只是获取ids4的端点信息和对OAuth2&OIDC协议的支持信息。
jwks响应报文
![](https://img2020.cnblogs.com/blog/542901/202109/542901-20210905232035362-920504165.png)
这个请求其实就是从ids4获取公钥,用户验证jwt的数字签名部分。
connect/token
请求报文
![](https://img2020.cnblogs.com/blog/542901/202109/542901-20210905232034670-1241817565.png)
响应报文
![](https://img2020.cnblogs.com/blog/542901/202109/542901-20210905232035240-1786612451.png)
以上两段报文信息内容比较简单,这里我想简单说下这个accesstoken访问令牌,它是由ids4框架里面的tokenendpoint端点负责生成,这个令牌就是后续请求受保护资源api的这么一个通行证。它的生成机制,在后续的ids4源码分析里面我会详细介绍。
/identity
请求头
![](https://img2020.cnblogs.com/blog/542901/202109/542901-20210905232035361-1991437988.png)
携带了上一个请求返回的accesstoken,并且token类型是Bearer。
响应内容
![](https://img2020.cnblogs.com/blog/542901/202109/542901-20210905232035123-809868230.png)
这个没啥特别的,就是api资源返回的内容。需要注意的地方,在此之前,也就是当前请求响应之前,api资源向ids4授权服务器发起了2个请求,也就是前面图里边显示的最后两个请求。这两个请求其实际是JwtBearer认证的范畴,因为我们在api工程的startup启动类里边配置了它的认证类型是JwtBearer并且identitycontroller上面配置授权特性[Authorize],熟悉aspnetcore认证授权机制的朋友应该知道,在执行action之前会做认证授权处理,因为未认证直接返回challenge,最终处理由认证handler子类JwtBearerHandler接管,在JwtBearerHandler处理逻辑中,通过调用我们api资源服务JwtBearer的options配置的Authority地址,获取ids4公钥(也就是后面的jwks请求)做accesstoken签名校验,后面两个请求我就不贴图片了,跟上面的两个请求一样。关于aspnetcore认证机制可以看我上一篇帖子。接下来我们来看一个稍微复杂一点的实例。
Hybrid Flow & AuthorizationCode Implicit
Hybrid Flow认证流程非常适合WEB服务器应用,OIDC官方文档描述HybridFlow流程其实是OAuth2 Code和Implicit授权类型的组合,也是最常用的一种认证流程吧。这个实例解决方案包含3个工程,client为aspnetcoremvc,api为webapicore,idp为ids4,该Demo实例支持自定义策略授权而且支持ReferenceToken刷新AccessToken,登出等操作,首先我们看下Hybrid Flow流程大致请求原理,见下图
![](https://img2020.cnblogs.com/blog/542901/202109/542901-20210905232035743-337447360.png)
这副图引用的是园子里边solenovex大神的,请求逻辑在这副图上已经描述的很清楚了,我就不再介绍,但是这里有个地方需要注意,response_type这个参数有3种组合,图示里面展示的是最多的一种情况,code(授权码)、id_token(用户token)、token(就是accesstoken),细心的朋友发现accesstoken、idtoken返回了两次,其实它们的值是不同的,最终访问受保护的api资源用的是下一个accesstoken。下面我们直接上代码吧。
identityserver
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_1); 4 // 其他代码... 5 // 注入ids4基础服务 6 var builder = services.AddIdentityServer(options => 7 { 8 options.Events.RaiseErrorEvents = true; 9 options.Events.RaiseInformationEvents = true; 10 options.Events.RaiseFailureEvents = true; 11 options.Events.RaiseSuccessEvents = true; 12 }) 13 // 测试用户 14 .AddTestUsers(TestUsers.Users); 15 // 这些个类似第一个实例 16 builder.AddInMemoryIdentityResources(Config.GetIdentityResources()); 17 builder.AddInMemoryApiResources(Config.GetApis()); 18 builder.AddInMemoryClients(Config.GetClients()); 19 // 临时开发证书,生成环境建议使用机构颁发的证书 20 if (Environment.IsDevelopment()) 21 { 22 builder.AddDeveloperSigningCredential(); 23 } 24 } 25 public void Configure(IApplicationBuilder app) 26 { 27 if (Environment.IsDevelopment()) 28 { 29 app.UseDeveloperExceptionPage(); 30 } 31 // 注册ids4中间件 32 app.UseIdentityServer(); 33 app.UseStaticFiles(); 34 app.UseMvcWithDefaultRoute(); 35 }
startup里面的代码跟上一个实例的代码几乎一样,这个实例里面我们着重看看这些资源。
resources
1 public static IEnumerable<IdentityResource> GetIdentityResources() 2 { 3 // 身份资源,类似上一个实例 4 return new IdentityResource[] 5 { 6 new IdentityResources.OpenId(), 7 new IdentityResources.Profile(), 8 new IdentityResources.Address(), 9 new IdentityResources.Phone(), 10 new IdentityResources.Email(), 11 // 策略资源,也是身份资源的一种吧 12 new IdentityResource("names", "姓名", new List<string> { "FamilyName" }), 13 }; 14 } 15 public static IEnumerable<ApiResource> GetApis() 16 { 17 return new ApiResource[] 18 { 19 new ApiResource("api1", "testapi", new List<string> { "FamilyName" }) 20 { 21 // 这个secrets主要是用于api在ids4上的验证,因为需要referencetoken刷新accesstoken 22 ApiSecrets = { new Secret("api1 secret".Sha256()) } 23 } 24 }; 25 } 26 public static IEnumerable<Client> GetClients() 27 { 28 return new[] 29 { 30 new Client 31 { 32 ClientId = "hybrid client mvc", 33 ClientSecrets = {new Secret("hybrid secret".Sha256()) }, 34 AllowedGrantTypes = GrantTypes.Hybrid, // 混合认证流程,OIDC协议 35 AccessTokenType = AccessTokenType.Jwt, // accesstoken类型jwt,还有referencetoken类型 36 AllowAccessTokensViaBrowser = true, 37 AccessTokenLifetime = 60, // 时间单位是秒,表示accesstoken60秒后过期 38 RedirectUris = // 跳转登录地址 39 { 40 "http://localhost:5002/signin-oidc" 41 }, 42 PostLogoutRedirectUris = // 跳转登出地址 43 { 44 "http://localhost:5002/signout-callback-oidc" 45 }, 46 AllowOfflineAccess = true, 47 AlwaysIncludeUserClaimsInIdToken = true, 48 AllowedScopes = 49 { 50 "api1", 51 IdentityServerConstants.StandardScopes.OpenId, 52 IdentityServerConstants.StandardScopes.Email, 53 IdentityServerConstants.StandardScopes.Address, 54 IdentityServerConstants.StandardScopes.Phone, 55 IdentityServerConstants.StandardScopes.Profile, 56 "names" // 策略scope 57 } 58 } 59 }; 60 } 61 62 // 测试用户 63 public static List<TestUser> Users = new List<TestUser> 64 { 65 new TestUser{ 66 SubjectId = "1", Username = "test", Password = "test", 67 Claims = 68 { 69 new Claim(JwtClaimTypes.Name, "test"), 70 new Claim(JwtClaimTypes.FamilyName, "test"), 71 new Claim(JwtClaimTypes.Email, "test@email.com"), 72 new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), 73 new Claim(JwtClaimTypes.Address, @"{ '街道': '万家丽', '地址': '长沙','country': '大中国' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json), 74 } 75 }, 76 new TestUser{ 77 SubjectId = "2", Username = "demo", Password = "demo", 78 Claims = 79 { 80 new Claim(JwtClaimTypes.Name, "demo"), 81 new Claim(JwtClaimTypes.FamilyName, "demo"), 82 new Claim(JwtClaimTypes.Email, "demo@email.com"), 83 new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), 84 new Claim(JwtClaimTypes.Address, @"{ '街道': '五一广场', '地址': '长沙','country': '大中国' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json), 85 new Claim("name", "FamilyName"), // 授权策略claim 86 } 87 } 88 }; 89 }
代码也很简单,跟上一个实例的资源代码有一定的区别,注释的地方需要多看一下。
Client
1 public void ConfigureServices(IServiceCollection services) 2 { 3 // 其他代码... 4 services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 5 JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // 清空jwt用户信息的映射关系。 6 services.AddAuthentication(options => 7 { 8 // 注册cookiesScheme,表示本地认证采用cookie认证 9 options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 10 // 注册oidcscheme,表示远程认证采用oidc认证 11 options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; 12 }) 13 // 注入cookie认证处理器, 14 .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => 15 { 16 options.AccessDeniedPath = "/Unauthorized/AccessDenied"; // 指定未授权的页面路由地址 17 }) 18 // 注入oidc认证处理器 19 .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => 20 { 21 // 认证成功登录到本地cookie,主要是解决本地认证的问题。 22 options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; 23 options.Authority = "http://localhost:5000"; // ids4 base地址 24 options.RequireHttpsMetadata = false; 25 options.ClientId = "hybrid client mvc"; 26 options.ClientSecret = "hybrid secret"; 27 options.SaveTokens = true; // 保存token到store 28 options.ResponseType = "code id_token token"; // responsetype有三种组合,这里使用的是组合最多的一种,可以看前面一张图 29 options.Scope.Clear(); 30 options.Scope.Add("api1"); 31 options.Scope.Add(OidcConstants.StandardScopes.OpenId); 32 options.Scope.Add(OidcConstants.StandardScopes.Profile); 33 options.Scope.Add(OidcConstants.StandardScopes.Email); 34 options.Scope.Add(OidcConstants.StandardScopes.Phone); 35 options.Scope.Add(OidcConstants.StandardScopes.Address); 36 options.Scope.Add("names"); // ids4那边定义的策略名称 37 options.Scope.Add(OidcConstants.StandardScopes.OfflineAccess); 38 }); 39 services.AddAuthorization(options => 40 { 41 options.AddPolicy("FamilyNamePolicy", builder => // 注入自定义策略 42 { 43 builder.AddRequirements(new NameRequirement()); 44 }); 45 }); 46 // 注入策略处理器 47 services.AddSingleton<IAuthorizationHandler, NameHandler>(); 48 } 49 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 50 { 51 if (env.IsDevelopment()) 52 { 53 app.UseDeveloperExceptionPage(); 54 } 55 else 56 { 57 app.UseExceptionHandler("/Home/Error"); 58 } 59 app.UseStaticFiles(); 60 // 注入认证中间件 61 app.UseAuthentication(); 62 app.UseCookiePolicy(); 63 app.UseMvc(routes => 64 { 65 routes.MapRoute( 66 name: "default", 67 template: "{controller=Home}/{action=Index}/{id?}"); 68 }); 69 }
客户端的startup里边的代码我也做了详细注释,需要注意的是更前面一个实例的client代码截然不同,比较绕的地方主要是对aspnetcore的认证授权系统的理解,由于篇幅问题,AddOpenidConnect这个方法里面的源码我就不贴了,我大概讲一下源码的实现逻辑,AddOpenidConnect方法内部其实际是注入了OIDC认证的处理器,这个处理器是OpenIdConnectHandler,这个handler跟jwthandler或者cookiehandler在aspnetcore平台里面的结构是一样样的,只是它们核心方法的自身逻辑处理各有不同,比如cookiehandler的认证方法,其内部实现是处理本地cookie并设置context,在授权阶段如果发现未认证返回challangeresult,最后由cookiehandler的challanger接管,跳转未认证页面,类似的oidc差别不大,它在认证阶段发生跳转,跳转到我们在options指定的地址,并完成认证,想要更多了解aspnetcore认证机制请参考我前面的文章,记得推介呵呵。下面我们看下授权策略怎么应用。
1 // homecontroller 2 public class HomeController : Controller 3 { 4 // 其他成员 5 6 public IActionResult Index() 7 { 8 var user = User.Identity; 9 return View(); 10 } 11 // 没什么特别的,其实就是aspnetcore内置那套策略机制 12 [Authorize(Policy = "FamilyNamePolicy")] 13 public IActionResult Privacy() 14 { 15 return View(); 16 } 17 } 18 19 // 自定义策略规则 20 public class NameHandler : AuthorizationHandler<NameRequirement > 21 { 22 // 自定义策略处理器 23 protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 24 NameRequirement requirement) 25 { 26 var familyName = context.User.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.FamilyName)?.Value; 27 if (familyName == "demo" && context.User.Identity.IsAuthenticated) // 规则为claim familyname为demo&为登录用户 28 { 29 context.Succeed(requirement); 30 return Task.CompletedTask; 31 } 32 context.Fail(); 33 return Task.CompletedTask; 34 } 35 }
早期版本我们在做授权的时候一般是基于角色,就灵活性而已,策略更具灵活性,目前是aspnetcore授权主流。说完策略授权之后,我们简单来看下accesstoken过期的刷新的问题。
1 // 刷新accesstoken 2 private async Task GetNewTokensAsync() 3 { 4 var client = new HttpClient(); 5 var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000"); 6 if (disco.IsError) 7 { } 8 var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); 9 // 获取新的token,包含idtoken accesstoken referencetoken等信息 10 var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest 11 { 12 Address = disco.TokenEndpoint, 13 ClientId = "hybrid client mvc", 14 ClientSecret = "hybrid secret", 15 Scope = "api1 openid profile email phone address names", 16 GrantType = OpenIdConnectGrantTypes.RefreshToken, 17 RefreshToken = refreshToken 18 }); 19 if (tokenResponse.IsError) 20 { } 21 // 刷新过期时间 22 var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn); 23 var tokens = new[] 24 { 25 new AuthenticationToken 26 { 27 Name = OpenIdConnectParameterNames.IdToken, 28 Value = tokenResponse.IdentityToken 29 }, 30 new AuthenticationToken 31 { 32 Name = OpenIdConnectParameterNames.AccessToken, 33 Value = tokenResponse.AccessToken 34 }, 35 new AuthenticationToken 36 { 37 Name = OpenIdConnectParameterNames.RefreshToken, 38 Value = tokenResponse.RefreshToken 39 }, 40 new AuthenticationToken 41 { 42 Name = "expires_at", 43 Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) 44 } 45 }; 46 // 本地重新cookie认证 47 var currentAuthenticateResult = 48 await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); 49 // 本地存储token 50 currentAuthenticateResult.Properties.StoreTokens(tokens); 51 // 重新登录 52 await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, 53 currentAuthenticateResult.Principal, currentAuthenticateResult.Properties); 54 return; 55 }
最后我们来看下api资源的代码。
API受保护的资源
1 // startup启动类 2 public void ConfigureServices(IServiceCollection services) 3 { 4 services.AddMvc(); 5 services.AddMvcCore() 6 .AddAuthorization() 7 .AddJsonFormatters(); 8 services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) 9 .AddIdentityServerAuthentication(options => // 之前这个地方是jwtbeare,我猜只是ids4的扩展方法对jwtbeare换了个马甲,后续源码分析会介绍 10 { 11 options.Authority = "http://localhost:5000"; // ids4 base基地址 12 options.ApiName = "api1"; // 刷新token需要提供api名称 13 options.RequireHttpsMetadata = false; 14 options.ApiSecret = "api1 secret"; // 需要刷新token ids4需要提供secret 15 options.JwtValidationClockSkew = TimeSpan.FromMinutes(1); // 检查时间,也就是api到ids4token验证时间间隔 16 }); 17 services.AddMemoryCache(); 18 } 19 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 20 { 21 // 注册中间件 22 app.UseAuthentication(); 23 app.UseMvc(); 24 }
ClientCredentials & HybridFlow认证流程的两个实例到此就全部介绍完毕,Fiddler监控图片我就比贴了,自行看下OAuth2和OIDC协议就行,请求和响应都是严格按照这两协议来实现的,接下来做个简单的总结吧。从ClientCredentials & HybridFlow这两个实例代码来看,其代码本身并不复杂,只需一些简单配置即可。IDS4遵循和实现了OAuth2 & OIDC大部协议,如果想要更好的理解IDS4源代码,首先建议看下OAUTH2和OIDC的一些协议规范,其次就是自身平台的认证授权系统,比如如果我的client或者api是netframework | netcore,那我们就需要熟悉这个平台自身的认证授权系统,其他异构平台均一样。最后一个环节,ids4源码分析。
IDS4源码分析
我个人在看netcore平台上面源码的时候,首先就是找到starpup启动类,然后找到对应的Middleware中间件,在IDS4这个框架里边有一个叫IdentityServerMiddleware的这么一个中间件,其实它就是我们要找的这个中间件,ids4认证授权的入口。下面我们看下它的定义。
1 public class IdentityServerMiddleware 2 { 3 // 其他成员... 4 public IdentityServerMiddleware(RequestDelegate next, ILogger<IdentityServerMiddleware> logger) 5 { 6 _next = next; 7 _logger = logger; 8 } 9 10 public async Task Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events) 11 { 12 // 从本地cookie读取认证信息,如果以认证直接设置请求上下文 13 await session.EnsureSessionIdCookieAsync(); 14 try 15 { 16 // 这个地方我们把它理解为请求路由查找对应的endpoint端点,有点类似拦截,因为如果匹配到请求地址,endpoint端点处理完之后直接response 17 var endpoint = router.Find(context); 18 if (endpoint != null) 19 { 20 // 如果匹配到由端点处理,比如我们上面介绍的tokenendpoint在处理完请求之后直接通过executeasync把accesstoken等信息直接写入response 21 var result = await endpoint.ProcessAsync(context); 22 if (result != null) 23 { 24 // 写入response 25 await result.ExecuteAsync(context); 26 } 27 28 return; 29 } 30 } 31 catch (Exception ex) 32 { 33 await events.RaiseAsync(new UnhandledExceptionEvent(ex)); 34 _logger.LogCritical(ex, "Unhandled exception: {exception}", ex.Message); 35 throw; 36 } 37 38 // 如果未被路由端点匹配,传递到下一个中间件。 39 await _next(context); 40 } 41 }
从IdentityServerMiddleware中间件的定义来看,特别的地方就是这些端点以及它们的处理,我们的OAuth2 & OIDC 的大部分协议就实现在这里边,接下来我们看看具体有哪些端点?在startup的configureservices方法里边,注入了这么一个方法叫addidentityserver,在这个扩展方法里面注入了端点,看代码。
1 // 注入平台默认的端点 2 public static IIdentityServerBuilder AddDefaultEndpoints(this IIdentityServerBuilder builder) 3 { 4 builder.Services.AddTransient<IEndpointRouter, EndpointRouter>(); 5 builder.AddEndpoint<AuthorizeCallbackEndpoint>(EndpointNames.Authorize, ProtocolRoutePaths.AuthorizeCallback.EnsureLeadingSlash()); 6 builder.AddEndpoint<AuthorizeEndpoint>(EndpointNames.Authorize, ProtocolRoutePaths.Authorize.EnsureLeadingSlash()); 7 builder.AddEndpoint<CheckSessionEndpoint>(EndpointNames.CheckSession, ProtocolRoutePaths.CheckSession.EnsureLeadingSlash()); 8 builder.AddEndpoint<DeviceAuthorizationEndpoint>(EndpointNames.DeviceAuthorization, ProtocolRoutePaths.DeviceAuthorization.EnsureLeadingSlash()); 9 builder.AddEndpoint<DiscoveryKeyEndpoint>(EndpointNames.Discovery, ProtocolRoutePaths.DiscoveryWebKeys.EnsureLeadingSlash()); 10 builder.AddEndpoint<DiscoveryEndpoint>(EndpointNames.Discovery, ProtocolRoutePaths.DiscoveryConfiguration.EnsureLeadingSlash()); 11 builder.AddEndpoint<EndSessionCallbackEndpoint>(EndpointNames.EndSession, ProtocolRoutePaths.EndSessionCallback.EnsureLeadingSlash()); 12 builder.AddEndpoint<EndSessionEndpoint>(EndpointNames.EndSession, ProtocolRoutePaths.EndSession.EnsureLeadingSlash()); 13 builder.AddEndpoint<IntrospectionEndpoint>(EndpointNames.Introspection, ProtocolRoutePaths.Introspection.EnsureLeadingSlash()); 14 builder.AddEndpoint<TokenRevocationEndpoint>(EndpointNames.Revocation, ProtocolRoutePaths.Revocation.EnsureLeadingSlash()); 15 builder.AddEndpoint<TokenEndpoint>(EndpointNames.Token, ProtocolRoutePaths.Token.EnsureLeadingSlash()); 16 builder.AddEndpoint<UserInfoEndpoint>(EndpointNames.UserInfo, ProtocolRoutePaths.UserInfo.EnsureLeadingSlash()); 17 return builder; 18 }
OAuth2协议里边定义的端点,大部分都在这里,tokenendpoint、authorizeendpoint等等还有oidc协议里边的userinfoendpoint等,接下来我们就从里面挑一两个详细介绍。
TokenEndPoint端点
TokenEndPoint端点就是我们那些token的生成都是由它来完成的,老套路,先看定义。
1 internal class TokenEndpoint : IEndpointHandler 2 { 3 private readonly IClientSecretValidator _clientValidator; 4 private readonly ITokenRequestValidator _requestValidator; 5 private readonly ITokenResponseGenerator _responseGenerator; 6 private readonly IEventService _events; 7 private readonly ILogger _logger; 8 9 // 其他成员... 10 11 public TokenEndpoint( 12 IClientSecretValidator clientValidator, 13 ITokenRequestValidator requestValidator, 14 ITokenResponseGenerator responseGenerator, 15 IEventService events, 16 ILogger<TokenEndpoint> logger) 17 { 18 _clientValidator = clientValidator; 19 _requestValidator = requestValidator; 20 _responseGenerator = responseGenerator; 21 _events = events; 22 _logger = logger; 23 } 24 25 // tokenendpoint端点处理请求入口方法 26 public async Task<IEndpointResult> ProcessAsync(HttpContext context) 27 { 28 29 // 首先验证http操作,非post 表单提交直接返回错误 30 if (!HttpMethods.IsPost(context.Request.Method) || !context.Request.HasFormContentType) 31 { 32 _logger.LogWarning("Invalid HTTP request for token endpoint"); 33 return Error(OidcConstants.TokenErrors.InvalidRequest); 34 } 35 // 进一步验证 36 return await ProcessTokenRequestAsync(context); 37 } 38 39 private async Task<IEndpointResult> ProcessTokenRequestAsync(HttpContext context) 40 { 41 // 首先验证client 就是oauth2协议里边定义的clientid secret等 42 var clientResult = await _clientValidator.ValidateAsync(context); 43 if (clientResult.Client == null) // 如果验证未通过直接返回invalidclient 44 { 45 return Error(OidcConstants.TokenErrors.InvalidClient); 46 } 47 var form = (await context.Request.ReadFormAsync()).AsNameValueCollection(); 48 _logger.LogTrace("Calling into token request validator: {type}", _requestValidator.GetType().FullName); 49 // 然后验证请求参数,这些参数可以查看OAuth2,根据不同的GrantType启动不同的验证规则,并且生成AuthorizationCode 50 var requestResult = await _requestValidator.ValidateRequestAsync(form, clientResult); 51 if (requestResult.IsError) 52 { 53 await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult)); 54 return Error(requestResult.Error, requestResult.ErrorDescription, requestResult.CustomResponse); 55 } 56 // create response 57 _logger.LogTrace("Calling into token request response generator: {type}", _responseGenerator.GetType().FullName); 58 // 生成accesstoken,如果有需要还会生成refencetoken id token等 59 var response = await _responseGenerator.ProcessAsync(requestResult); 60 await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult)); 61 LogTokens(response, requestResult); 62 // return result 63 _logger.LogDebug("Token request success."); 64 // 返回 65 return new TokenResult(response); 66 } 67 }
接下来我们看看client的验证规则,client的验证规则由IClientSecretValidator接口实现,下面我们看下它的定义。
1 public interface IClientSecretValidator 2 { 3 // 验证 4 Task<ClientSecretValidationResult> ValidateAsync(HttpContext context); 5 }
就干一件事验证。它的具体实现由它子类ClientSecretValidator完成。我们继续看。
1 public class ClientSecretValidator : IClientSecretValidator 2 { 3 // 其他成员... 4 public ClientSecretValidator(IClientStore clients, SecretParser parser, SecretValidator validator, IEventService events, ILogger<ClientSecretValidator> logger) 5 { 6 _clients = clients; 7 _parser = parser; 8 _validator = validator; 9 _events = events; 10 _logger = logger; 11 } 12 // client验证实现 13 public async Task<ClientSecretValidationResult> ValidateAsync(HttpContext context) 14 { 15 _logger.LogDebug("Start client validation"); 16 var fail = new ClientSecretValidationResult 17 { 18 IsError = true 19 }; 20 // 默认预设4种parser,这里为PostBodySecretParser,验证context form是否包含clien secret 21 var parsedSecret = await _parser.ParseAsync(context); 22 if (parsedSecret == null) 23 { 24 await RaiseFailureEventAsync("unknown", "No client id found"); 25 _logger.LogError("No client identifier found"); 26 return fail; 27 } 28 // 从内存或者其他存储介质通过id获取client,默认是内存里面加载,如果未重写资源存储 29 var client = await _clients.FindEnabledClientByIdAsync(parsedSecret.Id); 30 if (client == null) 31 { 32 await RaiseFailureEventAsync(parsedSecret.Id, "Unknown client"); 33 _logger.LogError("No client with id '{clientId}' found. aborting", parsedSecret.Id); 34 return fail; 35 } 36 SecretValidationResult secretValidationResult = null; 37 if (!client.RequireClientSecret || client.IsImplicitOnly()) 38 { 39 _logger.LogDebug("Public Client - skipping secret validation success"); 40 } 41 else 42 { 43 secretValidationResult = await _validator.ValidateAsync(parsedSecret, client.ClientSecrets); 44 if (secretValidationResult.Success == false) 45 { 46 await RaiseFailureEventAsync(client.ClientId, "Invalid client secret"); 47 _logger.LogError("Client secret validation failed for client: {clientId}.", client.ClientId); 48 return fail; 49 } 50 } 51 _logger.LogDebug("Client validation success"); 52 // 返回clientresult这么一个对象 53 var success = new ClientSecretValidationResult 54 { 55 IsError = false, 56 Client = client, 57 Secret = parsedSecret, 58 Confirmation = secretValidationResult?.Confirmation 59 }; 60 await RaiseSuccessEventAsync(client.ClientId, parsedSecret.Type); 61 return success; 62 } 63 }
认证完client之后,接着验证request请求,request请求被定义在接口ITokenRequestValidator这个接口里面,我们看看它的定义。
1 public interface ITokenRequestValidator 2 { 3 4 Task<TokenRequestValidationResult> ValidateRequestAsync(NameValueCollection parameters, ClientSecretValidationResult clientValidationResult); 5 }
这个接口办事也是非常专一的,就这么一个方法,接下来我们看看它的实现类 TokenRequestValidator。
1 internal class TokenRequestValidator : ITokenRequestValidator 2 { 3 // 其他代码 4 public async Task<TokenRequestValidationResult> ValidateRequestAsync(NameValueCollection parameters, ClientSecretValidationResult clientValidationResult) 5 { 6 7 _validatedRequest = new ValidatedTokenRequest 8 { 9 Raw = parameters ?? throw new ArgumentNullException(nameof(parameters)), 10 Options = _options 11 }; 12 13 if (clientValidationResult == null) throw new ArgumentNullException(nameof(clientValidationResult)); 14 15 _validatedRequest.SetClient(clientValidationResult.Client, clientValidationResult.Secret, clientValidationResult.Confirmation); 16 // 验证protocoltype是发oidc 17 if (_validatedRequest.Client.ProtocolType != IdentityServerConstants.ProtocolTypes.OpenIdConnect) 18 { 19 LogError("Invalid protocol type for client", 20 new 21 { 22 clientId = _validatedRequest.Client.ClientId, 23 expectedProtocolType = IdentityServerConstants.ProtocolTypes.OpenIdConnect, 24 actualProtocolType = _validatedRequest.Client.ProtocolType 25 }); 26 27 28 return Invalid(OidcConstants.TokenErrors.InvalidClient); 29 } 30 31 // grantType是否为空 32 var grantType = parameters.Get(OidcConstants.TokenRequest.GrantType); 33 if (grantType.IsMissing()) 34 { 35 LogError("Grant type is missing"); 36 return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType); 37 } 38 // 验证granttype字符串长度 39 if (grantType.Length > _options.InputLengthRestrictions.GrantType) 40 { 41 LogError("Grant type is too long"); 42 return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType); 43 } 44 45 _validatedRequest.GrantType = grantType; 46 // 根据不同的granttype,执行指定的验证规则 47 switch (grantType) 48 { 49 case OidcConstants.GrantTypes.AuthorizationCode: 50 return await RunValidationAsync(ValidateAuthorizationCodeRequestAsync, parameters); 51 case OidcConstants.GrantTypes.ClientCredentials: 52 return await RunValidationAsync(ValidateClientCredentialsRequestAsync, parameters); 53 case OidcConstants.GrantTypes.Password: 54 return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters); 55 case OidcConstants.GrantTypes.RefreshToken: 56 return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters); 57 case OidcConstants.GrantTypes.DeviceCode: 58 return await RunValidationAsync(ValidateDeviceCodeRequestAsync, parameters); 59 default: 60 return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters); 61 } 62 } 63 64 }
TokenRequestValidator代码比较多,我删掉了大部分,都是验证相关的。接下来我们看看AuthorizationCode具体的验证。
1 private async Task<TokenRequestValidationResult> RunValidationAsync(Func<NameValueCollection, Task<TokenRequestValidationResult>> validationFunc, NameValueCollection parameters) 2 { 3 // 还是验证,包括AllowedGrantTypes 生成code等等,具体就不贴代码了。 4 var result = await validationFunc(parameters); 5 if (result.IsError) 6 { 7 return result; 8 } 9 10 // run custom validation 11 _logger.LogTrace("Calling into custom request validator: {type}", _customRequestValidator.GetType().FullName); 12 13 // 自定义验证,验证到天荒地老,还好默认没有实现。 14 var customValidationContext = new CustomTokenRequestValidationContext { Result = result }; 15 await _customRequestValidator.ValidateAsync(customValidationContext); 16 17 if (customValidationContext.Result.IsError) 18 { 19 if (customValidationContext.Result.Error.IsPresent()) 20 { 21 LogError("Custom token request validator", new { error = customValidationContext.Result.Error }); 22 } 23 else 24 { 25 LogError("Custom token request validator error"); 26 } 27 28 return customValidationContext.Result; 29 } 30 31 LogSuccess(); 32 return customValidationContext.Result; 33 }
终于验证完了,接下来我们看看验证返回的TokenRequestValidationResult这个对象。
1 public class TokenRequestValidationResult : ValidationResult 2 { 3 // 其他代码... 4 // 这个属性就包含了前面疯狂验证的那些参数 5 public ValidatedTokenRequest ValidatedRequest { get; } 6 7 public Dictionary<string, object> CustomResponse { get; set; } 8 }
疯狂验证完之后就返回这么一个对象TokenRequestValidationResult,里面囊括了所有request这些验证过的参数还有个response字典。验证完之后,包括一些初始化完成之后,接下来就是创建response了。response的创建是由具有这么一个定义的接口ITokenResponseGenerator实现。
1 public interface ITokenResponseGenerator 2 { 3 Task<TokenResponse> ProcessAsync(TokenRequestValidationResult validationResult); 4 }
比较低调,就一个方法。我们来看下它的实现类TokenResponseGenerator。
1 public class TokenResponseGenerator : ITokenResponseGenerator 2 { 3 // 其他代码... 4 public virtual async Task<TokenResponse> ProcessAsync(TokenRequestValidationResult request) 5 { 6 switch (request.ValidatedRequest.GrantType) 7 { 8 case OidcConstants.GrantTypes.ClientCredentials: 9 return await ProcessClientCredentialsRequestAsync(request); 10 case OidcConstants.GrantTypes.Password: 11 return await ProcessPasswordRequestAsync(request); 12 case OidcConstants.GrantTypes.AuthorizationCode: 13 return await ProcessAuthorizationCodeRequestAsync(request); 14 case OidcConstants.GrantTypes.RefreshToken: 15 return await ProcessRefreshTokenRequestAsync(request); 16 case OidcConstants.GrantTypes.DeviceCode: 17 return await ProcessDeviceCodeRequestAsync(request); 18 default: 19 return await ProcessExtensionGrantRequestAsync(request); 20 } 21 } 22 }
这个类里面代码量也不小,其他我都删掉了,就留下这个入口方法ProcessAsync。接下来我们看看ProcessAuthorizationCodeRequestAsync方法的实现。
1 protected virtual async Task<TokenResponse> ProcessAuthorizationCodeRequestAsync(TokenRequestValidationResult request) 2 { 3 // 生成accesstoken机制,时间比较晚了,生成代码我就不贴了,我大概说下逻辑吧,jwt为列,header部分不用说,ploay部分是Claim身份声明信息,签名部分由rsa私钥做的签名, 4 // refreshtoken 如果配置需要生成,则生成,配置要求我在实例里面已经注释了。 5 (var accessToken, var refreshToken) = await CreateAccessTokenAsync(request.ValidatedRequest); 6 var response = new TokenResponse 7 { 8 AccessToken = accessToken, 9 AccessTokenLifetime = request.ValidatedRequest.AccessTokenLifetime, 10 Custom = request.CustomResponse, 11 Scope = request.ValidatedRequest.AuthorizationCode.RequestedScopes.ToSpaceSeparatedString(), 12 }; 13 // 如果需要生成refreshtoken就设置值 14 if (refreshToken.IsPresent()) 15 { 16 response.RefreshToken = refreshToken; 17 } 18 // AuthorizationCode授权的生成idtoken,包括Hybridflow等 19 if (request.ValidatedRequest.AuthorizationCode.IsOpenId) 20 { 21 Client client = null; 22 if (request.ValidatedRequest.AuthorizationCode.ClientId != null) 23 { 24 client = await Clients.FindEnabledClientByIdAsync(request.ValidatedRequest.AuthorizationCode.ClientId); 25 } 26 if (client == null) 27 { 28 throw new InvalidOperationException("Client does not exist anymore."); 29 } 30 var resources = await Resources.FindEnabledResourcesByScopeAsync(request.ValidatedRequest.AuthorizationCode.RequestedScopes); 31 var tokenRequest = new TokenCreationRequest 32 { 33 Subject = request.ValidatedRequest.AuthorizationCode.Subject, 34 Resources = resources, 35 Nonce = request.ValidatedRequest.AuthorizationCode.Nonce, 36 AccessTokenToHash = response.AccessToken, 37 ValidatedRequest = request.ValidatedRequest 38 }; 39 var idToken = await TokenService.CreateIdentityTokenAsync(tokenRequest); 40 var jwt = await TokenService.CreateSecurityTokenAsync(idToken); 41 response.IdentityToken = jwt; 42 } 43 // 返回response 44 return response; 45 }
response生成完之后,通过TokenResult包装并返回,接下来我们看看TokenResult对象的定义。
1 internal class TokenResult : IEndpointResult 2 { 3 public TokenResponse Response { get; set; } 4 public TokenResult(TokenResponse response) 5 { 6 if (response == null) throw new ArgumentNullException(nameof(response)); 7 Response = response; 8 } 9 // 把tokenresponse写入context输出流,json格式 10 public async Task ExecuteAsync(HttpContext context) 11 { 12 context.Response.SetNoCache(); 13 var dto = new ResultDto 14 { 15 id_token = Response.IdentityToken, 16 access_token = Response.AccessToken, 17 refresh_token = Response.RefreshToken, 18 expires_in = Response.AccessTokenLifetime, 19 token_type = OidcConstants.TokenResponse.BearerTokenType, 20 scope = Response.Scope 21 }; 22 if (Response.Custom.IsNullOrEmpty()) 23 { 24 await context.Response.WriteJsonAsync(dto); 25 } 26 else 27 { 28 var jobject = ObjectSerializer.ToJObject(dto); 29 jobject.AddDictionary(Response.Custom); 30 await context.Response.WriteJsonAsync(jobject); 31 } 32 } 33 // ids4的团队也有点随意啊。 34 internal class ResultDto 35 { 36 public string id_token { get; set; } 37 public string access_token { get; set; } 38 public int expires_in { get; set; } 39 public string token_type { get; set; } 40 public string refresh_token { get; set; } 41 public string scope { get; set; } 42 } 43 }
到此,这篇随笔算是写完了。
最后来个大总结吧,还是那句话,ids4本身代码不是那么的复杂,前提是其一要对OAuth2 & OIDC协议有所了解,虽然是英文版本的没关系,我英语也相当差,不是有有道吗?而且自己多少也认识几个单词啊。其二自身平台的认证授权机制要比较熟悉,毕竟跟ids4有太多的交互,而且安全要求也比较高,本来打算还讲下扩展资源存储到sqlserver或者mysql,时间太晚了,明天还要上班不写了,下个月见。谢谢各位看官支持, 有帮到你请点个推介,要求不高呵呵,谢谢。