类别:技术积累 / 日期:2025-10-27 / 浏览:23 / 评论:0

1、使用现有或新建一个"ASP.NET Core Empty"等相关类型的项目,在Nuget Package Manager中引用OpenIddict.AspNetCore、OpenIddict.EntityFrameworkCore两个Nuget包,或在*.csproj项目文件中新增如下行:

  <ItemGroup>
    <PackageReference Include="OpenIddict.AspNetCore" Version="7.1.0" />
    <PackageReference Include="OpenIddict.EntityFrameworkCore" Version="7.1.0" />
  </ItemGroup>

OpenIddict.AspNetCore是OpenIddict的核心类库,OpenIddict.EntityFrameworkCore是OpenIddict的EF Core支持类库。

2、自定义一个OpenIddict注册相关类:

/// <summary>
/// 
/// </summary>
public class Shared
{
    /// <summary>
    /// 
    /// </summary>
    public static string UnsupportedDatabaseTypeError { get; set; } = "";
}

/// <summary>
/// OIDC Options
/// </summary>
public class OIDCOptions
{
    /// <summary>
    /// Database Type
    /// </summary>
    public string DatabaseType { get; set; } = "";
    /// <summary>
    /// Connection String
    /// </summary>
    public string ConnectionString { get; set; } = "";
    /// <summary>
    /// Token EndpointUris
    /// </summary>
    public string TokenEndpointUris { get; set; } = "connect/token";
    /// <summary>
    /// Authorizatio nEndpointUris
    /// </summary>
    public string AuthorizationEndpointUris { get; set; } = "connect/authorize";
    /// <summary>
    /// UserInfo EndpointUris
    /// </summary>
    public string UserInfoEndpointUris { get; set; } = "connect/userinfo";
    /// <summary>
    /// EndSession EndpointUri
    /// </summary>
    public string EndSessionEndpointUri { get; set; } = "connect/logout";
    /// <summary>
    /// Action to Configure DbContextOptions
    /// </summary>
    public Action<DbContextOptionsBuilder>? ActionDbContextOptions { get; set; }
}

/// <summary>
/// 
/// </summary>
public class OpenIddictDbContext : IdentityDbContext
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="options"></param>
    public OpenIddictDbContext(DbContextOptions<OpenIddictDbContext> options)
        : base(options)
    {
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="builder"></param>
    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }
}

/// <summary>
/// Interface IRegisterable 
/// </summary>
public interface IOIDCRegisterable
{
    /// <summary>
    /// Register OIDC
    /// </summary>
    /// <param name="services">Service Collection</param>
    void Register(IServiceCollection services);
}

/// <summary>
/// OpenIddict Register
/// </summary>
public class OpenIddictRegister: Singleton<OpenIddictRegister>, IOIDCRegisterable
{
    /// <summary>
    /// Register OpenIddict
    /// </summary>
    /// <param name="services"></param>
    /// <exception cref="InvalidOperationException"></exception>
    public void Register(IServiceCollection services)
    {
        OIDCOptions oidcOptions;
        IStringLocalizer<Shared> SL;
        using (var serviceProvider = services.BuildServiceProvider())
        {
            oidcOptions = serviceProvider.GetRequiredService<IOptions<OIDCOptions>>().Value;
            SL = serviceProvider.GetRequiredService<IStringLocalizer<Shared>>();
        }
        services.AddDbContext<OpenIddictDbContext>(options =>
        {
            if (null != oidcOptions.ActionDbContextOptions)
            {
                oidcOptions.ActionDbContextOptions(options);
            }
            else
            {
                switch (oidcOptions.DatabaseType)
                {
                    case "SqlServer":
                        // Configure Entity Framework Core to use Microsoft SQL Server.
                        options.UseSqlServer(oidcOptions.ConnectionString);
                        break;
                    case "MySQL":
                        // Configure Entity Framework Core to use MySQL.
                        options.UseMySQL(oidcOptions.ConnectionString);
                        break;
                    case "Sqlite":
                        // Configure Entity Framework Core to use SQLite.
                        options.UseSqlite(oidcOptions.ConnectionString);
                        break;
                    default:
                        throw new InvalidOperationException(SL[nameof(Shared.UnsupportedDatabaseTypeError), oidcOptions.DatabaseType]);
                }
            }

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

        services.AddOpenIddict()
            // Register the OpenIddict core components.
            .AddCore(options =>
            {
                // Configure OpenIddict to use the Entity Framework Core stores and models.
                // Note: call ReplaceDefaultEntities() to replace the default entities.
                options.UseEntityFrameworkCore()
                        .UseDbContext<OpenIddictDbContext>();
            })
            // Register the OpenIddict server components.
            .AddServer(options =>
            {
                // Enable the token endpoint.
                options.SetTokenEndpointUris(oidcOptions.TokenEndpointUris);
                options.SetAuthorizationEndpointUris(oidcOptions.AuthorizationEndpointUris);
                options.SetUserInfoEndpointUris(oidcOptions.UserInfoEndpointUris);
                options.SetEndSessionEndpointUris(oidcOptions.EndSessionEndpointUri);

                // Register the signing and encryption credentials.
                options.AddDevelopmentEncryptionCertificate()
                        .AddDevelopmentSigningCertificate();

                // Enable the client credentials flow.
                options.AllowClientCredentialsFlow();
                options.AllowAuthorizationCodeFlow();
                options.AllowRefreshTokenFlow();

                // Register the ASP.NET Core host and configure the ASP.NET Core options.
                options.UseAspNetCore()
                        .EnableTokenEndpointPassthrough()
                        .EnableAuthorizationEndpointPassthrough()
                        .EnableUserInfoEndpointPassthrough()
                        .EnableEndSessionEndpointPassthrough()
                        .DisableTransportSecurityRequirement();
            })
            // Register the OpenIddict validation components.
            .AddValidation(options =>
            {
                // Import the configuration from the local OpenIddict server instance.
                options.UseLocalServer();

                // Register the ASP.NET Core host.
                options.UseAspNetCore();
            });
           
        services.AddDefaultIdentity<IdentityUser>(options =>
        {
            options.SignIn.RequireConfirmedAccount = true;
            options.SignIn.RequireConfirmedEmail = true;

            options.User.RequireUniqueEmail = true;

            options.Password.RequiredUniqueChars = 6;
            options.Password.RequiredLength = 8;
        })
            .AddEntityFrameworkStores<OpenIddictDbContext>();

        services.AddScoped<IOIDCExecutable, OpenIddictExecutor>();
    }
}

/// <summary>
/// 
/// </summary>
public static class OpenIddictRegisterHelper
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="services"></param>
    /// <param name="optionsBuidlerAction"></param>
    public static void AddOIDC(this IServiceCollection services, Action<OIDCOptions> optionsAction)
    {
        services.Configure(optionsAction);
        OpenIddictRegister.Instance.Register(services);
    }
}

/// <summary>
/// Interface IOIDCExecutable 
/// </summary>
public interface IOIDCExecutable
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="baseController"></param>
    /// <returns></returns>
    Task<IActionResult> Authorize(Controller baseController);

    /// <summary>
    /// 
    /// </summary>
    /// <param name="baseController"></param>
    /// <returns></returns>
    Task<IActionResult> GetUserInfo(Controller baseController);

    /// <summary>
    /// 
    /// </summary>
    /// <param name="baseController"></param>
    /// <returns></returns>
    Task<IActionResult> Logout(Controller baseController);

    /// <summary>
    /// 
    /// </summary>
    /// <param name="baseController"></param>
    /// <returns></returns>
    Task<IActionResult> GetToken(Controller baseController);
}

/// <summary>
/// OpenIddict Executor
/// </summary>
public class OpenIddictExecutor : IOIDCExecutable
{
    /// <summary>
    /// 
    /// </summary>
    private readonly IOpenIddictApplicationManager _applicationManager;
    /// <summary>
    /// 
    /// </summary>
    private readonly UserManager<IdentityUser> _userManager;
    /// <summary>
    /// 
    /// </summary>
    private readonly SignInManager<IdentityUser> _signInManager;

    /// <summary>
    /// 
    /// </summary>
    /// <param name="applicationManager"></param>
    /// <param name="userManager"></param>
    /// <param name="signInManager"></param>
    public OpenIddictExecutor(IOpenIddictApplicationManager applicationManager,
        UserManager<IdentityUser> userManager,
        SignInManager<IdentityUser> signInManager)
    {
        _applicationManager = applicationManager;
        _userManager = userManager;
        _signInManager = signInManager;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="baseController"></param>
    /// <returns></returns>
    /// <exception cref="NotImplementedException"></exception>
    public async Task<IActionResult> Authorize(Controller baseController)
    {
        var request = baseController.HttpContext.GetOpenIddictServerRequest();
        if (!request?.IsAuthorizationCodeGrantType() ?? false)
        {
            throw new NotImplementedException("The specified grant is not implemented.");
        }

        var authResult = await baseController.HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
        if (!authResult.Succeeded)
        {
            return baseController.Challenge(
                authenticationSchemes: IdentityConstants.ApplicationScheme,
                properties: new AuthenticationProperties
                {
                    RedirectUri = baseController.Request.PathBase + baseController.Request.Path + QueryString.Create(
                        baseController.Request.HasFormContentType ? baseController.Request.Form.ToList() : baseController.Request.Query.ToList())
                });
        }

        var principal = new ClaimsPrincipal(authResult.Principal);

        principal.SetScopes(request?.GetScopes());
        principal.SetClaim(Claims.Subject, principal.FindFirstValue(ClaimTypes.NameIdentifier));

        return baseController.SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="baseController"></param>
    /// <returns></returns>
    public async Task<IActionResult> GetUserInfo(Controller baseController)
    {
        var user = await _userManager.FindByIdAsync(baseController.User?.GetClaim(Claims.Subject) ?? "");
        if (user is null)
        {
            return baseController.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."
                }));
        }

        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] = await _userManager.GetUserIdAsync(user)
        };

        if (baseController.User?.HasScope(Scopes.Email) ?? false)
        {
            claims[Claims.Email] = await _userManager.GetEmailAsync(user) ?? "";
            claims[Claims.EmailVerified] = await _userManager.IsEmailConfirmedAsync(user);
        }

        if (baseController.User?.HasScope(Scopes.Phone) ?? false)
        {
            claims[Claims.PhoneNumber] = await _userManager.GetPhoneNumberAsync(user) ?? "";
            claims[Claims.PhoneNumberVerified] = await _userManager.IsPhoneNumberConfirmedAsync(user);
        }

        if (baseController.User?.HasScope(Scopes.Roles) ?? false)
        {
            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

        return baseController.Ok(claims);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="baseController"></param>
    /// <returns></returns>
    public async Task<IActionResult> Logout(Controller baseController)
    {
        // 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();

        // 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 baseController.SignOut(
            authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
            properties: new AuthenticationProperties
            {
                RedirectUri = "/"
            });
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="baseController"></param>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    /// <exception cref="NotImplementedException"></exception>
    public async Task<IActionResult> GetToken(Controller baseController)
    {
        var request = baseController.HttpContext.GetOpenIddictServerRequest() ??
            throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

        if (request.IsClientCredentialsGrantType())
        {
            // Note: the client credentials are automatically validated by OpenIddict:
            // if client_id or client_secret are invalid, this action won't be invoked.

            var application = await _applicationManager.FindByClientIdAsync(request.ClientId ?? "") ??
                throw new InvalidOperationException("The application cannot be found.");

            // Create a new ClaimsIdentity containing the claims that
            // will be used to create an id_token, a token or a code.
            var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, Claims.Name, Claims.Role);

            // Use the client_id as the subject identifier.
            identity.SetClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application));
            identity.SetClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application));

            identity.SetDestinations(static claim => claim.Type switch
            {
                // Allow the "name" claim to be stored in both the access and identity tokens
                // when the "profile" scope was granted (by calling principal.SetScopes(...)).
                Claims.Name when claim.Subject?.HasScope(Scopes.Profile) ?? false
                    => [Destinations.AccessToken, Destinations.IdentityToken],

                // Otherwise, only store the claim in the access tokens.
                _ => [Destinations.AccessToken]
            });

            return baseController.SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }
        else if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
        {
            // Retrieve the claims principal stored in the authorization code/refresh token.
            var result = await baseController.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 baseController.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 baseController.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, await _userManager.GetUserIdAsync(user))
            .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user))
            .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user))
                    .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user))
                    .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]);

            identity.SetDestinations(GetDestinations);

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

        throw new NotImplementedException("The specified grant is not implemented.");
    }

    /// <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) ?? false)
                    yield return Destinations.IdentityToken;

                yield break;

            case Claims.Email:
                yield return Destinations.AccessToken;

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

                yield break;

            case Claims.Role:
                yield return Destinations.AccessToken;

                if (claim.Subject?.HasScope(Scopes.Roles) ?? false)
                    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;
        }
    }
}

IOIDCRegisterable是注册OpenIddict的契约接口、IOIDCExecutable是OpenIddict各种授权流程具体实现的契约接口,使用本机服务端验证AccessToken。

3、在Program.cs如下设置:

using Microsoft.AspNetCore.Localization;
using RockstackAdmin.Infra.Common.Resources;
using RockstackAdmin.Infra.OIDC.OpenIddict;
using System.Globalization;

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

services.AddLocalization();
services.AddRazorPages();
services.AddControllersWithViews(options =>
{

})
.AddViewLocalization()
.AddDataAnnotationsLocalization(options =>
{
    options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(Shared));
});

const string defaultCulture = "en";
var supportedCultures = new[]
{
    new CultureInfo(defaultCulture),
    new CultureInfo("ko"),
    new CultureInfo("zh-CN"),
};
services.Configure<RequestLocalizationOptions>(options =>
{
    options.DefaultRequestCulture = new RequestCulture(defaultCulture);
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;
    options.ApplyCurrentCultureToResponseHeaders = true;
});

services.AddOIDC(options =>
{
    options.DatabaseType = configuration.GetValueByPath("OIDC.DatabaseType");
    options.ConnectionString = configuration.GetValueByPath("OIDC.ConnectionString");
    options.AuthorizationEndpointUris = configuration.GetValueByPath("OIDC.AuthorizationEndpointUris");
    options.EndSessionEndpointUri = configuration.GetValueByPath("OIDC.EndSessionEndpointUri");
    options.TokenEndpointUris = configuration.GetValueByPath("OIDC.TokenEndpointUris");
    options.UserInfoEndpointUris = configuration.GetValueByPath("OIDC.UserInfoEndpointUris");
});

var app = builder.Build();

app.UseRequestLocalization();

app.UseDeveloperExceptionPage();

app.UseForwardedHeaders();

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

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

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

app.Run();

4、appsettings.json配置:

{
  "OIDC": {
    "DatabaseType": "Sqlite",
    "ConnectionString": "Data Source=oidc.sqlite;Password=;Mode=ReadWriteCreate;Cache=Shared;Pooling=True;Default Timeout=5;",
    "TokenEndpointUris": "connect/token",
    "AuthorizationEndpointUris": "connect/authorize",
    "UserInfoEndpointUris": "connect/userinfo",
    "EndSessionEndpointUri": "connect/logout"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

5、控制器:

/// <summary>
/// 
/// </summary>
public class OIDCController : Controller
{
    /// <summary>
    /// 
    /// </summary>
    private readonly IOIDCExecutable _oidcExecutable;

    /// <summary>
    /// 
    /// </summary>
    /// <param name="oidcExecutable"></param>
    public OIDCController(IOIDCExecutable oidcExecutable)
    {
        _oidcExecutable = oidcExecutable;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <returns></returns>
    [HttpGet("~/connect/authorize"), HttpPost("~/connect/authorize")]
    public async Task<IActionResult> Authorize()
    {
        return await _oidcExecutable.Authorize(this);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <returns></returns>
    [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
    [HttpPost("~/connect/userinfo"), Consumes("application/json"), Produces("application/json")]
    public async Task<IActionResult> GetUserInfo()
    {
        return await _oidcExecutable.GetUserInfo(this);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <returns></returns>
    [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
    [HttpPost("~/connect/logout"), Consumes("application/json"), Produces("application/json")]
    public async Task<IActionResult> Logout()
    {
        return await _oidcExecutable.Logout(this);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <returns></returns>
    [HttpPost("~/connect/token"), Consumes("application/json"), Produces("application/json")]
    public async Task<IActionResult> GetToken()
    {
        return await _oidcExecutable.GetToken(this);
    }
}

~/connect/authorize:Authorization Code授权流程的入口页;

~/connect/userinfo:获取当前用户信息接口页;

~/connect/logout:注销登录接口页;

~/connect/token:获得AccessToken的接口页。

 可能感兴趣的文章

评论区

发表评论 / 取消回复

必填

选填

选填

◎欢迎讨论,请在这里发表您的看法及观点。