第三十六节:gRPC身份认证和授权(JWT模式 和 集成IDS4)
一. 再谈认证和授权
(详见:https://www.cnblogs.com/yaopengfei/p/13141548.html)
1.认证
是验证身份的一种机制,比如用户名和密码登录,这就是一种认证机制,再比如现在比较流行jwt校验,通过用户名和密码访问接口,验证通过获取token的过程,也叫做认证。
2.授权
是确定你是否有权限访问系统的某些资源. 比如用户登录成功进入系统以后,要进行增加博客这个功能,系统要判断一下该用户是否有权限访问这个增加博客的功能,这个过程就叫做授权。再比如某个客户端app携带token访问服务端某个api接口,这个时候服务端要校验一下该token是否有权限访问这个api接口,这个过程也是授权。
3.Core Mvc中认证和授权
在Core Mvc中,UseAuthentication()代表认证,UseAuthorization()代表授权, 需要注意的是这里的认证和授权 与 上述广义上的理解有点差异,在Core MVC中,UseAuthentication和UseAuthorization一般是成对出现,且UseAuthentication认证需要写在上面,且需要在对应的api接口上加[Authorize],代表该接口需要校验, 这样当该接口被请求的时候,才会走UseAuthentication中的认证逻辑。
(PS: 这里UseAuthentication + UseAuthorization 等于上面 广义上的授权)
举例:
下面的grpc的jwt校验,获取token的过程是认证,携带token请求api接口看是否能请求通过的过程是授权。
在携带token请求api接口的过程中,Core Mvc中同时开启了UseAuthentication 和 UseAuthorization,只有当接口上有[Authorize]特性,才会走UseAuthentication里的认证逻辑; 也就是说如果api接口上没有[Authorize]特性,该接口可以被随意访问,不会走UseAuthentication中的验证逻辑哦.
二. 基于JWT模式
1. 项目准备
GrpcServer1 服务端(自身集成认证和授权)
MyClient1 客户端(控制台)
2. 服务端搭建
(1).新建ticket.proto文件,声明方法GetAvailableTickets和BuyTickets,并对其添加链接引用
代码如下:
syntax = "proto3"; import "google/protobuf/empty.proto"; package ticket; // The banker service definition. service Ticketer { //获取剩余票数( 请求参数为空) rpc GetAvailableTickets (google.protobuf.Empty) returns (AvailableTicketsResponse); //买票 rpc BuyTickets (BuyTicketsRequest) returns (BuyTicketsResponse); } message AvailableTicketsResponse { int32 count = 1; } message BuyTicketsRequest { int32 count = 1; } message BuyTicketsResponse { bool success = 1; }
(2).新建TicketerService,重写GetAvailableTickets和BuyTickets方法,并对BuyTickets添加授权校验 [Authorize]
代码如下:
public class TicketerService : Ticketer.TicketerBase { private readonly ILogger _logger; private int _availableTickets = 5; public TicketerService(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger<TicketerService>(); } /// <summary> /// 获取剩余票数 /// </summary> /// <param name="request"></param> /// <param name="context"></param> /// <returns></returns> public override Task<AvailableTicketsResponse> GetAvailableTickets(Empty request, ServerCallContext context) { return Task.FromResult(new AvailableTicketsResponse { Count = _availableTickets }); ; } /// <summary> /// 买票 /// </summary> /// <param name="request"></param> /// <param name="context"></param> /// <returns></returns> [Authorize] public override Task<BuyTicketsResponse> BuyTickets(BuyTicketsRequest request, ServerCallContext context) { var user = context.GetHttpContext().User; var updatedCount = _availableTickets - request.Count; if (updatedCount < 0) { _logger.LogError($"{user} failed to purchase tickets. Not enough available tickets."); return Task.FromResult(new BuyTicketsResponse { Success = false }); } _availableTickets = updatedCount; _logger.LogInformation($"{user} successfully purchased tickets."); return Task.FromResult(new BuyTicketsResponse { Success = true }); } }
(3).通过nuget安装程序集【Microsoft.AspNetCore.Authentication.JwtBearer 3.1.6】,在ConfigureService注册认证和授权中间件,在Configure开启认证和授权中间件,并映射TicketerService服务。
代码如下:
public void ConfigureServices(IServiceCollection services) { services.AddGrpc(); //认证 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => { string key = Configuration["Authentication:SymmetricSecurityKey"]; options.TokenValidationParameters = new TokenValidationParameters { IssuerSigningKey = new SymmetricSecurityKey(Guid.Parse(key).ToByteArray()), ValidateAudience = false, ValidateIssuer = false, ValidateActor = false, ValidateLifetime = true, }; }); //授权 services.AddAuthorization(options => { options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy => { policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); policy.RequireClaim(ClaimTypes.Name); }); }); services.AddControllers(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); //认证 app.UseAuthentication(); //授权 app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<TicketerService>(); endpoints.MapControllers(); endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); }); }); }
(4).新增一个名为GetToken的方法,用于获取token
代码如下:
[Route("api/[controller]/[action]")] [ApiController] public class TokenController : ControllerBase { private readonly IConfiguration _configuration; public TokenController(IConfiguration configuration) { _configuration = configuration; } /// <summary> /// 获取Token /// </summary> /// <param name="clientId"></param> /// <param name="clientSecret"></param> /// <returns></returns> [HttpGet] public string GetToken([FromHeader]string clientId, [FromHeader]string clientSecret) { if (clientId == "ypf" && clientSecret == "123456") { string key = _configuration.GetValue<string>("Authentication:SymmetricSecurityKey"); var securityKey = new SymmetricSecurityKey(Guid.Parse(key).ToByteArray()); var claims = new[] { new Claim(ClaimTypes.Name, clientId), new Claim(ClaimTypes.NameIdentifier,clientId) }; var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken("TicketServer", "TicketClient", claims, expires: DateTime.Now.AddSeconds(60), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token); } else { return "非法请求,不能获取token"; } } }
PS: 上述grpc中的方法,只有BuyTickets加了[Authorize],再请求它的时候要走UseAuthentication里的认证逻辑, 其它方法没有加 [Authorize],则不进行验证,直接可以请求。
3. 客户端搭建
(1).对cert.proto文件添加服务链接引用,会自动安装相应的程序集(版本可能不是最新的,需要手动更新一下)
(2).编写代码:请求GetAvailableTickets获取票数 → 请求GetToken获取token →携带token请求BuyTickets
代码如下:
class Program { private const string address = "https://localhost:5001"; static async Task Main(string[] args) { await Task.Delay(TimeSpan.FromSeconds(1)); var grpcChannel = GrpcChannel.ForAddress(address); TicketerClient grpcClient = new TicketerClient(grpcChannel); try { Console.WriteLine("------------------------------下面开始获取票的数量--------------------------------------"); var availableResponse = await grpcClient.GetAvailableTicketsAsync(new Empty()); Console.WriteLine($"可用票数为:{availableResponse.Count}"); Console.WriteLine("------------------------------下面开始获取token--------------------------------------"); string token = await GetToken(); Console.WriteLine($"请求成功,token={token}"); Console.WriteLine("------------------------------下面携带token请求授权接口--------------------------------------"); Metadata headers = null; if (token != null) { headers = new Metadata(); headers.Add("Authorization", $"Bearer {token}"); } var buyTicketResponse = await grpcClient.BuyTicketsAsync(new BuyTicketsRequest { Count = 1 }, headers); if (buyTicketResponse.Success) { Console.WriteLine("Purchase successful."); } else { Console.WriteLine("Purchase failed. No tickets available."); } } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadKey(); } static async Task<string> GetToken() { HttpClient httpClient = new HttpClient(); var request = new HttpRequestMessage { RequestUri = new Uri($"{address}/api/Token/GetToken"), Method = HttpMethod.Get, Version = new Version(2, 0) //http2 }; request.Headers.Add("clientId", "ypf"); request.Headers.Add("clientSecret", "123456"); var tokenResponse = await httpClient.SendAsync(request); tokenResponse.EnsureSuccessStatusCode(); var token = await tokenResponse.Content.ReadAsStringAsync(); return token; } }
4. 测试
将GrpcServer1和MyClient1配置同时启动,查看结果。
三. 基于IDS4模式
1. 前情回顾
上一节我们手写了基于jwt的认证和授权,且grpc服务与认证授权放在一个项目上,有点冗杂. 本节我们引用成熟的认证授权框架IdentityServer4框架,并将grpc服务和认证授权分开,各司其职。
IdentityServer是基于OpenID Connect协议标准的身份认证和授权程序,它实现了OpenID 和 OAuth 2.0 协议。详见微服务章节:https://www.cnblogs.com/yaopengfei/p/12885217.html
IDS4有多种模式,本节采用的是客户端模式,即:GrantTypes.ClientCredentials
2.项目准备
IDS4Sever:认证和授权服务器 (7001端口)
GrpcServer2:gprc服务 (7002端口 https)
MyClient2: 客户端(控制台)
3. IDS4服务搭建
(1).通过Nuget给IDS4Sever安装【IdentityServer4 4.0.2】
(2).新建Config1配置类,包括方法:GetApiScopes、GetApiResources GetClients. 其中GetApiResources里包含需要保护的Api业务服务器名称,GetClients里包含了哪些客户端资源可以访问,其中可以通过AllowedScopes = { "GrpcServer2"} 来授权哪个客户端能访问哪些api资源,例外还要配置 ClientId、校验方式(GrantTypes.ClientCredentials)、密钥。
代码如下:
public class Config1 { /// <summary> /// 配置Api范围集合 /// 4.x版本新增的配置 /// </summary> /// <returns></returns> public static IEnumerable<ApiScope> GetApiScopes() { return new List<ApiScope> { new ApiScope("GrpcServer2") }; } /// <summary> /// 需要保护的Api资源 /// 4.x版本新增后续Scopes的配置 /// </summary> /// <returns></returns> public static IEnumerable<ApiResource> GetApiResources() { List<ApiResource> resources = new List<ApiResource>(); //ApiResource第一个参数是ServiceName,第二个参数是描述 resources.Add(new ApiResource("GrpcServer2", "GrpcServer2服务需要保护哦") { Scopes = { "GrpcServer2" } }); return resources; } /// <summary> /// 可以使用ID4 Server 客户端资源 /// </summary> /// <returns></returns> public static IEnumerable<Client> GetClients() { List<Client> clients = new List<Client>() { new Client { ClientId = "client1",//客户端ID AllowedGrantTypes = GrantTypes.ClientCredentials, //验证类型:客户端验证 ClientSecrets ={ new Secret("0001".Sha256())}, //密钥和加密方式 AllowedScopes = { "GrpcServer2" }, //允许访问的api服务 ClientClaimsPrefix="", //把前缀设置成空,就IDS4和Core MVC之间就不用转换了 }, new Client { ClientId = "client2",//客户端ID AllowedGrantTypes = GrantTypes.ClientCredentials, //验证类型:客户端验证 ClientSecrets ={ new Secret("0002".Sha256())}, //密钥和加密方式 AllowedScopes = { "GrpcServer2" }, //允许访问的api服务 //基于角色授权 Claims= { new ClientClaim("role","ypfRole"), new ClientClaim("group","mygroup") }, ClientClaimsPrefix="", //把前缀设置成空,就IDS4和Core MVC之间就不用转换了 } }; return clients; } }
(3).Startup中的ConfigureService和Config的配置。
代码如下:
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { //1. 客户端模式 services.AddIdentityServer() .AddDeveloperSigningCredential() //生成Token签名需要的公钥和私钥,存储在bin下tempkey.rsa(生产场景要用真实证书,此处改为AddSigningCredential) .AddInMemoryApiResources(Config1.GetApiResources()) //存储需要保护api资源 .AddInMemoryApiScopes(Config1.GetApiScopes()) //配置api范围 4.x版本必须配置的 .AddInMemoryClients(Config1.GetClients()); //存储客户端模式(即哪些客户端可以用) services.AddControllers(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseAuthorization(); //1.启用IdentityServe4 app.UseIdentityServer(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
(4).通过 属性→调试,将端口改为7001。
4. grpc服务搭建
(1).新建ticket.proto文件,声明方法GetAvailableTickets和BuyTickets,并对其添加链接引用。
代码如下:
syntax = "proto3"; import "google/protobuf/empty.proto"; package ticket; // The banker service definition. service Ticketer { //获取剩余票数( 请求参数为空) rpc GetAvailableTickets (google.protobuf.Empty) returns (AvailableTicketsResponse); //买票 rpc BuyTickets (BuyTicketsRequest) returns (BuyTicketsResponse); } message AvailableTicketsResponse { int32 count = 1; } message BuyTicketsRequest { int32 count = 1; } message BuyTicketsResponse { bool success = 1; }
(2).新建TicketerService,重写GetAvailableTickets和BuyTickets方法,并对BuyTickets添加授权校验 [Authorize]。
代码如下:
public class TicketerService : Ticketer.TicketerBase { private readonly ILogger _logger; private int _availableTickets = 5; public TicketerService(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger<TicketerService>(); } public override Task<AvailableTicketsResponse> GetAvailableTickets(Empty request, ServerCallContext context) { return Task.FromResult(new AvailableTicketsResponse { Count = _availableTickets }); ; } //[Authorize] [Authorize(Roles = "ypfRole")] //[Authorize(Policy = "group")] public override Task<BuyTicketsResponse> BuyTickets(BuyTicketsRequest request, ServerCallContext context) { var user = context.GetHttpContext().User; var updatedCount = _availableTickets - request.Count; if (updatedCount < 0) { _logger.LogError($"{user} failed to purchase tickets. Not enough available tickets."); return Task.FromResult(new BuyTicketsResponse { Success = false }); } _availableTickets = updatedCount; _logger.LogInformation($"{user} successfully purchased tickets."); return Task.FromResult(new BuyTicketsResponse { Success = true }); } }
(3).通过nuget安装程序集【IdentityServer4.AccessTokenValidation 3.0.1】,在ConfigureService注册认证和授权中间件,其中认证组件链接远程IDS4Sever的地址,在Configure开启认证和授权中间件,并映射TicketerService服务。
代码如下:
public void ConfigureServices(IServiceCollection services) { //校验AccessToken,从身份校验中心(IDS4Server)进行校验 services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) //Bear模式 .AddIdentityServerAuthentication(options => { options.Authority = "http://127.0.0.1:7001"; // 1、授权中心地址 options.ApiName = "GrpcServer2"; // 2、api名称(项目具体名称) options.RequireHttpsMetadata = false; // 3、https元数据,不需要 //进行转换 //options.NameClaimType = "client_id"; //options.RoleClaimType = "client_role"; }); services.AddAuthorization(options => { options.AddPolicy("group", config => config.RequireClaim("client_group", "mygroup")); }); services.AddGrpc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); //认证中间件(服务于上ID4校验,一定要放在UseAuthorization之前) app.UseAuthentication(); //授权中间件 app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<TicketerService>(); endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); }); }); }
(4).通过 属性→调试,将端口改为7002 (https)。
PS: 上述grpc中的方法,只有BuyTickets加了[Authorize],再请求它的时候要走UseAuthentication里的认证逻辑, 其它方法没有加 [Authorize],则不进行验证,直接可以请求。
5. 客户端搭建
(1).对cert.proto文件添加服务链接引用,会自动安装相应的程序集(版本可能不是最新的,需要手动更新一下)
(2).编写代码:请求GetAvailableTickets获取票数 → 请求GetToken获取token →携带token请求BuyTickets。
代码如下:
class Program { static async Task Main(string[] args) { var grpcChannel = GrpcChannel.ForAddress("https://localhost:7002"); TicketerClient grpcClient = new TicketerClient(grpcChannel); Console.WriteLine("正在获取剩余票数:..."); var availableResponse = await grpcClient.GetAvailableTicketsAsync(new Empty()); Console.WriteLine($"剩余的票数为:{availableResponse.Count}"); var client = new HttpClient(); var disco = await client.GetDiscoveryDocumentAsync("http://127.0.0.1:7001"); if (disco.IsError) { Console.WriteLine(disco.Error); return; } //向认证服务器发送请求,要求获得令牌 Console.WriteLine("---------------------------- 一.向认证服务器发送请求,要求获得令牌-----------------------------------"); var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { //在上面的地址上拼接:/connect/token,最终:http://127.0.0.1:7001/connect/token Address = disco.TokenEndpoint, ClientId = "client2", ClientSecret = "0002", }); if (tokenResponse.IsError) { Console.WriteLine($"认证错误:{tokenResponse.Error}"); Console.ReadKey(); } Console.WriteLine(tokenResponse.Json); //携带token向资源服务器发送请求 Console.WriteLine("----------------------------二.携带token向资源服务器发送请求-----------------------------------"); Metadata headers = null; if (tokenResponse.AccessToken != null) { headers = new Metadata(); headers.Add("Authorization", $"Bearer {tokenResponse.AccessToken}"); } try { var buyTicketResponse = await grpcClient.BuyTicketsAsync(new BuyTicketsRequest { Count = 1 }, headers); if (buyTicketResponse.Success) { Console.WriteLine("购买成功."); } else { Console.WriteLine("购买失败. No tickets available."); } } catch (Exception ex) { Console.WriteLine($"购买失败. {ex.Message}"); } Console.ReadKey(); } }
6. 测试
将IDS4Sever、GrpcServer2、MyClient2按照这个顺序配置同时启动, 分别测试client1和client2获取token后的请求情况,包括角色授权。
运行结果如下:
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。