ASP.NET Core 使用OpenIddict打造认证中心(1)

📅 2026/6/26 6:08:21
ASP.NET Core 使用OpenIddict打造认证中心(1)
以前用IdentityServer4但随着.NETCORE版本的提升以及IdentityServer4也不在维护在.NET Core8之后用什么做认证中心是个问题。可选的并不多1、Duende IdentityServer商用许可 (社区版免费)IdentityServer4官方继任者功能全面标准兼容性极强。但是想到要去申请社区版的许可就很麻烦。2、OpenIddict完全开源 (Apache 2.0).NET原生深度集成EF Core高度可控免费3、Keycloak独立服务 (Java)完全开源 (Apache 2.0)功能开箱即用含登录UI、用户管理、2FA社区庞大。但是语言变了如果不懂JAVA可能麻烦一些。所以我选择了OpenIddict当然代码也不是我一行一行敲出来的也不是一页一页看官方文档写复制粘贴来的这是AI的时代只需要亿点点的给给AI下任务它就能给我想要的而且写的比我好我喂的好这篇写的是AuthCenter部分废话不多说了直接上代码Program.cs这里用了OpenIddict的数据表但用户表我用的是自己的用了freesql来处理自己的数据你喜欢什么ORM就用什么这里随便发挥using AuthCenter.Data; using AuthCenter.Services; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.EntityFrameworkCore; using OpenIddict.Abstractions; using Quartz; using static OpenIddict.Abstractions.OpenIddictConstants; var builder WebApplication.CreateBuilder(args); // // 1. 配置 Cookie 认证用于登录页面 // builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options { options.LoginPath /Account/Login; options.LogoutPath /Account/Logout; options.ExpireTimeSpan TimeSpan.FromDays(14); options.SlidingExpiration true; }); // // 2. 配置数据库上下文仅用于 OpenIddict 核心数据 // builder.Services.AddDbContextAuthDbContext(options { // 使用 SQLServer数据库 options.UseSqlServer(builder.Configuration.GetConnectionString(DefaultConnection)); // 关键让 EF Core 使用 OpenIddict 的实体模型 options.UseOpenIddict(); }); builder.Services.AddSingletonIFreeSql(sp { var connectionString builder.Configuration.GetConnectionString(DefaultConnection); return new FreeSql.FreeSqlBuilder() .UseConnectionString(FreeSql.DataType.SqlServer, connectionString) // 根据实际数据库类型修改 .UseAutoSyncStructure(true) // 开发环境可自动创建表生产环境建议关闭 .Build(); }); // 配置 Quartz 服务 builder.Services.AddQuartz(options { options.UseInMemoryStore(); // 使用内存存储如需持久化可更换为数据库 }); // 添加 Quartz 托管服务 builder.Services.AddQuartzHostedService(options { options.WaitForJobsToComplete true; }); // // 3. 配置 OpenIddict // builder.Services.AddOpenIddict() // 3.1 核心配置存储层 .AddCore(options { options.UseEntityFrameworkCore() .UseDbContextAuthDbContext(); // 注册 Quartz.NET 定时任务用于清理过期Token推荐添加 options.UseQuartz(); }) // 3.2 服务器配置颁发Token .AddServer(options { // 设置端点 options.SetTokenEndpointUris(connect/token) .SetAuthorizationEndpointUris(connect/authorize) .SetIntrospectionEndpointUris(connect/introspect) .SetRevocationEndpointUris(connect/revoke) .SetUserInfoEndpointUris(connect/userinfo) .SetEndSessionEndpointUris(connect/logout); // 启用授权流程 options.AllowAuthorizationCodeFlow() // 授权码流程推荐前端用 .AllowRefreshTokenFlow() // 刷新Token //.AllowClientCredentialsFlow() // 客户端凭证服务间调用 .AllowPasswordFlow(); // 密码模式兼容旧系统非必须 // 注册声明和作用域 options.RegisterClaims( Claims.Subject, Claims.Name, Claims.Email, Claims.Role); options.RegisterScopes( Scopes.OpenId, Scopes.Profile, Scopes.Email, Scopes.Roles, Scopes.OfflineAccess, api1, api2, api3); // 你自己的 API 作用域 // Token 有效期配置 options.SetAccessTokenLifetime(TimeSpan.FromMinutes(30)); options.SetRefreshTokenLifetime(TimeSpan.FromDays(14)); options.SetRefreshTokenReuseLeeway(TimeSpan.FromMinutes(2)); // 开发环境使用临时证书生产环境务必换成持久化证书 options.AddDevelopmentEncryptionCertificate() .AddDevelopmentSigningCertificate(); // 关闭访问令牌加密方便调试生产环境可开启 options.DisableAccessTokenEncryption(); // 使用引用刷新令牌便于撤销 options.UseReferenceRefreshTokens(); // 集成 ASP.NET Core options.UseAspNetCore() .EnableTokenEndpointPassthrough() .EnableAuthorizationEndpointPassthrough() .EnableEndSessionEndpointPassthrough() .DisableTransportSecurityRequirement(); // 开发环境允许HTTP }) // 3.3 验证配置用于API验证Token .AddValidation(options { // 使用本地服务器验证如果API和认证中心在同一个项目或者内网通信 options.UseLocalServer(); options.UseAspNetCore(); }); // // 4. 注册你自己的服务对接FreeSql和用户表 // builder.Services.AddScopedIUserService, UserService(); // // 5. 注册 MVC 和 Razor Pages登录页面用 // builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); // 从配置中读取允许的源 var allowedOrigins builder.Configuration .GetSection(CorsSettings:AllowedOrigins) .Getstring[](); // 如果配置不存在或为空可以给一个默认值开发环境容错 if (allowedOrigins null || allowedOrigins.Length 0) { allowedOrigins new[] { http://localhost:3000 }; // 兜底默认值 } // 添加 CORS 策略允许 Vue 前端跨域访问 builder.Services.AddCors(options { options.AddPolicy(AllowVueApps, policy { policy.WithOrigins(allowedOrigins) // 使用配置的数组 .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); // 允许携带 Cookie }); }); var app builder.Build(); // // 6. 初始化数据库自动创建OpenIddict需要的表 // using (var scope app.Services.CreateScope()) { // 1. 创建 OpenIddict 表已完成 var dbContext scope.ServiceProvider.GetRequiredServiceAuthDbContext(); await dbContext.Database.EnsureCreatedAsync(); // 2. 强制同步 User 表新增 var fsql scope.ServiceProvider.GetRequiredServiceIFreeSql(); fsql.CodeFirst.SyncStructureUser(); // 创建 sys_user 表 Console.WriteLine(✅ User 表结构已同步); // 3. 种子客户端数据已有 await SeedClientsAsync(scope.ServiceProvider); // 4. 种子用户数据新增 await SeedUsersAsync(scope.ServiceProvider); } // // 7. 中间件管道 // if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler(/Error); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); // 启用 CORS 策略 在 UseRouting 之后UseAuthentication 之前 app.UseCors(AllowVueApps); // 注意顺序认证 - 授权 app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.MapRazorPages(); await app.RunAsync(); // // 种子数据注册你的 Vue 前端作为客户端 // async Task SeedClientsAsync(IServiceProvider provider) { var manager provider.GetRequiredServiceIOpenIddictApplicationManager(); // 检查客户端是否已存在不存在则创建 if (await manager.FindByClientIdAsync(vue_app) is null) { var descriptor new OpenIddictApplicationDescriptor { ClientId vue_app, ClientType ClientTypes.Public, // 前端应用通常是公共客户端 //ClientSecret vue_app_secret, // 生产环境请换强密码 DisplayName Vue 前端应用, ConsentType ConsentTypes.Implicit, RedirectUris { new Uri(http://localhost:3000/callback), new Uri(http://localhost:3001/callback), new Uri(http://localhost:3002/callback), new Uri(http://localhost:5173/callback) }, PostLogoutRedirectUris { new Uri(http://localhost:3000/signout-callback), new Uri(http://localhost:3001/signout-callback), new Uri(http://localhost:3002/signout-callback), new Uri(http://localhost:5173/signout-callback) }, Permissions { // 端点权限 Permissions.Endpoints.Authorization, Permissions.Endpoints.Token, Permissions.Endpoints.EndSession, // 授权类型 Permissions.GrantTypes.AuthorizationCode, Permissions.GrantTypes.RefreshToken, // 响应类型 Permissions.ResponseTypes.Code, // 使用 Permissions.Scopes 常量正确格式 scp:openid Permissions.Scopes.Profile, Permissions.Scopes.Email, Permissions.Scopes.Roles, scp:openid, scp:offline_access, // 自定义 scope 需手动加 scp: 前缀 scp:api1, scp:api2, scp:api3 }, Requirements { Requirements.Features.ProofKeyForCodeExchange // 启用 PKCE } }; await manager.CreateAsync(descriptor); } // 定义多个 API 资源客户端 var apiClients new[] { new { ClientId api1, Secret secret-123, DisplayName 订单 API }, new { ClientId api2, Secret secret-456, DisplayName 用户 API }, new { ClientId api3, Secret secret-789, DisplayName 产品 API } }; foreach (var api in apiClients) { if (await manager.FindByClientIdAsync(api.ClientId) is null) { var descriptor new OpenIddictApplicationDescriptor { ClientId api.ClientId, ClientSecret api.Secret, ClientType ClientTypes.Confidential, DisplayName api.DisplayName, Permissions { Permissions.Endpoints.Introspection } }; await manager.CreateAsync(descriptor); } } } // // 种子数据创建测试用户 // async Task SeedUsersAsync(IServiceProvider provider) { // 方案一如果你用的是 IBaseRepositoryUser // var userRepo provider.GetRequiredServiceIBaseRepositoryUser(); // 方案二如果你用的是 IFreeSql推荐更灵活 var fsql provider.GetRequiredServiceIFreeSql(); var userRepo fsql.GetRepositoryUser(); // 检查 admin 用户是否已存在 var exists await userRepo.Select.AnyAsync(u u.UserName admin); if (exists) { Console.WriteLine(✅ 测试用户 admin 已存在跳过创建); return; } // 用 BCrypt 加密密码 123456 var passwordHash BCrypt.Net.BCrypt.HashPassword(123456); var adminUser new User { UserName admin, PasswordHash passwordHash, Email adminexample.com, DisplayName 系统管理员, Roles admin,manager, IsActive true, EmailConfirmed true, CreatedAt DateTime.Now, UpdatedAt DateTime.Now }; await userRepo.InsertAsync(adminUser); Console.WriteLine(✅ 测试用户已创建: admin / 123456); }AuthorizationController控制器using AuthCenter.Services; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using System.Collections.Immutable; using System.Security.Claims; using static OpenIddict.Abstractions.OpenIddictConstants; namespace AuthCenter.Controllers; [ApiController] [Route(connect)] public class AuthorizationController : Controller { private readonly IUserService _userService; public AuthorizationController(IUserService userService) { _userService userService; } // // 1. 处理授权请求/connect/authorize // [HttpGet(authorize)] [HttpPost(authorize)] public IActionResult Authorize() { var request HttpContext.GetOpenIddictServerRequest(); if (request null) return BadRequest(无效的 OIDC 请求); // 检查用户是否已通过 Cookie 登录 if (!User.Identity?.IsAuthenticated true) { // 未登录重定向到 Razor 登录页并携带 ReturnUrl var challengeProps new AuthenticationProperties { RedirectUri Request.Path Request.QueryString }; return Challenge(challengeProps, CookieAuthenticationDefaults.AuthenticationScheme); } // 已登录从 Cookie 中提取用户标识 var userId User.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(userId)) return Forbid(无法识别当前用户); var identity new ClaimsIdentity( authenticationType: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, nameType: Claims.Name, roleType: Claims.Role); identity.AddClaim(new Claim(Claims.Subject, userId)); identity.AddClaim(new Claim(Claims.Name, User.FindFirstValue(Claims.Name) ?? userId)); var email User.FindFirstValue(Claims.Email); if (!string.IsNullOrEmpty(email)) identity.AddClaim(new Claim(Claims.Email, email)); var roles User.FindAll(Claims.Role).Select(c c.Value).ToList(); foreach (var role in roles) identity.AddClaim(new Claim(Claims.Role, role)); var principal new ClaimsPrincipal(identity); principal.SetScopes(request.GetScopes()); principal.SetResources(GetResources(request.GetScopes())); // 重定向回 redirect_uri并携带授权码 var authProperties new AuthenticationProperties { RedirectUri request.RedirectUri }; return SignIn(principal, authProperties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } // // 2. 处理 Token 请求/connect/token // [HttpPost(token)] [Produces(application/json)] public async TaskIActionResult Token() { var request HttpContext.GetOpenIddictServerRequest(); if (request null) return BadRequest(无效请求); // 密码模式用于测试 if (request.IsPasswordGrantType()) { var user await _userService.AuthenticateAsync( request.Username ?? , request.Password ?? ); if (user null) { // 替换原有的 Dictionarystring, string 为 Dictionarystring, string?以匹配 AuthenticationProperties 的构造函数签名 var authProperties new AuthenticationProperties(new Dictionarystring, string? { [OpenIddictServerAspNetCoreConstants.Properties.Error] Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] 用户名或密码错误 }); return Forbid(authProperties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } var identity new ClaimsIdentity( OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, Claims.Name, Claims.Role); identity.AddClaim(new Claim(Claims.Subject, user.Id.ToString())); identity.AddClaim(new Claim(Claims.Name, user.UserName)); identity.AddClaim(new Claim(Claims.Email, user.Email ?? )); foreach (var role in user.Roles ?? []) identity.AddClaim(new Claim(Claims.Role, role)); var principal new ClaimsPrincipal(identity); principal.SetScopes(request.GetScopes()); // 使用请求中的 scopes principal.SetResources(GetResources(request.GetScopes())); // 使用辅助方法 return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } // 授权码模式从 /authorize 跳转过来后用 code 换 token if (request.IsAuthorizationCodeGrantType()) { // 验证授权码并颁发 tokenOpenIddict 会自动处理 var authenticateResult await HttpContext.AuthenticateAsync( OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); var principal authenticateResult?.Principal; if (principal null) { return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } // 刷新令牌模式 if (request.IsRefreshTokenGrantType()) { var authenticateResult await HttpContext.AuthenticateAsync( OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); var principal authenticateResult?.Principal; if (principal null) { return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } return BadRequest(new { error unsupported_grant_type }); } // // 3. 处理 UserInfo 请求/connect/userinfo // [HttpGet(userinfo)] [Authorize(AuthenticationSchemes OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)] public IActionResult UserInfo() { var user HttpContext.User; var subject user.FindFirstValue(Claims.Subject) ?? string.Empty; var name user.FindFirstValue(Claims.Name) ?? string.Empty; var email user.FindFirstValue(Claims.Email) ?? string.Empty; var claims new Dictionarystring, object { [Claims.Subject] subject, [Claims.Name] name, [Claims.Email] email }; var roles user.FindAll(Claims.Role).Select(c c.Value).ToArray(); if (roles.Any()) claims[Claims.Role] roles; return Ok(claims); } // // 4. 处理 OIDC 登出请求/connect/logout // [HttpGet(logout)] [HttpPost(logout)] [AllowAnonymous] public async TaskIActionResult Logout() { // 1. 获取请求信息用于提取 post_logout_redirect_uri var request HttpContext.GetOpenIddictServerRequest(); // 2. 清除本地 Cookie应用会话 await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); // 3. 清除 OpenIddict 相关的认证状态如果有 await HttpContext.SignOutAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); // 4. 根据 OIDC 规范应该将用户重定向到 post_logout_redirect_uri // 如果请求中包含该参数且是允许的则重定向否则可以重定向到首页或显示成功页面。 if (!string.IsNullOrEmpty(request?.PostLogoutRedirectUri)) { // 注意建议验证该 URI 是否在客户端的 PostLogoutRedirectUris 白名单中。 // 在开发环境可以简单允许生产环境务必验证。 return Redirect(request.PostLogoutRedirectUri); } // 如果没有提供 post_logout_redirect_uri可以返回一个自定义页面或重定向到根路径 return Redirect(/); } // // 辅助方法根据作用域获取资源 // private static IEnumerablestring GetResources(ImmutableArraystring scopes) { //var resources new Liststring(); //if (scopes.Contains(api)) // resources.Add(api); //if (scopes.Contains(Scopes.OpenId)) // resources.Add(Scopes.OpenId); //if (scopes.Contains(Scopes.Profile)) // resources.Add(Scopes.Profile); //if (scopes.Contains(Scopes.Email)) // resources.Add(Scopes.Email); //return resources; // 只返回以 api 开头的作用域作为资源标识 return scopes.Where(s s.StartsWith(api, StringComparison.OrdinalIgnoreCase)); } }AuthDbContext数据库上下文using Microsoft.EntityFrameworkCore; using OpenIddict.EntityFrameworkCore.Models; namespace AuthCenter.Data; /// summary /// OpenIddict 专用的数据库上下文仅用于存储认证数据 /// /summary public class AuthDbContext : DbContext { public AuthDbContext(DbContextOptionsAuthDbContext options) : base(options) { } // OpenIddict 需要的四个实体集合 public DbSetOpenIddictEntityFrameworkCoreApplication Applications { get; set; } public DbSetOpenIddictEntityFrameworkCoreAuthorization Authorizations { get; set; } public DbSetOpenIddictEntityFrameworkCoreScope Scopes { get; set; } public DbSetOpenIddictEntityFrameworkCoreToken Tokens { get; set; } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // 使用 OpenIddict 的 EF Core 默认映射配置 builder.UseOpenIddict(); } }UserService用户服务using AuthCenter.Data; using FreeSql; namespace AuthCenter.Services { public interface IUserService { TaskAppUser AuthenticateAsync(string userName, string password); } public class AppUser { public int Id { get; set; } public string UserName { get; set; } public string Email { get; set; } public Liststring Roles { get; set; } []; public string DisplayName { get; set; } public bool IsActive { get; set; } } public class UserService : IUserService { private readonly IFreeSql _fsql; public UserService(IFreeSql fsql) { _fsql fsql; } public async TaskAppUser? AuthenticateAsync(string userName, string password) { // 直接用 _fsql 查询 var user await _fsql.SelectUser() .Where(u u.UserName userName u.IsActive) .FirstAsync(); if (user null) return null; if (!VerifyPassword(password, user.PasswordHash)) return null; return new AppUser { Id user.Id, UserName user.UserName, Email user.Email, DisplayName user.DisplayName, Roles user.RoleList, IsActive user.IsActive }; } private bool VerifyPassword(string input, string storedHash) { // 这里用你自己的密码验证逻辑 return BCrypt.Net.BCrypt.Verify(input, storedHash); } } }还有几个页面没贴出来源代码下载