當前位置:
首頁 > 最新 > NET Core微服務之基於Ocelot+IdentityServer實現統一驗證與授權

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。


......"AuthenticationOptions": {"AuthenticationProviderKey":"ClientServiceKey","AllowedScopes": [] } ......"AuthenticationOptions": {"AuthenticationProviderKey":"ProductServiceKey","AllowedScopes": [] } ......

上面分別為兩個示例API Service增加Authentication的選項,為其設置ProviderKey。下面會對不同的路由規則設置的ProviderKey設置具體的驗證方式。


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。


//////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或資料庫中讀取。


在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

其他具體邏輯請參考示例代碼。


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


這裡新增一個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"}


與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微服務介紹課程》


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 edisonchou 的精彩文章:

TAG:edisonchou |