c# - 一个项目中的 IdentityServer4 和 Web Api 无法通过身份验证
问题描述
我坚持寻找我在这里遇到的问题。我试图从 SO 中的问题中查找,但可以找出问题所在。所以我很绝望。
所以在我的解决方案中,我们有 3 个项目
API
- 生产 API 资源
身份服务器4
- 身份服务器4
- 用于访问 IdentityServ4 上的客户端、范围等的 WebAPI 管理 API
客户端APP
- MVC 应用程序
一切都很顺利。客户端可以通过IS4登录和认证,访问生产资源。现在还需要创建 api 来从客户端应用程序管理 IS4。但似乎我无法使用 IS4 发布的相同令牌进行身份验证。
IS4 Log 上的信息如下
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Route matched with {action = "GetUserAccountsList", controller = "Accounts"}. Executing action Identity.API.API.AccountsController.GetUserAccountsList (Identity.API)
dbug: IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[9]
AuthenticationScheme: Bearer was not authenticated.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
info: Microsoft.AspNetCore.Mvc.ChallengeResult[1]
Executing ChallengeResult with authentication schemes (Bearer).
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12]
AuthenticationScheme: BearerIdentityServerAuthenticationJwt was challenged.
info: IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[12]
AuthenticationScheme: Bearer was challenged.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action Identity.API.API.AccountsController.GetUserAccountsList (Identity.API) in 0.212ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 0.6503ms 401
IS4 Web API 上的 API 代码
[Authorize(AuthenticationSchemes = "Bearer")]
[HttpGet]
public async Task<IActionResult> GetUserAccountsList()
{
var userAccounts = await _accountService.GetIdentityAccountsAsync();
return new JsonResult(userAccounts);
}
并在启动时配置服务
public void ConfigureServices(IServiceCollection services)
{
var dbConnectionName = Constants.Environment.Development;
if (_env.IsProduction())
{
dbConnectionName = Constants.Environment.Production;
}
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString(dbConnectionName), sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
}));
services.AddIdentity<ApplicationUser, IdentityRole>()
// use this if we want to implement default ASP.NET identity
//services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Configure DI
ConfigureDependencies(services);
services.AddMvc();
#region Registering ASP.NET Identity Server
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer(options =>
{
options.IssuerUri = Constants.Address.GetIdentityServerAdress(_env.IsDevelopment());
options.Authentication.CookieLifetime = TimeSpan.FromHours(2);
})
// change to certificate credentials on production
// .AddSigningCredential(Certificate.Get())
.AddDeveloperSigningCredential()
.AddAspNetIdentity<ApplicationUser>()
//// this adds the config data from DB (clients, resources) instead of memory
//.AddInMemoryIdentityResources(Config.GetIdentityResources())
//.AddInMemoryClients(Config.GetClients(_env.IsProduction()))
//.AddInMemoryApiResources(Config.GetApiResources())
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(Configuration.GetConnectionString(dbConnectionName),
sql => sql.MigrationsAssembly(migrationsAssembly));
})
//// this adds the operational data from DB (codes, tokens, consents)
//.AddInMemoryPersistedGrants()
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(Configuration.GetConnectionString(dbConnectionName),
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
})
.AddProfileService<IdentityProfileService>(); ;
#endregion Registering ASP.NET Identity Server
services.RegisterApplicationPolicy();
#region External Auth
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = Constants.Address.GetIdentityServerAdress(_env.IsDevelopment());
options.RequireHttpsMetadata = false;
options.ApiName = Constants.Resource.Identity;
// options.SupportedTokens = SupportedTokens.Both;
}); ;
#endregion External Auth
}
Startup.cs配置
public void Configure(IApplicationBuilder app, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
{
InitializeDatabase(app, _env, userManager, roleManager);
if (_env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseIdentityServer();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
IS4 上的客户端播种到 DB 并设置如下
public static class Resource
{
public static List<string> GetAllResourceList()
{
return new List<string>()
{
Clinic,
Subscription,
Module,
Identity // this is IDS4 Server
};
}
// this is used on db seed only.
// resource name has to be updated on DB after db seed
public const string Clinic = "Clinic";
public const string Subscription = "Subscription";
public const string Module = "Module";
public const string Identity = "Identity";
public const string ClinicAddress = "http://localhost:5100";
public const string SubscriptionAddress = "http://localhost:5200";
public const string ModuleAddress = "http://localhost:5300";
public const string IdentityAddress = "http://localhost:5000";
}
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return Constants.Resource.GetAllResourceList().Select(s => new ApiResource(s));
}
// client want to access resources (aka scopes)
public static IEnumerable<Client> GetClients(bool isDevelopment)
{
var client = new List<Client>();
var mvcClient = new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { $"{Constants.Address.GetClientServerAdress(isDevelopment)}/signin-oidc" },
PostLogoutRedirectUris =
{$"{Constants.Address.GetClientServerAdress(isDevelopment)}/signout-callback-oidc"},
AlwaysIncludeUserClaimsInIdToken = true,
AllowAccessTokensViaBrowser = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
},
AllowOfflineAccess = true
};
foreach (var resource in Constants.Resource.GetAllResourceList())
{
mvcClient.AllowedScopes.Add(resource);
}
client.Add(mvcClient);
return client;
}
//Add support for the standard openid (subject id) and profile scopes
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
}
在 Client App Startup.cs 上如下
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
// Adding Authentication options+
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = $"{Constants.Address.GetIdentityServerAdress(_env.IsDevelopment())}";
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.RequireHttpsMetadata = false;
foreach (var resource in Constants.Resource.GetAllResourceList())
{
options.Scope.Add(resource);
}
options.Scope.Add("offline_access");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role",
};
});
// Adding Authorisation
services.RegisterApplicationPolicy();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseAuthentication();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
我们正在尝试使用以下代码访问 IS4 上的 Web Api。看到下面的调用总是返回 401 Unauthorize
var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
client.SetBearerToken(accessToken);
var response = await client.GetAsync($"{Constants.Address.GetIdentityServerAdress(_env.IsDevelopment())}/api/Accounts/GetUserAccountsList");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
ViewBag.Json = JArray.Parse(content).ToString();
}
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
return Unauthorized();
}
return View("Json");
任何有关如何解决问题的建议都会有所帮助。目前我使用表单身份验证在 IS4 上进行客户端管理。
解决方案
IdentityServer 用于验证现有用户,而不是用于管理用户。所以我建议你创建另一个受 IdenitityServer 保护的 api 应用程序来管理用户,api 应用程序和身份服务器应用程序共享同一个数据库。
但当然,您也可以将 api 添加到您的身份服务器应用程序。根据您的代码,您应该修改为:
Config.cs
您应该在 Identity Server 中添加 Api 资源:public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("api1", "My API") }; }
在您的身份服务器中修改您的客户端
Config.cs
,允许客户端获取访问令牌以访问 api 资源:AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "api1" },
您的身份服务器应验证访问令牌:
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) .AddIdentityServerAuthentication(options => { options.Authority = "https://localhost:44373"; //IDS's endpoint options.RequireHttpsMetadata = false; options.ApiName = "api1"; //api name });
修改您的客户端以使用 Hybrid Flow 获取访问令牌:
.AddOpenIdConnect("oidc", options => { options.SignInScheme = "Cookies"; options.Authority = "https://localhost:44373/"; options.RequireHttpsMetadata = false; options.ClientId = "mvc2"; options.ClientSecret = "secret"; options.ResponseType = "code id_token"; options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; options.Scope.Add("api1"); //Api name options.Scope.Add("offline_access"); });
获取访问令牌后,您可以使用
Authorization: Bearer xxxx
header 请求 api 端点。
推荐阅读
- sorting - 带有两位小数的 JavaFX 浮点列
- node.js - 通过 Wiki.js 在 Google App Engine 上使用 mLab(连接超时)
- tfs - TFS 2010 到 2018 迁移到新硬件和域
- javascript - 如何实现动量缩放
- jupyter-notebook - Jupyter/IPython:如何以传统的数学符号获得结果?
- arduino - ESP32 接入点
- python-2.7 - 如何在 TensorFlow 中提供自定义渐变
- ios - 单例属性根据调用返回不同的值
- android - Android - “无法连接到相机服务” - 如何清除/重置?
- jpa - JPQL - 基于最新元素实现条件逻辑