在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配置就可以了
源碼:https://gitee.com/XiaoShenXiana/abp_ids4.git
原文
參考資料