Tip: 此篇已加入.NET Core微服務基礎系列文章索引html
這裏,假設咱們有兩個客戶端(一個Web網站,一個移動App),他們要使用系統,須要經過API網關(這裏API網關始終做爲客戶端的統一入口)先向IdentityService進行Login以進行驗證並獲取Token,在IdentityService的驗證過程當中會訪問數據庫以驗證。而後再帶上Token經過API網關去訪問具體的API Service。這裏咱們的IdentityService基於IdentityServer4開發,它具備統一登陸驗證和受權的功能。git
這裏主要基於前兩篇已經搭好的API Gateway進行改寫,如不熟悉,能夠先瀏覽前兩篇文章:Part 1和Part 2。github
...... "AuthenticationOptions": { "AuthenticationProviderKey": "ClientServiceKey", "AllowedScopes": [] } ...... "AuthenticationOptions": { "AuthenticationProviderKey": "ProductServiceKey", "AllowedScopes": [] } ......
上面分別爲兩個示例API Service增長Authentication的選項,爲其設置ProviderKey。下面會對不一樣的路由規則設置的ProviderKey設置具體的驗證方式。web
public void ConfigureServices(IServiceCollection services) { // IdentityServer #region IdentityServerAuthenticationOptions => need to refactor Action<IdentityServerAuthenticationOptions> isaOptClient = option => { option.Authority = Configuration["IdentityService:Uri"]; option.ApiName = "clientservice"; option.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]); option.SupportedTokens = SupportedTokens.Both; option.ApiSecret = Configuration["IdentityService:ApiSecrets:clientservice"]; }; Action<IdentityServerAuthenticationOptions> isaOptProduct = option => { option.Authority = Configuration["IdentityService:Uri"]; option.ApiName = "productservice"; option.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]); option.SupportedTokens = SupportedTokens.Both; option.ApiSecret = Configuration["IdentityService:ApiSecrets:productservice"]; }; #endregion services.AddAuthentication() .AddIdentityServerAuthentication("ClientServiceKey", isaOptClient) .AddIdentityServerAuthentication("ProductServiceKey", isaOptProduct); // Ocelot services.AddOcelot(Configuration); ...... }
這裏的ApiName主要對應於IdentityService中的ApiResource中定義的ApiName。這裏用到的配置文件定義以下:數據庫
"IdentityService": { "Uri": "http://localhost:5100", "UseHttps": false, "ApiSecrets": { "clientservice": "clientsecret", "productservice": "productsecret" } }
這裏的定義方式,我暫時還沒想好怎麼重構,不過確定是須要重構的,否則這樣一個一個寫比較繁瑣,且不利於配置。json
這裏咱們會基於以前基於IdentityServer的兩篇文章,新增一個IdentityService,不熟悉的朋友能夠先瀏覽一下Part 1和Part 2。api
新建一個ASP.NET Core Web API項目,綁定端口5100,NuGet安裝IdentityServer4。配置好證書,並設置其爲「較新則複製」,以便可以在生成目錄中讀取到。服務器
/// <summary> /// One In-Memory Configuration for IdentityServer => Just for Demo Use /// </summary> public class InMemoryConfiguration { public static IConfiguration Configuration { get; set; } /// <summary> /// Define which APIs will use this IdentityServer /// </summary> /// <returns></returns> public static IEnumerable<ApiResource> GetApiResources() { return new[] { new ApiResource("clientservice", "CAS Client Service"), new ApiResource("productservice", "CAS Product Service"), new ApiResource("agentservice", "CAS Agent Service") }; } /// <summary> /// Define which Apps will use thie IdentityServer /// </summary> /// <returns></returns> public static IEnumerable<Client> GetClients() { return new[] { new Client { ClientId = "cas.sg.web.nb", ClientName = "CAS NB System MPA Client", ClientSecrets = new [] { new Secret("websecret".Sha256()) }, AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, AllowedScopes = new [] { "clientservice", "productservice", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } }, new Client { ClientId = "cas.sg.mobile.nb", ClientName = "CAS NB System Mobile App Client", ClientSecrets = new [] { new Secret("mobilesecret".Sha256()) }, AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, AllowedScopes = new [] { "productservice", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } }, new Client { ClientId = "cas.sg.spa.nb", ClientName = "CAS NB System SPA Client", ClientSecrets = new [] { new Secret("spasecret".Sha256()) }, AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, AllowedScopes = new [] { "agentservice", "clientservice", "productservice", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } }, new Client { ClientId = "cas.sg.mvc.nb.implicit", ClientName = "CAS NB System MVC App Client", AllowedGrantTypes = GrantTypes.Implicit, RedirectUris = { Configuration["Clients:MvcClient:RedirectUri"] }, PostLogoutRedirectUris = { Configuration["Clients:MvcClient:PostLogoutRedirectUri"] }, AllowedScopes = new [] { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "agentservice", "clientservice", "productservice" }, //AccessTokenLifetime = 3600, // one hour AllowAccessTokensViaBrowser = true // can return access_token to this client } }; } /// <summary> /// Define which IdentityResources will use this IdentityServer /// </summary> /// <returns></returns> public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), }; } }
這裏使用了上一篇的內容,再也不解釋。實際環境中,則應該考慮從NoSQL或數據庫中讀取。mvc
在IdentityServer中,要實現自定義的驗證用戶名和密碼,須要實現一個接口:IResourceOwnerPasswordValidatorapp
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { private ILoginUserService loginUserService; public ResourceOwnerPasswordValidator(ILoginUserService _loginUserService) { this.loginUserService = _loginUserService; } public Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { LoginUser loginUser = null; bool isAuthenticated = loginUserService.Authenticate(context.UserName, context.Password, out loginUser); if (!isAuthenticated) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid client credential"); } else { context.Result = new GrantValidationResult( subject : context.UserName, authenticationMethod : "custom", claims : new Claim[] { new Claim("Name", context.UserName), new Claim("Id", loginUser.Id.ToString()), new Claim("RealName", loginUser.RealName), new Claim("Email", loginUser.Email) } ); } return Task.CompletedTask; } }
這裏的ValidateAsync方法中(你也能夠把它寫成異步的方式,這裏使用的是同步的方式),會調用EF去訪問數據庫進行驗證,數據庫的定義以下(密碼應該作加密,這裏只作demo,沒用弄):
至於EF部分,則是一個典型的簡單的Service調用Repository的邏輯,下面只貼Repository部分:
public class LoginUserRepository : RepositoryBase<LoginUser, IdentityDbContext>, ILoginUserRepository { public LoginUserRepository(IdentityDbContext dbContext) : base(dbContext) { } public LoginUser Authenticate(string _userName, string _userPassword) { var entity = DbContext.LoginUsers.FirstOrDefault(p => p.UserName == _userName && p.Password == _userPassword); return entity; } }
其餘具體邏輯請參考示例代碼。
public void ConfigureServices(IServiceCollection services) { // IoC - DbContext services.AddDbContextPool<IdentityDbContext>( options => options.UseSqlServer(Configuration["DB:Dev"])); // IoC - Service & Repository services.AddScoped<ILoginUserService, LoginUserService>(); services.AddScoped<ILoginUserRepository, LoginUserRepository>(); // IdentityServer4 string basePath = PlatformServices.Default.Application.ApplicationBasePath; InMemoryConfiguration.Configuration = this.Configuration; services.AddIdentityServer() .AddSigningCredential(new X509Certificate2(Path.Combine(basePath, Configuration["Certificates:CerPath"]), Configuration["Certificates:Password"])) //.AddTestUsers(InMemoryConfiguration.GetTestUsers().ToList()) .AddInMemoryIdentityResources(InMemoryConfiguration.GetIdentityResources()) .AddInMemoryApiResources(InMemoryConfiguration.GetApiResources()) .AddInMemoryClients(InMemoryConfiguration.GetClients()) .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>() .AddProfileService<ProfileService>(); ...... }
這裏高亮的是新增的部分,爲了實現自定義驗證。關於ProfileService的定義以下:
public class ProfileService : IProfileService { public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var claims = context.Subject.Claims.ToList(); context.IssuedClaims = claims.ToList(); } public async Task IsActiveAsync(IsActiveContext context) { context.IsActive = true; } }
這裏新增一個LoginController:
[Produces("application/json")] [Route("api/Login")] public class LoginController : Controller { private IConfiguration configuration; public LoginController(IConfiguration _configuration) { configuration = _configuration; } [HttpPost] public async Task<ActionResult> RequestToken([FromBody]LoginRequestParam model) { Dictionary<string, string> dict = new Dictionary<string, string>(); dict["client_id"] = model.ClientId; dict["client_secret"] = configuration[$"IdentityClients:{model.ClientId}:ClientSecret"]; dict["grant_type"] = configuration[$"IdentityClients:{model.ClientId}:GrantType"]; dict["username"] = model.UserName; dict["password"] = model.Password; using (HttpClient http = new HttpClient()) using (var content = new FormUrlEncodedContent(dict)) { var msg = await http.PostAsync(configuration["IdentityService:TokenUri"], content); if (!msg.IsSuccessStatusCode) { return StatusCode(Convert.ToInt32(msg.StatusCode)); } string result = await msg.Content.ReadAsStringAsync(); return Content(result, "application/json"); } } }
這裏假設客戶端會傳遞用戶名,密碼以及客戶端ID(ClientId,好比上面InMemoryConfiguration中的cas.sg.web.nb或cas.sg.mobile.nb)。而後構造參數再調用connect/token接口進行身份驗證和獲取token。這裏將client_secret等機密信息封裝到了服務器端,無須客戶端傳遞(對於機密信息通常也不會讓客戶端知道):
"IdentityClients": { "cas.sg.web.nb": { "ClientSecret": "websecret", "GrantType": "password" }, "cas.sg.mobile.nb": { "ClientSecret": "mobilesecret", "GrantType": "password" } }
在API網關的Ocelot配置文件中加入配置,配置以下(這裏我是開發用,因此沒有用服務發現,實際環境建議採用服務發現):
// --> Identity Service Part { "UseServiceDiscovery": false, // do not use Consul service discovery in DEV env "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": "5100" } ], "ServiceName": "CAS.IdentityService", "LoadBalancerOptions": { "Type": "RoundRobin" }, "UpstreamPathTemplate": "/api/identityservice/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "RateLimitOptions": { "ClientWhitelist": [ "admin" ], // 白名單 "EnableRateLimiting": true, // 是否啓用限流 "Period": "1m", // 統計時間段:1s, 5m, 1h, 1d "PeriodTimespan": 15, // 多少秒以後客戶端能夠重試 "Limit": 10 // 在統計時間段內容許的最大請求數量 }, "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 2, // 容許多少個異常請求 "DurationOfBreak": 5000, // 熔斷的時間,單位爲秒 "TimeoutValue": 3000 // 若是下游請求的處理時間超過多少則視如該請求超時 }, "HttpHandlerOptions": { "UseTracing": false // use butterfly to tracing request chain }, "ReRoutesCaseSensitive": false // non case sensitive }
(1)安裝IdentityServer4.AccessTokenValidation
NuGet>Install-Package IdentityServer4.AccessTokenValidation
(2)改寫StartUp類
public IServiceProvider ConfigureServices(IServiceCollection services) { ...... // IdentityServer services.AddAuthentication(Configuration["IdentityService:DefaultScheme"]) .AddIdentityServerAuthentication(options => { options.Authority = Configuration["IdentityService:Uri"]; options.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]); }); ...... }
這裏配置文件的定義以下:
"IdentityService": { "Uri": "http://localhost:5100", "DefaultScheme": "Bearer", "UseHttps": false, "ApiSecret": "clientsecret" }
與ClientService一致,請參考示例代碼。
(1)統一驗證&獲取token (by API網關)
(2)訪問clientservice (by API網關)
(3)訪問productservice(by API網關)
因爲在IdentityService中咱們定義了一個mobile的客戶端,可是其訪問權限只有productservice,因此咱們來測試一下:
(1)統一驗證&獲取token
(2)訪問ProductService(by API網關)
(3)訪問ClientService(by API網關) => 401 Unauthorized
本篇主要基於前面Ocelot和IdentityServer的文章的基礎之上,將Ocelot和IdentityServer進行結合,經過創建IdentityService進行統一的身份驗證和受權,最後演示了一個案例以說明如何實現。不過,本篇實現的Demo還存在諸多不足,好比須要重構的代碼較多如網關中各個Api的驗證選項的註冊,沒有對各個請求作用戶角色和權限的驗證等等,相信隨着研究和深刻的深刻,這些均可以逐步解決。後續會探索一下數據一致性的基本知識以及框架使用,到時再作一些分享。
Click Here => 點我進入GitHub
楊中科,《.NET Core微服務介紹課程》
原文出處:https://www.cnblogs.com/edisonchou/p/integration_authentication-authorization_service_foundation.html