abp集成IdentityServer4和單點登錄

  • 2020 年 2 月 13 日
  • 筆記

在abp開發的系統後,需要使用這個系統作單點登錄,及其他項目登錄賬號依靠abp開發的系統。在官方文檔上只找到作為登錄服務Identity Server Integration,但是host項目卻無法使用登錄服務生成的Token獲取數據。所有的搜索結果包括abp的issue都是說去看identity server4的文檔。我比較笨,文檔看了還是不會。好在最後還是試出來了。


創建登錄中心項目

  1. 到官網下載一個最新的模板項目,項目類型自選(我們項目用的vue,所以我選擇的vue項目,.net core3.x)。保證可以運行起來並正常登錄。
  2. 右鍵src目錄添加一個asp.net core web 空項目,在項目中添加Startup文件夾,把Startup.cs和Program.cs移動到Startup文件夾,並修改這兩個文件的命名空間增加Startup。不然會有命名空間和類名衝突。
  3. 在nuget添加Abp.ZeroCore.IdentityServer4、Abp、Abp.Castle.Log4Net等引用,添加Web.Core、EntityFrameworkCore項目引用
  4. 在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());          }      }  }
  1. 在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;          }      }  }
  1. 修改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();              });          }      }  }
  1. 從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項目及其他修改

  1. 按照Identity Server Integration文檔修改EntityFrameworkCore項目和nuget添加引用,同時把項目因為沒有引用包報錯的添加引用。現在運行IdentityServer項目從connect/token中獲取到token了,但是這個token還不能用。即使按照IdentityServerDemo配置了也用不了,IdentityServerDemo中實際上每個web項目都是登錄中心。
  2. 修改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"    }  }
  1. 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"];  })  ;
  1. 修改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();              //...其他代碼          }      }  }
  1. 修改登錄方法從授權中心獲取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配置就可以

原文

參考資料