ASP.NET Core+Angular模板中Duende M2M客户端配置及C#令牌请求问题
问题背景
使用Visual Studio最新版ASP.NET Core with Angular模板创建项目,新增Windows Service调用API的需求,需通过IdentityServer获取令牌。但Postman请求令牌时始终返回“Authentication failed”,日志提示“Unknown client 'postman'”,内存中定义的客户端未生效;尝试添加.AddAspNetIdentity()又触发System.InvalidOperationException: No service for type 'Duende.IdentityServer.Stores.ISigningCredentialStore' has been registered错误。
核心问题
模板自带的.AddApiAuthorization<ApplicationUser, ApplicationDbContext>()方法会自动配置IdentityServer使用EF数据库存储,覆盖手动添加的内存客户端、资源配置,导致Postman无法找到定义的postman客户端。
解决方案一:启用内存存储(开发/测试场景)
调整IdentityServer注册逻辑,移除自动EF存储配置,手动添加AspNetIdentity支持及签名凭证:
修改Program.cs的IdentityServer配置
替换原IdentityServer注册代码:
builder.Services.AddIdentityServer(options => { options.Events.RaiseErrorEvents = true; options.Events.RaiseInformationEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseSuccessEvents = true; options.EmitStaticAudienceClaim = true; }) .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryClients(Config.Clients) .AddAspNetIdentity<ApplicationUser>() // 添加AspNetIdentity用户支持 .AddDeveloperSigningCredential(); // 开发环境生成临时签名证书,生产需替换为正式证书
注意:
.AddDeveloperSigningCredential()仅用于开发环境,生产环境需使用持久化的签名证书(如从文件或密钥管理服务加载)。
补充Angular客户端配置
由于移除了.AddApiAuthorization()的自动配置,需在Config.Clients中添加Angular SPA的客户端配置(确保与Angular项目环境变量中的clientId一致):
// Angular SPA 客户端 new Client { ClientId = "Project1_Angular", // 替换为你的Angular项目clientId ClientName = "Angular SPA Client", AllowedGrantTypes = GrantTypes.Code, RequirePkce = true, RequireClientSecret = false, RedirectUris = { "https://localhost:5001/authentication/login-callback" }, // 替换为你的Angular登录回调地址 PostLogoutRedirectUris = { "https://localhost:5001/authentication/logout-callback" }, AllowedCorsOrigins = { "https://localhost:5001" }, // 替换为你的Angular应用地址 AllowedScopes = { "openid", "profile", "api1" } }
解决方案二:使用数据库存储(生产场景)
若需保留EF数据库存储,可将内存中的客户端、资源配置迁移到数据库:
- 保留
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(),移除.AddInMemoryXXX方法。 - 添加初始化代码,启动时将Config中的配置写入数据库:
var app = builder.Build(); // 初始化IdentityServer配置到数据库 using (var scope = app.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); var clientStore = scope.ServiceProvider.GetRequiredService<IClientStore>(); // 检查并添加postman客户端 var existingClient = await clientStore.FindClientByIdAsync("postman"); if (existingClient == null) { var postmanClient = Config.Clients.First(c => c.ClientId == "postman"); context.Clients.Add(postmanClient.ToEntity()); await context.SaveChangesAsync(); } // 同理初始化ApiScopes和IdentityResources } // 后续管道配置...
C#实现客户端凭证模式令牌请求
Windows Service可通过客户端凭证模式获取令牌,示例代码如下:
using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Newtonsoft.Json; public class TokenService { private readonly HttpClient _httpClient; public TokenService(HttpClient httpClient) { _httpClient = httpClient; _httpClient.BaseAddress = new Uri("https://localhost:7229/"); // 替换为你的IdentityServer地址 } public async Task<string> GetAccessTokenAsync() { var requestContent = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("grant_type", "client_credentials"), new KeyValuePair<string, string>("client_id", "postman"), new KeyValuePair<string, string>("client_secret", "511536EF-F270-4058-80CA-1C89C192F69A"), new KeyValuePair<string, string>("scope", "api1") }); var response = await _httpClient.PostAsync("connect/token", requestContent); response.EnsureSuccessStatusCode(); var responseJson = await response.Content.ReadAsStringAsync(); var tokenResult = JsonConvert.DeserializeObject<TokenResponse>(responseJson); return tokenResult.AccessToken; } } // 令牌响应模型 public class TokenResponse { [JsonProperty("access_token")] public string AccessToken { get; set; } [JsonProperty("token_type")] public string TokenType { get; set; } [JsonProperty("expires_in")] public int ExpiresIn { get; set; } }
使用时,在Windows Service中注册HttpClient和TokenService,调用GetAccessTokenAsync获取令牌后,在API请求头中添加Authorization: Bearer {token}即可访问受保护API。
内容的提问来源于stack exchange,提问作者Andrei Dobrin

