基于OpenIddict6.4.0搭建授权认证服务

什么是 OpenIddict?

OpenIddict 旨在提供一种多功能解决方案 ,以在任何 .NET 应用程序中实现OpenID Connect 客户端、服务器和令牌验证支持

入门

如果您正在寻找不涉及编写自定义逻辑的交钥匙解决方案,请考虑查看ABP FrameworkOrchardCore:它们开箱即用地支持 OpenIddict,并且是实现标准 OpenID Connect 服务器的最佳选择(即使您不熟悉该标准!)。

如果您更喜欢实施自定义解决方案,请阅读入门指南

可以在"集成"中找到其他集成(免费或商业)。

为什么不选择IdentityServer?

identityserver是最灵活且符合标准的 ASP.NET Core OpenID Connect 和 OAuth 2.0 框架。但是由于从 IdentityServer4 的 Apache 2.0 协议转变为 Duende IdentityServer 的商业许可。有功能受限的 Community Edition ,以及需要付费的 Business EditionEnterprise Edition

总结与对比

项目 当前状态 许可证 是否免费? 是否开源?
IdentityServer4 已停止维护 Apache 2.0
Duende IdentityServer 当前官方版本 商业许可证 有条件免费 (年收入<100万美金)
OpenIddict 当前官方版本 开源许可(Apache 2.0)。完全免费用于商业和个人项目

OpenIddict的实践

预期部署架构

项目 许可证
授权中心服务 https://xxx.com/passport
资源API服务 http://localhost:9000

搭建授权中心服务

新增代码入口Program.cs代码

cs 复制代码
using BigData.Passport.API;
using BigData.Passport.Domain;
using BigData.Passport.Domain.Models;
using BigData.Passport.Domain.Services;
using BigData.SSO.Passport;
using BigData.SSO.Passport.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using OpenIddict.EntityFrameworkCore.Models;
using TrendLib.MiniWeb;

var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;

//  数据库上下文配置
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    // Configure the context to use MySQL Server.
    options.UseMySQL(configuration.GetConnectionString("DefaultConnection"));

    // Register the entity sets needed by OpenIddict.
    // Note: use the generic overload if you need
    // to replace the default OpenIddict entities.
    options.UseOpenIddict();
});

// 配置cookie数据保护(持久化存储),避免服务器容器重启登录失效的问题
builder.Services.AddDataProtection()
    .PersistKeysToDbContext<ApplicationDbContext>()
    .SetApplicationName("Passport")
    .SetDefaultKeyLifetime(TimeSpan.FromDays(90));

// Register the Identity services.
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
    // 放宽密码策略
    options.Password.RequireDigit = false;          // 不需要数字
    options.Password.RequiredLength = 6;            // 最小长6
    options.Password.RequireLowercase = false;      // 不需要小写字母
    options.Password.RequireUppercase = false;      // 不需要大写字母
    options.Password.RequireNonAlphanumeric = false; // 不需要特殊字符
    options.Password.RequiredUniqueChars = 1;       // 唯一字符数

    // 其他配置
    options.SignIn.RequireConfirmedAccount = false;
    options.User.RequireUniqueEmail = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

// 配置 Application Cookie
builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.Name = "SSO.Auth";
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Lax;

    options.LoginPath = "/Account/Login";
    options.AccessDeniedPath = "/Account/AccessDenied";
    options.LogoutPath = "/Account/LogOut";

    options.ExpireTimeSpan = TimeSpan.FromDays(7); // cookie:sso.auth,勾选记住我,可以保留7天,否则就是会话级别cookie 
    options.SlidingExpiration = true;
});

// 配置 OpenIddict 核心
builder.Services.AddOpenIddict()
    .AddCore(options =>
    {
        // 使用内置的内存存储
        options.UseEntityFrameworkCore().UseDbContext<ApplicationDbContext>()
        .ReplaceDefaultEntities<OpenIddictApplications, OpenIddictAuthorization, OpenIddictScopes, OpenIddictToken, string>();
    })
    .AddServer(options =>
    {
        // 设置发行者地址
        options.SetIssuer(configuration["IdentityServer:Authority"]);

        // Enable the authorization, introspection and token endpoints.
        options.SetAuthorizationEndpointUris("connect/authorize")
               .SetTokenEndpointUris("connect/token")
               .SetUserInfoEndpointUris("connect/userinfo")
               .SetEndSessionEndpointUris("connect/logout")
               .SetConfigurationEndpointUris(".well-known/openid-configuration"); // Discovery 端点

        // Mark the "email", "profile" and "roles" scopes as supported scopes.
        options.RegisterScopes(OpenIddictConstants.Scopes.OpenId, //// 必须包含 openid 才能返回 id_token
            OpenIddictConstants.Scopes.Profile,
            OpenIddictConstants.Scopes.Email,
            OpenIddictConstants.Scopes.Roles,
            OpenIddictConstants.Scopes.OfflineAccess
        );

        // Note: this sample only uses the authorization code and refresh token
        // flows but you can enable the other flows if you need to support implicit,
        // password or client credentials.
        options.AllowAuthorizationCodeFlow()
            .AllowClientCredentialsFlow()
            .AllowPasswordFlow()
            .AllowRefreshTokenFlow()
            .AllowHybridFlow()
            .AllowImplicitFlow();

        // Register the encryption credentials. This sample uses a symmetric
        // encryption key that is shared between the server and the Api2 sample
        // (that performs local token validation instead of using introspection).
        //
        // Note: in a real world application, this encryption key should be
        // stored in a safe place (e.g in Azure KeyVault, stored as a secret).
        // options.AddEncryptionKey(new SymmetricSecurityKey(Convert.FromBase64String("DRjd/GnduI3Efzen9V9BvbNUfc/VKgXltV7Kbk9sMkY=")));

        // 加密和签名证书(开发环境)
        options.AddDevelopmentEncryptionCertificate()
            .AddDevelopmentSigningCertificate();
        
        // 配置令牌生命周期
        options.SetAccessTokenLifetime(TimeSpan.FromDays(1));
        options.SetRefreshTokenLifetime(TimeSpan.FromDays(7));

        // 禁用访问令牌加密(开发环境)
        options.DisableAccessTokenEncryption();

        // 配置 ASP.NET Core 集成
        options.UseAspNetCore()
               .EnableAuthorizationEndpointPassthrough()
               .EnableTokenEndpointPassthrough()
               .EnableUserInfoEndpointPassthrough()
               .EnableEndSessionEndpointPassthrough()
               .DisableTransportSecurityRequirement(); // 关键:禁用传输安全要求
    })
    .AddValidation(options =>
    {
        options.UseLocalServer();
        options.UseAspNetCore();
    });

// Add services to the container.
builder.Services.AddControllersWithViews(options =>
{
    // handle BusinessException
    options.Filters.Add<BusinessExceptionFilter>();
});
builder.Services.AddNginx();

builder.Services.AddCors(options => options.AddDefaultPolicy(policy =>
    policy.AllowAnyHeader()
          .AllowAnyMethod()
          .AllowAnyOrigin()));


// 配置客户端
builder.Services.AddHostedService<Worker>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

// Configure the HTTP request pipeline.
app.UseNginx();

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseCors();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

新增注册应用appliction,Scope范围域以及User用户信息代码Worker.cs

cs 复制代码
using BigData.Passport.Domain;
using BigData.Passport.Domain.Models;
using BigData.SSO.Passport.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using OpenIddict.Abstractions;
using Org.BouncyCastle.Asn1.Ocsp;
using System.Text.Json;
using static OpenIddict.Abstractions.OpenIddictConstants;

namespace BigData.SSO.Passport
{
    /// <summary>
    /// 客户端配置
    /// </summary>
    public class Worker : IHostedService
    {
        /// <summary>
        /// 
        /// </summary>
        private readonly IServiceProvider _serviceProvider;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="serviceProvider"></param>
        public Worker(IServiceProvider serviceProvider)
            => _serviceProvider = serviceProvider;

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            using var scope = _serviceProvider.CreateScope();

            var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            await context.Database.EnsureCreatedAsync();

            await CreateApplicationsAsync();
            await CreateScopesAsync();
            await CreateUserAsync();


            async Task CreateApplicationsAsync()
            {
                var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();

                if (await manager.FindByClientIdAsync("console_app") is null)
                {
                    await manager.CreateAsync(new OpenIddictApplicationDescriptor
                    {
                        ApplicationType = ApplicationTypes.Native,
                        ClientId = "console_app",
                        ClientType = ClientTypes.Public,
                        DisplayName = "console_app名称",
                        RedirectUris =
                        {
                            new Uri("http://localhost/")
                        },
                        Permissions =
                        {
                            Permissions.Endpoints.Authorization,
                            Permissions.Endpoints.Token,

                            Permissions.GrantTypes.AuthorizationCode,

                            Permissions.ResponseTypes.Code,

                            Permissions.Scopes.Email,
                            Permissions.Scopes.Profile,
                            Permissions.Scopes.Roles,
                            Permissions.Prefixes.Scope + "api1",
                            Permissions.Prefixes.Scope + "api2"
                        }
                    });
                }

                if (await manager.FindByClientIdAsync("spa") is null)
                {
                    await manager.CreateAsync(new OpenIddictApplicationDescriptor
                    {
                        ClientId = "spa",
                        //ClientSecret = "4cb68986-a64f-11f0-9a7b-005056927c86",
                        ClientType = ClientTypes.Public,
                        DisplayName = "测试",
                        // 关键配置:设置同意类型为隐式同意
                        ConsentType = ConsentTypes.Implicit,
                        // 登入重定向 URI
                        RedirectUris =
                        {
                            new Uri("https://xxx/signin-callback"),
                            new Uri("https://xxx/silent-renew.html") // 静默续订
                        },
                        // 退出后重定向 URI
                        PostLogoutRedirectUris =
                        {
                            new Uri("https://xxx/signout-callback"),
                        },
                        Permissions =
                        {
                            Permissions.Endpoints.Authorization,
                            Permissions.Endpoints.EndSession,
                            Permissions.Endpoints.Token,

                            Permissions.GrantTypes.AuthorizationCode,
                            Permissions.GrantTypes.RefreshToken,

                            Permissions.ResponseTypes.Code,

                            Permissions.Scopes.Email,
                            Permissions.Scopes.Profile,
                            Permissions.Scopes.Roles,
                            Permissions.Scopes.Phone,
                            Permissions.Scopes.Address,
                            Permissions.Prefixes.Scope + "api1"
                        },
                        Requirements =
                        {
                            Requirements.Features.ProofKeyForCodeExchange,
                        },
                    });
                }

                if (await manager.FindByClientIdAsync("web") is null)
                {
                    await manager.CreateAsync(new OpenIddictApplicationDescriptor
                    {
                        ApplicationType = ApplicationTypes.Web,
                        ClientId = "web",
                        ClientSecret = "400fe3d8-a64f-11f0-9a7b-005056927c86",
                        //ClientType = ClientTypes.Public,
                        DisplayName = "web前端",
                        // 关键配置:设置同意类型为隐式同意
                        ConsentType = ConsentTypes.Implicit,
                        // 登入重定向 URI
                        RedirectUris =
                        {
                            new Uri("https://xxx/signin-callback"),
                            new Uri("https://xxx/silent-renew.html") // 静默续订
                        },
                        // 退出后重定向 URI
                        PostLogoutRedirectUris =
                        {
                            new Uri("https://xxx/signout-callback"),
                        },
                        Permissions =
                        {
                            Permissions.Endpoints.Authorization,
                            Permissions.Endpoints.EndSession,
                            Permissions.Endpoints.Token,

                            Permissions.GrantTypes.AuthorizationCode,
                            Permissions.GrantTypes.ClientCredentials,
                            Permissions.GrantTypes.Password,
                            Permissions.GrantTypes.RefreshToken,

                            Permissions.ResponseTypes.Code,

                            Permissions.Scopes.Email,
                            Permissions.Scopes.Profile,
                            Permissions.Scopes.Roles,
                            Permissions.Scopes.Phone,
                            Permissions.Scopes.Address,
                            Permissions.Prefixes.Scope + "api1"
                        },
                        Requirements =
                        {
                            Requirements.Features.ProofKeyForCodeExchange,
                        },
                    });
                }

                if (await manager.FindByClientIdAsync("client1") is null)
                {
                    await manager.CreateAsync(new OpenIddictApplicationDescriptor
                    {
                        ClientId = "client1",
                        ClientSecret = "846B62D0-DEF9-4215-A99D-86E6B8DAB342",
                        DisplayName = "第一个应用client1",
                        Permissions =
                        {
                            Permissions.Endpoints.Introspection,
                            Permissions.Endpoints.Token,

                            Permissions.GrantTypes.ClientCredentials,
                            Permissions.GrantTypes.Password,

                            Permissions.Scopes.Email,
                            Permissions.Scopes.Profile,
                            Permissions.Scopes.Roles,
                            Permissions.Scopes.Phone,
                            Permissions.Scopes.Address,
                            Permissions.Prefixes.Scope + "api1",
                        }
                    });
                }

                // Note: no client registration is created for resource_server_2
                // as it uses local token validation instead of introspection.
            }

            async Task CreateScopesAsync()
            {
                var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictScopeManager>();

                if (await manager.FindByNameAsync("api1") is null)
                {
                    await manager.CreateAsync(new OpenIddictScopeDescriptor
                    {
                        Name = "api1",
                        Resources =
                        {
                            "resource_server_1"
                        }
                    });
                }

                if (await manager.FindByNameAsync("api2") is null)
                {
                    await manager.CreateAsync(new OpenIddictScopeDescriptor
                    {
                        Name = "api2",
                        Resources =
                        {
                            "resource_server_2"
                        }
                    });
                }
            }

            async Task CreateUserAsync()
            {
                var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
                if (await userManager.FindByNameAsync("admin") is null)
                {
                    var applicationUser = new ApplicationUser
                    {
                        UserName = "admin",
                        NickName = "管理员",
                        Email = "admin@qq.com",
                        PhoneNumber = "1",
                        IsEnabled = true,
                        CreatedTime = DateTime.Now,
                    };
                    var result = await userManager.CreateAsync(applicationUser, "admin123456");
                }
            }

        }

        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    }
}

实现一个自定义的授权控制器AuthorizationController.cs

cs 复制代码
using BigData.SSO.Passport.Models;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using System.Collections.Immutable;
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
using Microsoft.IdentityModel.Tokens;
using BigData.SSO.Passport.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using System.Security.Principal;
using Polly;
using BigData.Passport.Domain.Models;

namespace BigData.SSO.Passport.Controllers
{
    public class AuthorizationController : Controller
    {
        private readonly ILogger<AuthorizationController> _logger;
        private readonly ApplicationDbContext _context;
        private readonly IOpenIddictApplicationManager _applicationManager;
        private readonly IOpenIddictAuthorizationManager _authorizationManager;
        private readonly IOpenIddictScopeManager _scopeManager;
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly UserManager<ApplicationUser> _userManager;
        // IAuthenticationSchemeProvider _schemeProvider;
        private readonly IUserService _userService;
        private const string applicationname = CookieAuthenticationDefaults.AuthenticationScheme;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="logger"></param>
        /// <param name="context"></param>
        /// <param name="applicationManager"></param>
        /// <param name="authorizationManager"></param>
        /// <param name="scopeManager"></param>
        /// <param name="signInManager"></param>
        /// <param name="userManager"></param>
        /// <param name="userService"></param>
        public AuthorizationController(ILogger<AuthorizationController> logger,
            ApplicationDbContext context,
            IOpenIddictApplicationManager applicationManager,
            IOpenIddictAuthorizationManager authorizationManager,
            IOpenIddictScopeManager scopeManager,
            SignInManager<ApplicationUser> signInManager,
            UserManager<ApplicationUser> userManager,
            IUserService userService
            )
        {
            _logger = logger;
            _context = context;
            _applicationManager = applicationManager;
            _authorizationManager = authorizationManager;
            _scopeManager = scopeManager;
            _signInManager = signInManager;
            _userManager = userManager;
            _userService = userService;
        }

        #region 授权端点的操作 指定路由 这一步自己处理
        /// <summary>
        /// 授权
        /// </summary>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException"></exception>
        [HttpGet("~/connect/authorize")]
        [HttpPost("~/connect/authorize")]
        [IgnoreAntiforgeryToken]
        public async Task<IActionResult> Authorize()
        {
            _logger.LogInformation("开始授权Authorize.");
            var request = HttpContext.GetOpenIddictServerRequest() ??
                throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

            // Try to retrieve the user principal stored in the authentication cookie and redirect
            // the user agent to the login page (or to an external provider) in the following cases:
            //
            //  - If the user principal can't be extracted or the cookie is too old.
            //  - If prompt=login was specified by the client application.
            //  - If max_age=0 was specified by the client application (max_age=0 is equivalent to prompt=login).
            //  - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough.
            //
            // For scenarios where the default authentication handler configured in the ASP.NET Core
            // authentication options shouldn't be used, a specific scheme can be specified here.
            var result = await HttpContext.AuthenticateAsync();
            if (result is not { Succeeded: true } ||
                ((request.HasPromptValue(PromptValues.Login) || request.MaxAge is 0 ||
                 (request.MaxAge is not null && result.Properties?.IssuedUtc is not null &&
                  DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) &&
                TempData["IgnoreAuthenticationChallenge"] is null or false))
            {
                // If the client application requested promptless authentication,
                // return an error indicating that the user is not logged in.
                if (request.HasPromptValue(PromptValues.None))
                {
                    return Forbid(
                        authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                        properties: new AuthenticationProperties(new Dictionary<string, string?>
                        {
                            [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
                            [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
                        }));
                }

                // To avoid endless login endpoint -> authorization endpoint redirects, a special temp data entry is
                // used to skip the challenge if the user agent has already been redirected to the login endpoint.
                //
                // Note: this flag doesn't guarantee that the user has accepted to re-authenticate. If such a guarantee
                // is needed, the existing authentication cookie MUST be deleted AND revoked (e.g using ASP.NET Core
                // Identity's security stamp feature with an extremely short revalidation time span) before triggering
                // a challenge to redirect the user agent to the login endpoint.
                TempData["IgnoreAuthenticationChallenge"] = true;

                // For scenarios where the default challenge handler configured in the ASP.NET Core
                // authentication options shouldn't be used, a specific scheme can be specified here.
                return Challenge(new AuthenticationProperties
                {
                    RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
                        Request.HasFormContentType ? Request.Form : Request.Query)
                });
            }

            // Retrieve the profile of the logged in user.
            var user = await _userManager.GetUserAsync(result.Principal) ??
                throw new InvalidOperationException("The user details cannot be retrieved.");

            // Retrieve the application details from the database.
            var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) ??
                throw new InvalidOperationException("Details concerning the calling client application cannot be found.");

            // Retrieve the permanent authorizations associated with the user and the calling client application.
            var authorizations = await _authorizationManager.FindAsync(
                subject: await _userManager.GetUserIdAsync(user),
                client: await _applicationManager.GetIdAsync(application),
                status: Statuses.Valid,
                type: AuthorizationTypes.Permanent,
                scopes: request.GetScopes()).ToListAsync();

            // 验证用户是否可以访问该客户端应用
            var userClientRel = await _context.CustomUserClientRel
                .Where(x => x.UserId == user.Id && x.ClientId == request.ClientId)
                .Where(x=>x.IsEnabled == true)
                .FirstOrDefaultAsync();
            if (userClientRel == null)
            {
                return Forbid(
                        authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                        properties: new AuthenticationProperties(new Dictionary<string, string?>
                        {
                            [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
                            [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "该用户没有访问该应用的权限."
                        }));
            }

            var consentType = await _applicationManager.GetConsentTypeAsync(application);
            switch (await _applicationManager.GetConsentTypeAsync(application))
            {
                // If the consent is external (e.g when authorizations are granted by a sysadmin),
                // immediately return an error if no authorization can be found in the database.
                case ConsentTypes.External when authorizations.Count is 0:
                    return Forbid(
                        authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                        properties: new AuthenticationProperties(new Dictionary<string, string?>
                        {
                            [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
                            [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
                                "The logged in user is not allowed to access this client application."
                        }));

                // If the consent is implicit or if an authorization was found,
                // return an authorization response without displaying the consent form.
                case ConsentTypes.Implicit:
                case ConsentTypes.External when authorizations.Count is not 0:
                case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPromptValue(PromptValues.Consent):
                    // Create the claims-based identity that will be used by OpenIddict to generate tokens.
                    //var identity = new ClaimsIdentity(
                    //    authenticationType: TokenValidationParameters.DefaultAuthenticationType,
                    //    nameType: Claims.Name,
                    //    roleType: Claims.Role);

                    //// Add the claims that will be persisted in the tokens.
                    //identity.SetClaim(Claims.Subject, user.Id)
                    //    .SetClaim(Claims.Email, user.Email)
                    //    .SetClaim(Claims.Name, user.UserName)
                    //    .SetClaim(Claims.Nickname, user.NickName)
                    //    //.SetClaim(Claims.PhoneNumber, user.PhoneNumber)
                    //    .SetClaims(Claims.Role, (await _userManager.GetRolesAsync(user)).ToImmutableArray());
                    // 设置声明
                    var identity = await this.SetClaims(user, request);

                    // Note: in this sample, the granted scopes match the requested scope
                    // but you may want to allow the user to uncheck specific scopes.
                    // For that, simply restrict the list of scopes before calling SetScopes.
                    identity.SetScopes(request.GetScopes());
                    identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());

                    // Automatically create a permanent authorization to avoid requiring explicit consent
                    // for future authorization or token requests containing the same scopes.
                    var authorization = authorizations.LastOrDefault();
                    authorization ??= await _authorizationManager.CreateAsync(
                        identity: identity,
                        subject: await _userManager.GetUserIdAsync(user),
                        client: (await _applicationManager.GetIdAsync(application))!,
                        type: AuthorizationTypes.Permanent,
                        scopes: identity.GetScopes());

                    identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
                    identity.SetDestinations(GetDestinations);

                    return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

                // At this point, no authorization was found in the database and an error must be returned
                // if the client application specified prompt=none in the authorization request.
                case ConsentTypes.Explicit when request.HasPromptValue(PromptValues.None):
                case ConsentTypes.Systematic when request.HasPromptValue(PromptValues.None):
                    return Forbid(
                        authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                        properties: new AuthenticationProperties(new Dictionary<string, string?>
                        {
                            [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
                            [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
                                "Interactive user consent is required."
                        }));

                // In every other case, render the consent form.
                default:
                    return View(new AuthorizeViewModel
                    {
                        ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application),
                        Scope = request.Scope
                    });
            }
        }

        /// <summary>
        /// 同意授权
        /// </summary>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException"></exception>
        [Authorize]
        [HttpPost("~/connect/authorize")]
        [HttpPost("~/connect/authorize/accept")]
        public async Task<IActionResult> Accept()
        {
            _logger.LogInformation("同意授权Authorize.Accept");
            // 手动创建 OpenIddict 请求
            var request = new OpenIddictRequest(Request.Form);

            // 方法 2: 或者直接从 HttpContext 获取(确保参数正确传递)
            var directRequest = HttpContext.GetOpenIddictServerRequest();
            if (directRequest != null)
            {
                request = directRequest;
            }

            if (request == null)
            {
                return BadRequest("无法处理授权请求");
            }

            // var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

            // Retrieve the profile of the logged in user.
            var user = await _userManager.GetUserAsync(User) ??
                throw new InvalidOperationException("The user details cannot be retrieved.");

            // Retrieve the application details from the database.
            var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) ??
                throw new InvalidOperationException("Details concerning the calling client application cannot be found.");

            // Retrieve the permanent authorizations associated with the user and the calling client application.
            var authorizations = await _authorizationManager.FindAsync(
                subject: await _userManager.GetUserIdAsync(user),
                client: await _applicationManager.GetIdAsync(application),
                status: Statuses.Valid,
                type: AuthorizationTypes.Permanent,
                scopes: request.GetScopes()).ToListAsync();

            // Note: the same check is already made in the other action but is repeated
            // here to ensure a malicious user can't abuse this POST-only endpoint and
            // force it to return a valid response without the external authorization.
            if (authorizations.Count is 0 && await _applicationManager.HasConsentTypeAsync(application, ConsentTypes.External))
            {
                return Forbid(
                    authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                    properties: new AuthenticationProperties(new Dictionary<string, string?>
                    {
                        [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
                        [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
                            "The logged in user is not allowed to access this client application."
                    }));
            }

            // Create the claims-based identity that will be used by OpenIddict to generate tokens.
            //var identity = new ClaimsIdentity(
            //    authenticationType: TokenValidationParameters.DefaultAuthenticationType,
            //    nameType: Claims.Name,
            //    roleType: Claims.Role);

            //// Add the claims that will be persisted in the tokens.
            //identity.SetClaim(Claims.Subject, user.Id)
            //    .SetClaim(Claims.Email, user.Email)
            //    .SetClaim(Claims.Name, user.UserName)
            //    .SetClaim(Claims.Nickname, user.NickName)
            //    //.SetClaim(Claims.PhoneNumber, user.PhoneNumber)
            //    .SetClaims(Claims.Role, (await _userManager.GetRolesAsync(user)).ToImmutableArray());
            // 设置声明
            var identity = await this.SetClaims(user, request);

            // Note: in this sample, the granted scopes match the requested scope
            // but you may want to allow the user to uncheck specific scopes.
            // For that, simply restrict the list of scopes before calling SetScopes.
            identity.SetScopes(request.GetScopes());
            identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());

            // Automatically create a permanent authorization to avoid requiring explicit consent
            // for future authorization or token requests containing the same scopes.
            var authorization = authorizations.LastOrDefault();
            authorization ??= await _authorizationManager.CreateAsync(
                identity: identity,
                subject: await _userManager.GetUserIdAsync(user),
                client: (await _applicationManager.GetIdAsync(application))!,
                type: AuthorizationTypes.Permanent,
                scopes: identity.GetScopes());

            identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
            identity.SetDestinations(GetDestinations);

            // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
            return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            // return Authorize(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        /// <summary>
        /// 拒绝授权
        /// </summary>
        /// <returns></returns>
        [Authorize]
        [HttpPost("~/connect/authorize")]
        [HttpPost("~/connect/authorize/deny")]
        // Notify OpenIddict that the authorization grant has been denied by the resource owner
        // to redirect the user agent to the client application using the appropriate response_mode.
        public IActionResult Deny()
        {
            _logger.LogInformation("拒绝授权Authorize.Deny");
            return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }
        #endregion

        #region 获取Token地址 包括所有方式
        /// <summary>
        /// 可以指定不同的获取Token的客户端逻辑
        /// </summary>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException"></exception>
        [HttpPost("~/connect/token"), Produces("application/json")]
        public async Task<IActionResult> Exchange()
        {
            try
            {
                _logger.LogInformation("开始处理令牌请求");
                var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("OIDC请求不存在.");
                if (request == null)
                {
                    _logger.LogError("无法获取 OpenIddict 请求");
                    return BadRequest(new OpenIddictResponse
                    {
                        Error = OpenIddictConstants.Errors.InvalidRequest,
                        ErrorDescription = "无效的请求"
                    });
                }

                _logger.LogInformation("授权类型: {GrantType}", request.GrantType);

                // 根据授权类型路由到不同的处理方法
                return request.GrantType switch
                {
                    GrantTypes.AuthorizationCode => await HandleAuthorizationCodeGrantTypeAsync(request),
                    GrantTypes.Password => await HandlePasswordGrantTypeAsync(request),
                    GrantTypes.ClientCredentials => await HandleClientCredentialsGrantTypeAsync(request),
                    GrantTypes.RefreshToken => await HandleRefreshTokenGrantTypeAsync(request),
                    _ => HandleUnsupportedGrantType(request)
                };
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "处理令牌请求时发生异常");
                return BadRequest(new OpenIddictResponse
                {
                    Error = OpenIddictConstants.Errors.ServerError,
                    ErrorDescription = "服务器内部错误"
                });
            }
        }

        /// <summary>
        /// 处理授权码流程
        /// </summary>
        private async Task<IActionResult> HandleAuthorizationCodeGrantTypeAsync(OpenIddictRequest request)
        {
            _logger.LogInformation("处理授权码流程,客户端:{clientId}", request.ClientId);

            // Retrieve the claims principal stored in the authorization code/refresh token.
            var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

            // Retrieve the user profile corresponding to the authorization code/refresh token.
            var user = await _userManager.FindByIdAsync(result.Principal!.GetClaim(Claims.Subject)!);
            if (user is null)
            {
                return Forbid(
                    authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                    properties: new AuthenticationProperties(new Dictionary<string, string?>
                    {
                        [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                        [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
                    }));
            }

            // Ensure the user is still allowed to sign in.
            if (!await _signInManager.CanSignInAsync(user))
            {
                return Forbid(
                    authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                    properties: new AuthenticationProperties(new Dictionary<string, string?>
                    {
                        [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                        [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
                    }));
            }

            //var identity = new ClaimsIdentity(result.Principal!.Claims,
            //    authenticationType: TokenValidationParameters.DefaultAuthenticationType,
            //    nameType: Claims.Name,
            //    roleType: Claims.Role);

            //// Override the user claims present in the principal in case they
            //// changed since the authorization code/refresh token was issued.
            //identity.SetClaim(Claims.Subject, user.Id)
            //    .SetClaim(Claims.Email, user.Email)
            //    .SetClaim(Claims.Name, user.UserName)
            //    .SetClaim(Claims.Nickname, user.NickName)
            //    //.SetClaim(Claims.PhoneNumber, user.PhoneNumber)
            //    .SetClaims(Claims.Role, (await _userManager.GetRolesAsync(user)).ToImmutableArray());
            var identity = await this.SetClaims(user, request);

            identity.SetDestinations(GetDestinations);

            _logger.LogInformation("授权码流程成功: {UserId}", user.Id);
            // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
            return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        /// <summary>
        /// 处理密码授权流程
        /// </summary>
        private async Task<IActionResult> HandlePasswordGrantTypeAsync(OpenIddictRequest request)
        {
            _logger.LogInformation("处理密码授权流程,客户端:{clientId},用户名: {Username}", request.ClientId, request.Username);

            // 验证必需参数
            if (string.IsNullOrEmpty(request.Username) || string.IsNullOrEmpty(request.Password))
            {
                _logger.LogWarning("用户名或密码为空");
                return BadRequest(new OpenIddictResponse
                {
                    Error = OpenIddictConstants.Errors.InvalidRequest,
                    ErrorDescription = "用户名和密码不能为空"
                });
            }

            // 查找用户
            var user = await _userManager.FindByNameAsync(request.Username);
            if (user == null)
            {
                _logger.LogWarning("用户不存在: {Username}", request.Username);
                return Unauthorized(new OpenIddictResponse
                {
                    Error = OpenIddictConstants.Errors.InvalidGrant,
                    ErrorDescription = "用户名或密码不正确"
                });
            }

            // 验证密码
            var passwordValid = await _userManager.CheckPasswordAsync(user, request.Password);
            if (!passwordValid)
            {
                _logger.LogWarning("密码验证失败: {Username}", request.Username);
                return Unauthorized(new OpenIddictResponse
                {
                    Error = OpenIddictConstants.Errors.InvalidGrant,
                    ErrorDescription = "用户名或密码不正确"
                });
            }

            // 设置声明
            var identity = await this.SetClaims(user, request);

            // Set the list of scopes granted to the client application.
            identity.SetScopes(request.GetScopes());
            identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
            identity.SetDestinations(GetDestinations);

            _logger.LogInformation("密码授权成功: {Username}", request.Username);
            return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        /// <summary>
        /// 处理客户端凭据流程
        /// </summary>
        private async Task<IActionResult> HandleClientCredentialsGrantTypeAsync(OpenIddictRequest request)
        {
            _logger.LogInformation("处理客户端凭据流程,客户端:{clientId}", request.ClientId);

            var application = await _applicationManager.FindByClientIdAsync(request.ClientId!);
            if (application == null)
            {
                throw new InvalidOperationException("The application details cannot be found in the database.");
            }

            // Create the claims-based identity that will be used by OpenIddict to generate tokens.
            var identity = new ClaimsIdentity(
                authenticationType: TokenValidationParameters.DefaultAuthenticationType,
                nameType: Claims.Name,
                roleType: Claims.Role);

            // Add the claims that will be persisted in the tokens (use the client_id as the subject identifier).
            identity.SetClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application));
            identity.SetClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application));

            // Note: In the original OAuth 2.0 specification, the client credentials grant
            // doesn't return an identity token, which is an OpenID Connect concept.
            //
            // As a non-standardized extension, OpenIddict allows returning an id_token
            // to convey information about the client application when the "openid" scope
            // is granted (i.e specified when calling principal.SetScopes()). When the "openid"
            // scope is not explicitly set, no identity token is returned to the client application.

            // Set the list of scopes granted to the client application in access_token.
            identity.SetScopes(request.GetScopes());
            identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
            identity.SetDestinations(GetDestinations);

            _logger.LogInformation("客户端凭据授权成功: {ClientId}", request.ClientId);
            return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        /// <summary>
        /// 处理刷新令牌流程
        /// </summary>
        private async Task<IActionResult> HandleRefreshTokenGrantTypeAsync(OpenIddictRequest request)
        {
            _logger.LogInformation("处理刷新令牌流程");

            // Retrieve the claims principal stored in the refresh token.
            var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

            // Retrieve the user profile corresponding to the refresh token.
            var user = await _userManager.FindByIdAsync(result.Principal!.GetClaim(Claims.Subject)!);
            if (user == null)
            {
                var properties = new AuthenticationProperties(new Dictionary<string, string?>
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                    [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The refresh token is no longer valid."
                });

                return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }

            // Ensure the user is still allowed to sign in.
            if (!await _signInManager.CanSignInAsync(user))
            {
                var properties = new AuthenticationProperties(new Dictionary<string, string?>
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                    [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
                });

                return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }

            //var identity = new ClaimsIdentity(result.Principal!.Claims,
            //    authenticationType: TokenValidationParameters.DefaultAuthenticationType,
            //    nameType: Claims.Name,
            //    roleType: Claims.Role);

            //// Override the user claims present in the principal in case they changed since the refresh token was issued.
            //identity.SetClaim(Claims.Subject, user.Id)
            //    .SetClaim(Claims.Email, user.Email)
            //    .SetClaim(Claims.Name, user.UserName)
            //    .SetClaim(Claims.Nickname, user.NickName)
            //    //.SetClaim(Claims.PhoneNumber, user.PhoneNumber)
            //    .SetClaims(Claims.Role, (await _userManager.GetRolesAsync(user)).ToImmutableArray());
            // 设置声明
            var identity = await this.SetClaims(user, request);

            identity.SetDestinations(GetDestinations);

            _logger.LogInformation("刷新令牌成功: {UserId}", user.Id);
            return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        /// <summary>
        /// 处理不支持的授权类型
        /// </summary>
        private IActionResult HandleUnsupportedGrantType(OpenIddictRequest request)
        {
            _logger.LogWarning("不支持的授权类型: {GrantType}", request.GrantType);
            return BadRequest(new OpenIddictResponse
            {
                Error = OpenIddictConstants.Errors.UnsupportedGrantType,
                ErrorDescription = $"不支持的授权类型: {request.GrantType}"
            });
        }

        /// <summary>
        /// 设置声明
        /// </summary>
        /// <param name="user"></param>
        /// <param name="request"></param>
        /// <returns></returns>
        private async Task<ClaimsIdentity> SetClaims(ApplicationUser user, OpenIddictRequest request)
        {
            // Create the claims-based identity that will be used by OpenIddict to generate tokens.
            var identity = new ClaimsIdentity(
                authenticationType: TokenValidationParameters.DefaultAuthenticationType,
                nameType: Claims.Name,
                roleType: Claims.Role);

            // 获取请求的范围
            var scopes = request.GetScopes();

            // Add the claims that will be persisted in the tokens.
            identity.SetClaim(Claims.Subject, user.Id)
                .SetClaim(Claims.Name, user.UserName);

            // 根据范围添加声明
            if (scopes.Contains(Scopes.Profile))
            {
                identity.SetClaim(Claims.Nickname, user.NickName);
            }

            if (scopes.Contains(Scopes.Email))
            {
                identity.SetClaim(Claims.Email, user.Email);
            }
            // 只有明确请求 roles 范围时才包含角色
            if (scopes.Contains(Scopes.Roles))
            {
                identity.SetClaims(Claims.Role, (await _userManager.GetRolesAsync(user)).ToImmutableArray());
            }
            return identity;
        }

        /// <summary>
        /// 退出
        /// </summary>
        /// <param name="claim"></param>
        /// <returns></returns>
        private static IEnumerable<string> GetDestinations(Claim claim)
        {
            // Note: by default, claims are NOT automatically included in the access and identity tokens.
            // To allow OpenIddict to serialize them, you must attach them a destination, that specifies
            // whether they should be included in access tokens, in identity tokens or in both.

            switch (claim.Type)
            {
                case Claims.Name or Claims.PreferredUsername:
                    yield return Destinations.AccessToken;

                    if (claim.Subject!.HasScope(Scopes.Profile))
                        yield return Destinations.IdentityToken;

                    yield break;

                case Claims.Email:
                    yield return Destinations.AccessToken;

                    if (claim.Subject!.HasScope(Scopes.Email))
                        yield return Destinations.IdentityToken;

                    yield break;

                case Claims.Role:
                    yield return Destinations.AccessToken;

                    if (claim.Subject!.HasScope(Scopes.Roles))
                        yield return Destinations.IdentityToken;

                    yield break;

                // Never include the security stamp in the access and identity tokens, as it's a secret value.
                case "AspNet.Identity.SecurityStamp": yield break;

                default:
                    yield return Destinations.AccessToken;
                    yield break;
            }
        }
        #endregion

        /// <summary>
        /// 前端调用-退出
        /// </summary>
        /// <returns></returns>
        [HttpGet("~/connect/logout")]
        public async Task<IActionResult> Logout() 
        {
            _logger.LogInformation("用户退出开始.");
            // Ask ASP.NET Core Identity to delete the local and external cookies created
            // when the user agent is redirected from the external identity provider
            // after a successful authentication flow (e.g Google or Facebook).
            await _signInManager.SignOutAsync();

            _logger.LogInformation("用户退出结束.");
            // Returning a SignOutResult will ask OpenIddict to redirect the user agent
            // to the post_logout_redirect_uri specified by the client application or to
            // the RedirectUri specified in the authentication properties if none was set.
            return SignOut(
                authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                properties: new AuthenticationProperties
                {
                    RedirectUri = Url.Content("~/")
                });
        }

        /// <summary>
        /// 前端调用-退出
        /// </summary>
        /// <returns></returns>
        [ActionName(nameof(Logout)), HttpPost("~/connect/logout"), ValidateAntiForgeryToken]
        public async Task<IActionResult> LogoutPost()
        {
            _logger.LogInformation("用户退出开始.");
            // Ask ASP.NET Core Identity to delete the local and external cookies created
            // when the user agent is redirected from the external identity provider
            // after a successful authentication flow (e.g Google or Facebook).
            await _signInManager.SignOutAsync();

            _logger.LogInformation("用户退出结束.");
            // Returning a SignOutResult will ask OpenIddict to redirect the user agent
            // to the post_logout_redirect_uri specified by the client application or to
            // the RedirectUri specified in the authentication properties if none was set.
            return SignOut(
                authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                properties: new AuthenticationProperties
                {
                    RedirectUri = Url.Content("~/")
                });
        }

        /// <summary>
        /// 获取用户信息
        /// </summary>
        /// <returns></returns>
        [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
        [HttpGet("~/connect/userinfo"), HttpPost("~/connect/userinfo"), Produces("application/json")]
        public async Task<IActionResult> Userinfo()
        {
            _logger.LogInformation("获取用户信息开始.");
            var user = await _userManager.FindByIdAsync(User.GetClaim(Claims.Subject)!);
            if (user == null)
            {
                return Challenge(
                    authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                    properties: new AuthenticationProperties(new Dictionary<string, string?>
                    {
                        [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken,
                        [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
                            "The specified access token is bound to an account that no longer exists."
                    }));
            }
            _logger.LogInformation("获取用户信息,{userName}.", user.UserName);
            var claims = new Dictionary<string, object?>(StringComparer.Ordinal)
            {
                // Note: the "sub" claim is a mandatory claim and must be included in the JSON response.
                [Claims.Subject] = user.Id,
                [Claims.Name] = user.UserName,
                [Claims.Nickname] = user.NickName,
            };

            if (User.HasScope(Scopes.Email))
            {
                claims[Claims.Email] = user.Email;
            }

            if (User.HasScope(Scopes.Phone))
            {
                claims[Claims.PhoneNumber] = user.PhoneNumber;
            }

            if (User.HasScope(Scopes.Roles))
            {
                claims[Claims.Role] = await _userManager.GetRolesAsync(user);
            }

            // Note: the complete list of standard claims supported by the OpenID Connect specification
            // can be found here: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
            _logger.LogInformation("获取用户信息结束,用户信息:{claims}", claims);
            return Ok(claims);
        }
    }
}

新增配置appsettings.Development.json

cs 复制代码
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=openiddict;User=root;Password=123456;SSL Mode=None;"
  },
  "IdentityServer": {
    "Authority": "https://xxx.com/passport"
  }

搭建资源API服务

新增代码入口程序

cs 复制代码
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Validation.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddOpenIddict()
    .AddValidation(options =>
    {
        // Note: the validation handler uses OpenID Connect discovery
        // to retrieve the issuer signing keys used to validate tokens.
        options.SetIssuer(configuration["IdentityServer:Authority"]);
        options.AddAudiences(configuration["IdentityServer:Audience"]);


        // Register the encryption credentials. This sample uses a symmetric
        // encryption key that is shared between the server and the API project.
        //
        // Note: in a real world application, this encryption key should be
        // stored in a safe place (e.g in Azure KeyVault, stored as a secret).
        // options.AddEncryptionKey(new SymmetricSecurityKey(Convert.FromBase64String("DRjd/GnduI3Efzen9V9BvbNUfc/VKgXltV7Kbk9sMkY=")));
        //options.AddEncryptionKey(new SymmetricSecurityKey(Convert.FromBase64String("DRjd/GnduI3Efzen9V9BvbNUfc/VKgXltV7Kbk9sMkY=")));

        

        // Register the System.Net.Http integration.
        options.UseSystemNetHttp();

        // Register the ASP.NET Core host.
        options.UseAspNetCore();
    });

builder.Services.AddCors(options => options.AddDefaultPolicy(policy =>
    policy.AllowAnyHeader()
          .AllowAnyMethod()
          .AllowAnyOrigin()));

builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
builder.Services.AddAuthorization(options =>
{
    // 定义 ITAdminOnly 策略:需要 role=admin AND name=zhangsan
    options.AddPolicy("ZhangSanAdminOnly", policy =>
    {
        policy.RequireAuthenticatedUser();  // 必须认证
        policy.RequireRole("admin");        // 必须拥有 Admin 角色
        policy.RequireClaim("name", "zhangsan");  // 必须拥有 name=zhangsan 声明
    });

    // 可选:其他相关策略
    options.AddPolicy("AdminOnly", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireRole("admin"); // 必须拥有 Admin 角色
    });

    options.AddPolicy("ITDepartment", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("department", "IT");// 必须拥有 department=IT 声明
    });
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseCors();
app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

新增测试控制器WeatherForecastController.cs

cs 复制代码
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace BigData.Sample.Api1.Controllers
{
    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        /// <summary>
        /// 需要认证后才能访问
        /// </summary>
        /// <returns></returns>
        [HttpGet("Get")]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }

        /// <summary>
        /// 基于policy授权的token才可以访问
        /// </summary>
        /// <returns></returns>
        [Authorize("ZhangSanAdminOnly")]
        [HttpGet("GetTime")]
        public string GetTime()
        {
            return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        }

        /// <summary>
        /// 基于角色的授权的token才可以访问
        /// </summary>
        /// <returns></returns>
        [Authorize(Roles ="admin")]
        [HttpGet("GetTime1")]
        public string GetTime1()
        {
            return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        }
    }
}

新增配置appsettings.Development.json

cs 复制代码
{
  "IdentityServer": {
    "Authority": "https://xxx.com/passport",
    "Audience": "resource_server_1"
  }
}

Postman测试结果

  1. 使用password方式获取token

  2. 使用token访问资源api

总结:

  1. 一个轻量级、易用、高性能的开源替代品。它更"现代化"和"敏捷",非常适合 ASP.NET Core 应用、API 保护和移动/SPA 应用集成。
  2. 完全开源免费
相关推荐
黑马金牌编程3 小时前
Jenkins的Linux与window部署方式
linux·运维·windows·jenkins·持续集成·cicd
web安全工具库3 小时前
告别刀耕火种:用 Makefile 自动化 C 语言项目编译
linux·运维·c语言·开发语言·数据库·算法·自动化
金纬软件13 小时前
电脑监控软件有哪些?企业监控软件应该怎么选?
大数据·运维
DechinPhy3 小时前
Ubuntu挂载新硬盘
linux·运维·服务器·ubuntu
lht6319356124 小时前
Ubuntu Server 系统安装图形界面远程工具(RDP)
linux·运维·ubuntu
云计算练习生4 小时前
linux shell编程实战 02 变量与交互式输入
linux·运维·shell编程·shell 变量
Dovis(誓平步青云)4 小时前
《简易制作 Linux Shell:详细分析原理、设计与实践》
linux·运维·服务器
爱宇阳5 小时前
Linux 教程:如何查看服务器当前目录中的文件
linux·运维·github
wheeldown5 小时前
【Linux】Linux 进程通信:System V 共享内存(最快方案)C++ 封装实战 + 通信案例,4 类经典 Bug 快速修复
linux·运维·服务器·开发语言