abp集成IdentityServer4和單點登錄
- 2020 年 2 月 13 日
- 筆記
在abp開發的系統後,需要使用這個系統作單點登錄,及其他項目登錄賬號依靠abp開發的系統。在官方文檔上只找到作為登錄服務Identity Server Integration,但是host項目卻無法使用登錄服務生成的Token獲取數據。所有的搜索結果包括abp的issue都是說去看identity server4的文檔。我比較笨,文檔看了還是不會。好在最後還是試出來了。
創建登錄中心項目
- 到官網下載一個最新的模板項目,項目類型自選(我們項目用的vue,所以我選擇的vue項目,.net core3.x)。保證可以運行起來並正常登錄。
- 右鍵src目錄添加一個asp.net core web 空項目,在項目中添加Startup文件夾,把Startup.cs和Program.cs移動到Startup文件夾,並修改這兩個文件的命名空間增加Startup。不然會有命名空間和類名衝突。
- 在nuget添加Abp.ZeroCore.IdentityServer4、Abp、Abp.Castle.Log4Net等引用,添加Web.Core、EntityFrameworkCore項目引用
- 在Startup文件加新增xxxModule文件,初始化登錄中心項目,因為這個項目要用到abp的模塊所以要添加module
using Abp.Ids4; using Abp.Ids4.Configuration; using Abp.Modules; using Abp.Reflection.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; namespace Abp.Ids4.Server.Startup { [DependsOn( typeof(Ids4WebCoreModule))] public class AbpIds4ServerModule: AbpModule { private readonly IWebHostEnvironment _env; private readonly IConfigurationRoot _appConfiguration; public AbpIds4ServerModule(IWebHostEnvironment env) { _env = env; _appConfiguration = env.GetAppConfiguration(); } public override void Initialize() { IocManager.RegisterAssemblyByConvention(typeof(AbpIds4ServerModule).GetAssembly()); } } }
- 在Startup文件加新增AuthConfigurer.cs文件,你也可以直接從IdentityServerDemo項目複製文件過來,但是記得修改命名空間
using System; using System.Linq; using System.Text; using System.Threading.Tasks; using Abp.Authorization; using Abp.Ids4; using Abp.Runtime.Security; using IdentityServer4.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; namespace Abp.Ids4.Server.Startup { public static class AuthConfigurer { /// <summary> /// Configures the specified application. /// </summary> /// <param name="app">The application.</param> /// <param name="configuration">The configuration.</param> public static void Configure(IServiceCollection services, IConfiguration configuration) { var authenticationBuilder = services.AddAuthentication(); if (bool.Parse(configuration["Authentication:JwtBearer:IsEnabled"])) { authenticationBuilder.AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { // The signing key must match! ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration["Authentication:JwtBearer:SecurityKey"])), // Validate the JWT Issuer (iss) claim ValidateIssuer = true, ValidIssuer = configuration["Authentication:JwtBearer:Issuer"], // Validate the JWT Audience (aud) claim ValidateAudience = true, ValidAudience = configuration["Authentication:JwtBearer:Audience"], // Validate the token expiry ValidateLifetime = true, // If you want to allow a certain amount of clock drift, set that here ClockSkew = TimeSpan.Zero }; options.Events = new JwtBearerEvents { OnMessageReceived = QueryStringTokenResolver }; }); } IdentityModelEventSource.ShowPII = true; authenticationBuilder.AddIdentityServerAuthentication("Bearer", options => { options.Authority = configuration["IdentityServer:Authority"]; options.ApiName = configuration["IdentityServer:ApiName"]; options.ApiSecret = configuration["IdentityServer:ApiSecret"]; options.RequireHttpsMetadata = false; }); } /* This method is needed to authorize SignalR javascript client. * SignalR can not send authorization header. So, we are getting it from query string as an encrypted text. */ private static Task QueryStringTokenResolver(MessageReceivedContext context) { if (!context.HttpContext.Request.Path.HasValue || !context.HttpContext.Request.Path.Value.StartsWith("/signalr")) { //We are just looking for signalr clients return Task.CompletedTask; } var qsAuthToken = context.HttpContext.Request.Query["enc_auth_token"].FirstOrDefault(); if (qsAuthToken == null) { //Cookie value does not matches to querystring value return Task.CompletedTask; } //Set auth token from cookie context.Token = SimpleStringCipher.Instance.Decrypt(qsAuthToken, AppConsts.DefaultPassPhrase); return Task.CompletedTask; } } }
- 修改Startup文件,因為有部分文件在Web.Core項目中,但是還沒有添加進來,所以現在編譯會報錯,先忽略
using System; using Abp.AspNetCore; using Abp.AspNetCore.Mvc.Antiforgery; using Abp.Castle.Logging.Log4Net; using Abp.Dependency; using Abp.Ids4.Configuration; using Abp.Ids4.Identity; using Abp.Ids4.Web.Core.IdentityServer; using Abp.Json; using Castle.Facilities.Logging; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Newtonsoft.Json.Serialization; namespace Abp.Ids4.Server.Startup { public class Startup { private readonly IConfigurationRoot _appConfiguration; public Startup(IWebHostEnvironment env) { _appConfiguration = env.GetAppConfiguration(); } public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddControllersWithViews( options => { options.Filters.Add(new AbpAutoValidateAntiforgeryTokenAttribute()); } ).AddNewtonsoftJson(options => { options.SerializerSettings.ContractResolver = new AbpMvcContractResolver(IocManager.Instance) { NamingStrategy = new CamelCaseNamingStrategy() }; }); IdentityRegistrar.Register(services); IdentityServerRegistrar.Register(services, _appConfiguration); AuthConfigurer.Configure(services, _appConfiguration); // Configure Abp and Dependency Injection return services.AddAbp<AbpIds4ServerModule>( // Configure Log4Net logging options => options.IocManager.IocContainer.AddFacility<LoggingFacility>( f => f.UseAbpLog4Net().WithConfig("log4net.config") ) ); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseAbp(); //Initializes ABP framework. if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } if (bool.Parse(_appConfiguration["IdentityServer:IsEnabled"])) { app.UseJwtTokenMiddleware(); app.UseIdentityServer(); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); }); } } }
- 從Web.Core項目中複製appsettings.json和log4net.config到IdentityServer項目,在appsettings.json文件中增加IdentityServer4配置
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "ConnectionStrings": { "Default": "Server=localhost\sqlexpress; Database=Ids4Db; Trusted_Connection=True;" }, "Authentication": { "Facebook": { "IsEnabled": "false", "AppId": "", "AppSecret": "" }, "Google": { "IsEnabled": "false", "ClientId": "", "ClientSecret": "" }, "JwtBearer": { "IsEnabled": "false", "SecurityKey": "Ids4_C421AAEE0D126E5C", "Issuer": "Ids4", "Audience": "Ids4" } }, "IdentityServer": { "IsEnabled": "true", "Authority": "http://localhost:5000", "ApiName": "default-api", "ApiSecret": "secret", "Clients": [ { "ClientId": "client", "AllowedGrantTypes": [ "password", "client_credentials" ], "ClientSecrets": [ { "Value": "def2e777-5d42-4edc-a84a-30136c340e13" } ], "AllowedScopes": [ "default-api", "openid", "profile", "email" ] }, { "ClientId": "mvc_implicit", "ClientName": "MVC Client", "AllowedGrantTypes": [ "implicit" ], "RedirectUris": [ "http://localhost:5002/signin-oidc" ], "PostLogoutRedirectUris": [ "http://localhost:5002/signout-callback-oidc" ], "AllowedScopes": [ "openid", "profile", "email", "default-api" ], "AllowAccessTokensViaBrowser": true } ] } }
最終項目結構如下:
修改Web.Core項目
從IdentityServerDemo項目複製IdentityServer目錄和文件到xxx.Web.Core項目,修改文件中的命名空間和當前項目對應。修改IdentityServerRegistrar文件中的dbcontext,把直接引用dbcontext實例改成引用接口,如下:
public static void Register(IServiceCollection services, IConfigurationRoot configuration) { services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources()) .AddInMemoryApiResources(IdentityServerConfig.GetApiResources()) .AddInMemoryClients(IdentityServerConfig.GetClients(configuration)) -- .AddAbpPersistedGrants<IdentityServerDemoDbContext>() ++ .AddAbpPersistedGrants<IAbpPersistedGrantDbContext>() .AddAbpIdentityServer<User>(); }
EntityFrameworkCore項目及其他修改
- 按照Identity Server Integration文檔修改EntityFrameworkCore項目和nuget添加引用,同時把項目因為沒有引用包報錯的添加引用。現在運行IdentityServer項目從connect/token中獲取到token了,但是這個token還不能用。即使按照IdentityServerDemo配置了也用不了,IdentityServerDemo中實際上每個web項目都是登錄中心。
- 修改Web.Host項目的appsettings.json
{ "ConnectionStrings": { "Default": "Server=localhost\sqlexpress; Database=Ids4Db; Trusted_Connection=True;" }, "App": { "ServerRootAddress": "http://localhost:21022/", "ClientRootAddress": "http://localhost:8080/", "CorsOrigins": "http://localhost:4200,http://localhost:8080,http://localhost:8081,http://localhost:3000" }, "Authentication": { "JwtBearer": { "IsEnabled": "true", "SecurityKey": "Ids4_C421AAEE0D126E5C", "Issuer": "Ids4", "Audience": "Ids4" } }, "IdentityServer": { "IsEnabled": "true", "Authority": "http://localhost:5000", "ApiName": "default-api", "ApiSecret": "secret", "ClientId": "client", // no interactive user, use the clientid/secret for authentication "AllowedGrantTypes": "password", // secret for authentication "ClientSecret": "def2e777-5d42-4edc-a84a-30136c340e13", // scopes that client has access to "AllowedScopes": "default-api" } }
- Web.Host項目在AuthConfigurer.cs文件的Configure方法中增加如下代碼
var authenticationBuilder = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme); IdentityModelEventSource.ShowPII = true; authenticationBuilder // .AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme, options => //{ // options.Authority = configuration["IdentityServer:Authority"]; // options.ApiName = configuration["IdentityServer:ApiName"]; // options.ApiSecret = configuration["IdentityServer:ApiSecret"]; // //options.Audience = configuration["IdentityServer:ApiName"]; // options.RequireHttpsMetadata = false; //}) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.Authority = configuration["IdentityServer:Authority"]; options.RequireHttpsMetadata = false; options.Audience = configuration["IdentityServer:ApiName"]; }) ;
- 修改Web.Host項目中的Startup類
using System; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Castle.Facilities.Logging; using Abp.AspNetCore; using Abp.AspNetCore.Mvc.Antiforgery; using Abp.Castle.Logging.Log4Net; using Abp.Extensions; using Abp.Ids4.Configuration; using Abp.Ids4.Identity; using Abp.AspNetCore.SignalR.Hubs; using Abp.Dependency; using Abp.Json; using Microsoft.OpenApi.Models; using Newtonsoft.Json.Serialization; using Abp.Ids4.Web.Core.IdentityServer; namespace Abp.Ids4.Web.Host.Startup { public class Startup { private const string _defaultCorsPolicyName = "localhost"; private readonly IConfigurationRoot _appConfiguration; public Startup(IWebHostEnvironment env) { _appConfiguration = env.GetAppConfiguration(); } public IServiceProvider ConfigureServices(IServiceCollection services) { //MVC services.AddControllersWithViews( options => { options.Filters.Add(new AbpAutoValidateAntiforgeryTokenAttribute()); } ).AddNewtonsoftJson(options => { options.SerializerSettings.ContractResolver = new AbpMvcContractResolver(IocManager.Instance) { NamingStrategy = new CamelCaseNamingStrategy() }; }); IdentityRegistrar.Register(services); AuthConfigurer.Configure(services, _appConfiguration); //其他代碼 //... } public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { app.UseAbp(options => { options.UseAbpRequestLocalization = false; }); // Initializes ABP framework. app.UseCors(_defaultCorsPolicyName); // Enable CORS! app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); //app.UseJwtTokenMiddleware(); if (bool.Parse(_appConfiguration["IdentityServer:IsEnabled"])) { app.UseJwtTokenMiddleware(); } app.UseAbpRequestLocalization(); //...其他代碼 } } }
- 修改登錄方法從授權中心獲取token,修改Web.Core項目TokenAuthController.cs的Authenticate方法
public async Task<AuthenticateResultModel> Authenticate([FromBody] AuthenticateModel model) { var loginResult = await GetLoginResultAsync( model.UserNameOrEmailAddress, model.Password, GetTenancyNameOrNull() ); if (loginResult.Result != AbpLoginResultType.Success) { throw new UserFriendlyException("登錄失敗"); } //var accessToken = CreateAccessToken(CreateJwtClaims(loginResult.Identity)); var client = new HttpClient(); var disco = await client.GetDiscoveryDocumentAsync(_appConfiguration["IdentityServer:Authority"]); if (disco.IsError) { throw new UserFriendlyException(disco.Error); } var tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest { Address = disco.TokenEndpoint, ClientId = _appConfiguration["IdentityServer:ClientId"], ClientSecret = _appConfiguration["IdentityServer:ClientSecret"], UserName = model.UserNameOrEmailAddress, Password = model.Password, Scope = _appConfiguration["IdentityServer:AllowedScopes"], }); if (tokenResponse.IsError) { throw new UserFriendlyException(tokenResponse.Error); } var accessToken = tokenResponse.AccessToken; return new AuthenticateResultModel { AccessToken = accessToken, EncryptedAccessToken = GetEncryptedAccessToken(accessToken), ExpireInSeconds = (int)_configuration.Expiration.TotalSeconds, UserId = loginResult.User.Id }; }
至此host項目的登錄獲取的token就是從登錄中心獲取的了,其他客戶端的對接按照使用Identity Server 4建立Authorization Server配置就可以
參考資料