前言
Identity Server4基于OAuth2.0协议的一套用于身份认证和授权的框架。OAuth2.0协议是一个委托协议,通过这个协议,我们可以让某个客户端顶着一个有资源访问权限的身份去访问那些被保护的资源。授权的流程简单概括起来,客户端应用需要先去请求Identity Server4,如果Identity Server4觉得你的身份符合就会给你一个Access Token,之后这些客户端再带着这个Access Token去访问受保护的资源(例如说一个Asp.Net Core WebApi),就能够得到想要的Response了。下面两张是来自官方的示意图。
原理概括起来是这样,但是由于存在着多种多样的客户端程序(ASP.Net Core MVC,WPF,Angular等),需要针对不同的类型选择不同的授权方式(Grant Types, 也常称flows或者protocol flows)来完成授权,接下来我们从易到难的依次学习不同的授权流程,以下篇幅是基于.NET Core 3.1与IdentityServer4 V3.1.0.0版本开发。
准备工作
- 为本地安装Identity Server4框架的模板在cmd中执行“dotnet new -i identityServer4.Templates"安装Identity Server4的应用模板。执行完之后可调用“dotnet new --help”看到我们的vs多出6个模板,如下图
- 接着我们需要选择一种模板来创建项目,简单起见,我们先选择了In-Memory Stores and Test User这个方式,因为这个从名字看就知道是提供给学习者入门用的,算是简单的一种了,接着cd到一个存放项目的路径下后,接着执行: dotnet new is4inmem --name Is4Test1,这样就能创建一个Identity Server4的项目了,这就是template的便捷。
- 接下来补充一点理论上的东西,我们前面提到根据不同的情况,我们会选择不同的授权方法,在OAuth2.0上授权类型分为下图(官网上的图)这几种,今天我们从 “Client Credentials 客户端凭证”的授权方式入手,先从这个开始学习也是因为这是最简单的一种方式,这种方式常用于没有具体用户(人)的情况下,用官文的话说就是Machine to Machine Communication,我们从代码中边做实验边学习。
配置Identity Server4
首先我们打开Identity Server4项目,就是刚刚利用模板创建的项目“Is4Test1”,由于我们选择的模板是将身份认证的数据存在内存中的,在Startup.cs中,可以看到在ConfigureServices方法中利用静态类进行了必要数据的填充。
1 builder.AddInMemoryIdentityResources(Config.Ids); 2 builder.AddInMemoryApiResources(Config.Apis); 3 builder.AddInMemoryClients(Config.Clients);
从Config类导航进去可以看到相关的信息,在Client Credentials授权方式下,我们需要在授权服务器上定义两样东西
-
API资源,可以看成我们需要保护资源的门牌号吧。
1 public static IEnumerable<ApiResource> Apis => 2 new ApiResource[] 3 { 4 new ApiResource("api1", "WebApi #1"), 5 new ApiResource("api2", "MVC #2"), 6 };
-
Client客户端,这里并非是定义我们的客户端程序,看成客户端程序访问授权服务器时,用以身份验证的Client,其中包括为几个属性赋值
1 public static IEnumerable<Client> Clients => 2 new Client[] 3 { 4 new Client 5 { 6 ClientId = "client1", //看成“申请授权的客户端程序”的账号 7 ClientName = "Client Credentials Client", //看成只是对这个Client的描述信息 8 AllowedGrantTypes = GrantTypes.ClientCredentials, //枚举类型,指明我们的授权方式是:Client Credentials 9 ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) }, //可以看成“申请授权的客户端程序”的密码 10 AllowedScopes = { "api1" } //这个客户端可以访问“api1”这个门牌号里对应的资源 11 } 12 };
接着,需要在Startup的ConfigureService中添加对IdentityServer4的服务注册
1 //在这里注册Identity Server4 2 var builder = services.AddIdentityServer(options => 3 { 4 options.Events.RaiseErrorEvents = true; 5 options.Events.RaiseInformationEvents = true; 6 options.Events.RaiseFailureEvents = true; 7 options.Events.RaiseSuccessEvents = true; 8 }); 9 10 // 将数据保存在内存中 11 builder.AddInMemoryIdentityResources(Config.Ids); //用户的身份信息,也是受保护资源的一部分(Identity Data) 12 //由于当前使用Client Credentials方式没有具体的用户,所以这部分资源暂时用不上 13 builder.AddInMemoryApiResources(Config.Apis); //受保护的资源(Apis) 14 builder.AddInMemoryClients(Config.Clients); //授权的用户模式 15 builder.AddTestUsers(TestUsers.Users); //增加了测试用户
到此,可以使用Client Credential的授权服务器就已经配置完毕了。接下来直接运行就会跳转到5000端口的默认页面(如果没有改配置中的端口的话默认是5000),这个时候访问“http://localhost:5000/.well-known/openid-configuration”或者点击页面上的“discovery document”链接便可跳转到Identity Server4的“发现文档(discovery Doc)”可以看到授权服务器的很多终结点(EndPoint)信息和描述信息,这些终结点是后面客户端程序访问授权服务器需要用到的。如下图可见。
创建要添加保护的Api资源
当Identity Server4运行起来后就可以创建一个我们想保护的Api资源。我们创建一个ASP.NET Core WebApi项目,配置端口为5001(自便,不要和授权服务器的5000端口冲突就好),随后创建一个Controller命名为“IdentityController”。
1 namespace API1.Controllers 2 { 3 [Route("identity")] 4 [Authorize] 5 public class IdentityController : ControllerBase 6 { 7 [HttpGet] 8 public IActionResult Get() 9 { 10 return new JsonResult(from c in User.Claims select new { c.Type, c.Value }); 11 } 12 } 13 }
接着来到Startup中来注册服务和配置中间件,在这之前我们需要通过Nuget添加一个“Microsoft.AspNetCore.Authentication.JwtBearer”的引用,Jwt就是JSON Web Token的缩写,是一种流行的跨域认证解决方案。而Bearer是OAuth中的一种认证类型。
1 public class Startup 2 { 3 public void ConfigureServices(IServiceCollection services) 4 { 5 services.AddControllers(); 6 services.AddAuthentication("Bearer") 7 .AddJwtBearer("Bearer", options => 8 { 9 options.Authority = "http://localhost:5000"; //这里指定授权服务器的地址 10 options.RequireHttpsMetadata = false; //暂时先不用https 11 options.Audience = "api1"; //关联到授权服务器上的api资源,住进“api1”这个门牌号里 12 }); 13 } 14 15 public void Configure(IApplicationBuilder app) 16 { 17 app.UseRouting(); 18 app.UseAuthentication(); //添加身份认证的中间件 19 app.UseAuthorization(); //添加授权中间件,是我们api的终结点被访问时需要经过授权,从而拒绝匿名访问 20 app.UseEndpoints(endpoints => 21 { 22 endpoints.MapControllers() 23 .RequireAuthorization(); 24 //在这里配置RequireAuthorization()的话整个网站应用都会要进行授权验证, 25 //我们也可通过在Controller上应用[Authorize]特性来达到同样的效果 26 }); 27 } 28 29 }
创建客户端来访问受保护的API
新建一个Console控制台应用程序,并通过Nuget添加“IdentityModel”引用。随后我们便可以像下面一样进行实验。
1 static async Task Main(string[] args) 2 { 3 //**********【Step1】 获取Identity Server4的终结点信息************** 4 var client = new HttpClient(); 5 var disco = await client.GetDiscoveryDocumentAsync(); //调用这个方法获取Identity Server4的信息 6 if (disco.IsError) 7 { 8 Console.WriteLine(disco.Error); 9 } 10 11 //**********【Step2】 请求Access Token********** 12 var tokenReponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest() 13 { 14 Address = disco.TokenEndpoint, 15 ClientId = "client1", //类比成这个客户端应用的账户 16 ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A", //类比成这个客户端应用的密码 17 Scope = "api1" //指定授权范围 18 }); 19 if (tokenReponse.IsError) 20 { 21 Console.WriteLine(tokenReponse.Error); 22 } 23 24 //**********【Step3】 如果上面正常运行下来,就获得了Access Token了,那么接下来要使用Access Token来访问我们受保护的资源 25 var apiClient = new HttpClient(); 26 apiClient.SetBearerToken(tokenReponse.AccessToken); //将获得的Access Token放到Header中再去请求资源 27 var response = await client.GetAsync("http://localhost:5001/identity"); //访问受保护资源 28 if (!response.IsSuccessStatusCode) //如果访问失败 29 { 30 Console.WriteLine(response.StatusCode); 31 } 32 else //访问成功,将结果输出到控制台 33 { 34 var content = await response.Content.ReadAsStringAsync(); 35 Console.WriteLine(JArray.Parse(content)); 36 } 37 Console.ReadLine(); 38 }
启动Api程序和Console程序,运行起来后我们可以在控制台上看到从Api上获取到的信息,你也可以直接在浏览器中去直接请求Api的identity资源,应该会得到一个401的错误消息代表用户没有权限。
接着我们用抓包工具来验证一下,我们抓取在访问资源时的请求,在请求的Header中看到确实存在Access Token。
现在我们再回过头来整理一遍Client Credentials的认证流程,我绘制了下面的一个简图
参考资料:
官文:https://identityserver4.readthedocs.io/en/latest/quickstarts/1_client_credentials.html
杨旭大佬:https://www.cnblogs.com/cgzl/p/9221488.html
大佬的视频教程:https://www.bilibili.com/video/av42364337