• 第三十六节: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;
    }
    View Code

     (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 });
            }
        }
    View Code

     (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");
                    });
                });
            }
    View Code

     (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";
                }
            }
        }
    View Code

    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;
            }
        }
    View Code

    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;
            }
    
    
        }
    View Code

     (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();
                });
            }
        }
    View Code

     (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;
    }
    View Code

     (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 });
            }
        }
    View Code

     (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");
                    });
                });
            }
    View Code

     (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();
            }
        }
    View Code

    6. 测试

     将IDS4Sever、GrpcServer2、MyClient2按照这个顺序配置同时启动, 分别测试client1和client2获取token后的请求情况,包括角色授权。

     运行结果如下:

     

    !

    • 作       者 : Yaopengfei(姚鹏飞)
    • 博客地址 : http://www.cnblogs.com/yaopengfei/
    • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
    • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
     
  • 相关阅读:
    JS进阶篇2---函数防抖(debounce)
    vue 的"响应式"是什么意思/ Object.freeze( ) 阻止数据响应
    try{...}catch(){...}语句的使用
    总结一下ES6的promise
    《ES6标准入门》(六)之Promise对象2——then()和catch()方法
    大白话讲解Promise(一)
    解决VSCode单击文件会替换已经打开文件的问题
    通俗理解“回调函数”
    vue中的时间格式处理
    vue之项目踩坑笔记
  • 原文地址:https://www.cnblogs.com/yaopengfei/p/13403001.html
Copyright © 2020-2023  润新知