第三十六節: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 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM