NET Core微服務之基於Ocelot+IdentityServer實現統一驗證與授權
一、案例結構總覽
這裡,假設我們有兩個客戶端(一個Web網站,一個移動App),他們要使用系統,需要先向IdentityService進行Login以進行驗證並獲取Token,在IdentityService的驗證過程中會訪問資料庫以驗證。然後再帶上Token通過API網關去訪問具體的API Service。這裡我們的IdentityService基於IdentityServer4開發,它具有統一登錄驗證和授權的功能。當然,我們也可以將統一登錄驗證獨立出來,寫成一個單獨的API Service,託管在API網關中,這裡我不想太麻煩,便直接將其也寫在了IdentityService中。
二、改寫API Gateway
這裡主要基於前兩篇已經搭好的API Gateway進行改寫,如不熟悉,可以先瀏覽前兩篇文章:Part 1和Part 2。
2.1 配置文件的改動
......"AuthenticationOptions": {"AuthenticationProviderKey":"ClientServiceKey","AllowedScopes": [] } ......"AuthenticationOptions": {"AuthenticationProviderKey":"ProductServiceKey","AllowedScopes": [] } ......
上面分別為兩個示例API Service增加Authentication的選項,為其設置ProviderKey。下面會對不同的路由規則設置的ProviderKey設置具體的驗證方式。
2.2 改寫StartUp類
publicvoidConfigureServices(IServiceCollection services) {//IdentityServer#regionIdentityServerAuthenticationOptions => need to refactorAction 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 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"]; };#endregionservices.AddAuthentication() .AddIdentityServerAuthentication("ClientServiceKey", isaOptClient) .AddIdentityServerAuthentication("ProductServiceKey", isaOptProduct);//Ocelotservices.AddOcelot(Configuration); ...... }
這裡的ApiName主要對應於IdentityService中的ApiResource中定義的ApiName。這裡用到的配置文件定義如下:
View Code
這裡的定義方式,我暫時還沒想好怎麼重構,不過肯定是需要重構的,不然這樣一個一個寫比較繁瑣,且不利於配置。
三、新增IdentityService
這裡我們會基於之前基於IdentityServer的兩篇文章,新增一個IdentityService,不熟悉的朋友可以先瀏覽一下Part 1和Part 2。
3.1 準備工作
3.2 定義一個InMemoryConfiguration用於測試
//////One In-Memory Configuration for IdentityServer => Just for Demo Use///publicclassInMemoryConfiguration {publicstaticIConfiguration Configuration {get;set; }//////Define which APIs will use this IdentityServer//////publicstaticIEnumerableGetApiResources() {returnnew[] {newApiResource("clientservice","CAS Client Service"),newApiResource("productservice","CAS Product Service"),newApiResource("agentservice","CAS Agent Service") }; }//////Define which Apps will use thie IdentityServer//////publicstaticIEnumerableGetClients() {returnnew[] {newClient { ClientId="cas.sg.web.nb", ClientName="CAS NB System MPA Client", ClientSecrets=new[] {newSecret("websecret".Sha256()) }, AllowedGrantTypes=GrantTypes.ResourceOwnerPassword, AllowedScopes=new[] {"clientservice","productservice", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } },newClient { ClientId="cas.sg.mobile.nb", ClientName="CAS NB System Mobile App Client", ClientSecrets=new[] {newSecret("mobilesecret".Sha256()) }, AllowedGrantTypes=GrantTypes.ResourceOwnerPassword, AllowedScopes=new[] {"productservice", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } },newClient { ClientId="cas.sg.spa.nb", ClientName="CAS NB System SPA Client", ClientSecrets=new[] {newSecret("spasecret".Sha256()) }, AllowedGrantTypes=GrantTypes.ResourceOwnerPassword, AllowedScopes=new[] {"agentservice","clientservice","productservice", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } },newClient { 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 hourAllowAccessTokensViaBrowser =true//can return access_token to this client} }; }//////Define which IdentityResources will use this IdentityServer//////publicstaticIEnumerableGetIdentityResources() {returnnewList{newIdentityResources.OpenId(),newIdentityResources.Profile(), }; } }
這裡使用了上一篇的內容,不再解釋。實際環境中,則應該考慮從NoSQL或資料庫中讀取。
3.3 定義一個ResourceOwnerPasswordValidator
在IdentityServer中,要實現自定義的驗證用戶名和密碼,需要實現一個介面:IResourceOwnerPasswordValidator
publicclassResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator {privateILoginUserService loginUserService;publicResourceOwnerPasswordValidator(ILoginUserService _loginUserService) {this.loginUserService =_loginUserService; }publicTask ValidateAsync(ResourceOwnerPasswordValidationContext context) { LoginUser loginUser=null;boolisAuthenticated = loginUserService.Authenticate(context.UserName, context.Password,outloginUser);if(!isAuthenticated) { context.Result=newGrantValidationResult(TokenRequestErrors.InvalidGrant,"Invalid client credential"); }else{ context.Result=newGrantValidationResult( subject : context.UserName, authenticationMethod :"custom", claims :newClaim[] {newClaim("Name", context.UserName),newClaim("Id", loginUser.Id.ToString()),newClaim("RealName", loginUser.RealName),newClaim("Email", loginUser.Email) } ); }returnTask.CompletedTask; } }
這裡的ValidateAsync方法中(你也可以把它寫成非同步的方式,這裡使用的是同步的方式),會調用EF去訪問資料庫進行驗證,資料庫的定義如下(密碼應該做加密,這裡只做demo,沒用弄):
至於EF部分,則是一個典型的簡單的Service調用Repository的邏輯,下面只貼Repository部分:
View Code
其他具體邏輯請參考示例代碼。
3.4 改寫StarUp類
publicvoidConfigureServices(IServiceCollection services) {//IoC - DbContextservices.AddDbContextPool( options=> options.UseSqlServer(Configuration["DB:Dev"]));//IoC - Service & Repositoryservices.AddScoped(); services.AddScoped();//IdentityServer4stringbasePath =PlatformServices.Default.Application.ApplicationBasePath; InMemoryConfiguration.Configuration=this.Configuration; services.AddIdentityServer() .AddSigningCredential(newX509Certificate2(Path.Combine(basePath, Configuration["Certificates:CerPath"]), Configuration["Certificates:Password"]))//.AddTestUsers(InMemoryConfiguration.GetTestUsers().ToList()).AddInMemoryIdentityResources(InMemoryConfiguration.GetIdentityResources()) .AddInMemoryApiResources(InMemoryConfiguration.GetApiResources()) .AddInMemoryClients(InMemoryConfiguration.GetClients()).AddResourceOwnerValidator() .AddProfileService
();...... }
這裡高亮的是新增的部分,為了實現自定義驗證。關於ProfileService的定義如下:
View Code
3.5 新增統一Login入口
這裡新增一個LoginController:
[Produces("application/json")] [Route("api/Login")]publicclassLoginController : Controller {privateIConfiguration configuration;publicLoginController(IConfiguration _configuration) { configuration=_configuration; } [HttpPost]publicasyncTaskRequestToken([FromBody]LoginRequestParam model) { Dictionary dict =newDictionary(); dict["client_id"] =model.ClientId; dict["client_secret"] = configuration[$"IdentityClients::ClientSecret"]; dict["grant_type"] = configuration[$"IdentityClients::GrantType"]; dict["username"] =model.UserName; dict["password"] =model.Password;using(HttpClient http =newHttpClient())using(varcontent =newFormUrlEncodedContent(dict)) {varmsg =awaithttp.PostAsync(configuration["IdentityService:TokenUri"], content);if(!msg.IsSuccessStatusCode) {returnStatusCode(Convert.ToInt32(msg.StatusCode)); }stringresult =awaitmsg.Content.ReadAsStringAsync();returnContent(result,"application/json"); } } }
四、改寫業務API Service
4.1 ClientService
NuGet>Install-Package IdentityServer4.AccessTokenValidation
(2)改寫StartUp類
publicIServiceProvider ConfigureServices(IServiceCollection services) { ......//IdentityServerservices.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"}
4.2 ProductService
與ClientService一致,請參考示例代碼。
五、測試
(1)統一驗證&獲取token
(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微服務介紹課程》
TAG:edisonchou |