类别:技术积累 / 日期: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的接口页。

发表评论 / 取消回复