网站首页 网站源码
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Dpz.Core.Auth.Models;
using Dpz.Core.EnumLibrary;
using Dpz.Core.Infrastructure;
using Dpz.Core.MongodbAccess;
using Dpz.Core.Public.Entity;
using Dpz.Core.Public.Entity.Auth;
using Dpz.Core.Public.ViewModel;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using MongoDB.Driver.Linq;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
namespace Dpz.Core.Auth.Controllers;
public class AuthorizationController(
OpenIddictApplicationManager<DpzApplication> openIddictApplicationManager,
OpenIddictAuthorizationManager<DpzAuthorization> openIddictAuthorizationManager,
SignInManager<VmUserInfo> signInManager,
UserManager<VmUserInfo> userManager,
IOpenIddictTokenManager tokenManager,
IOptionsMonitor<OpenIddictServerOptions> serverOptions,
ILogger<AuthorizationController> logger,
IRepository<AllowedClient> allowedClientRepository,
IRepository<ApplicationAccessRequest> accessRequestRepository,
IConfiguration configuration
) : Controller
{
/// <summary>
/// 用户授权
/// 发起授权码流程,跳转到登录页并请求用户授权
/// </summary>
/// <returns></returns>
[HttpGet("~/connect/authorize")]
public async Task<IActionResult> Authorize(string? returnUrl = null)
{
var request =
HttpContext.GetOpenIddictServerRequest()
?? throw new ArgumentException("The request cannot be retrieved.");
if (User.Identity?.IsAuthenticated != true)
{
// 未登录,跳转到登录页
return Challenge(authenticationSchemes: ConstValues.DefaultScheme);
}
var clientId = request.ClientId ?? "";
var scopes = request.GetScopes();
var application = await openIddictApplicationManager.FindByClientIdAsync(clientId);
if (application == null)
{
throw new InvalidOperationException(
"Details concerning the calling client application cannot be found."
);
}
var userId = User.NameIdentifier;
if (string.IsNullOrEmpty(userId))
{
return Challenge(authenticationSchemes: ConstValues.DefaultScheme);
}
var result = await CheckApplicationAccessAsync(userId, clientId, application);
if (result is { Success: false, Data: not null })
{
return result.Data;
}
// 已登录,判断是否授权
var authorizations = openIddictAuthorizationManager.FindAsync(
userId,
application.Id.ToString(),
OpenIddictConstants.Statuses.Valid,
null,
scopes
);
if (!await authorizations.AnyAsync(x => x.CreationDate >= DateTime.Now.AddDays(-3)))
{
var currentUser = await userManager.GetUserAsync(User);
var consentModel = new ConsentModel(application, currentUser, returnUrl, scopes);
return View("Consent", consentModel);
}
var userInfo = await userManager.GetUserAsync(User);
if (userInfo == null)
{
return Challenge(authenticationSchemes: ConstValues.DefaultScheme);
}
return await BuildSignInResultAsync(userInfo, scopes);
}
[ValidateAntiForgeryToken, HttpPost("~/connect/authorize"), Authorize]
public async Task<IActionResult> Accept()
{
var request =
HttpContext.GetOpenIddictServerRequest()
?? throw new ArgumentException("The request cannot be retrieved.");
var userInfo = await userManager.GetUserAsync(User);
if (userInfo == null)
{
return Challenge(authenticationSchemes: ConstValues.DefaultScheme);
}
var clientId = request.ClientId ?? "";
var application = await openIddictApplicationManager.FindByClientIdAsync(clientId);
if (application == null)
{
throw new InvalidOperationException(
"Details concerning the calling client application cannot be found."
);
}
var result = await CheckApplicationAccessAsync(userInfo.Id, clientId, application);
if (result is { Success: false, Data: not null })
{
return result.Data;
}
return await BuildSignInResultAsync(userInfo, request.GetScopes());
}
[NonAction]
private async Task<ResponseResult<IActionResult>> CheckApplicationAccessAsync(
string userId,
string clientId,
DpzApplication application
)
{
var result = new ResponseResult<IActionResult>();
// 必须显式授权
// 根据需求:"确认用户是否可以访问某个应用"
// 策略:如果没有显式授权,则拒绝。
var defaultApplications =
configuration.GetSection("DefaultApplications").Get<string[]>() ?? [];
// 特殊处理:如果是管理员(Permission.System),允许所有
var isAllowed =
(
Enum.TryParse(
User.FindFirst("Permissions")?.Value ?? "",
out Permissions permissions
)
&& (permissions & Permissions.System) == Permissions.System
)
|| defaultApplications.Contains(clientId)
|| await allowedClientRepository
.SearchFor(x => x.Account == userId && x.ApplicationId == clientId)
.AnyAsync();
if (!isAllowed)
{
// 检查是否有待处理的申请
var pendingRequest = await accessRequestRepository
.SearchFor(x =>
x.UserId == userId
&& x.ClientId == clientId
&& x.Status == AccessRequestStatus.Pending
)
.AnyAsync();
var vm = new AccessDeniedViewModel
{
Application = application,
User = await userManager.GetUserAsync(User) ?? new VmUserInfo(),
ReturnUrl = HttpContext.Request.Method.Equals(
"GET",
StringComparison.OrdinalIgnoreCase
)
? HttpContext.Request.Path + HttpContext.Request.QueryString
: "",
HasPendingRequest = pendingRequest,
};
result.Success = false;
result.Message = "未授权";
result.Data = View("AccessDenied", vm);
return result;
}
return result.SuccessResult(null);
}
[ValidateAntiForgeryToken, HttpPost("~/connect/apply-access"), Authorize]
public async Task<IActionResult> ApplyAccess(
string clientId,
string reason,
string? returnUrl = null
)
{
var application = await openIddictApplicationManager.FindByClientIdAsync(clientId);
if (application == null)
{
return NotFound("Application not found");
}
var userId = User.NameIdentifier;
if (string.IsNullOrEmpty(userId))
{
return Challenge(authenticationSchemes: ConstValues.DefaultScheme);
}
// 检查是否已有 Pending 申请
var existingRequest = await accessRequestRepository
.SearchFor(x =>
x.UserId == userId
&& x.ClientId == clientId
&& x.Status == AccessRequestStatus.Pending
)
.FirstOrDefaultAsync();
if (existingRequest == null)
{
var request = new ApplicationAccessRequest
{
UserId = userId,
ClientId = clientId,
ClientDisplayName = application.DisplayName,
Status = AccessRequestStatus.Pending,
Reason = reason,
RequestTime = DateTime.Now,
};
await accessRequestRepository.InsertAsync(request);
}
// 重定向回原始请求地址(即重新发起 Authorize 请求),此时会显示“已申请”状态
if (!string.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home");
}
#region 共用:根据用户与 scope 构造 principal,设置 claims 投递目标并返回 SignIn 结果
[NonAction]
private async Task<IActionResult> BuildSignInResultAsync(
VmUserInfo userInfo,
IEnumerable<string> scopes
)
{
var principal = await signInManager.CreateUserPrincipalAsync(userInfo);
// 标准化附加身份信息(若工厂未添加,则补充;已存在则跳过)
EnsureClaim(principal, OpenIddictConstants.Claims.Gender, GetGenderValue(userInfo));
EnsureClaim(principal, OpenIddictConstants.Claims.Picture, userInfo.Avatar);
EnsureClaim(principal, OpenIddictConstants.Claims.PreferredUsername, userInfo.Id);
// 若存在邮箱,则标记 email_verified=true
if (!string.IsNullOrWhiteSpace(userInfo.Email))
{
EnsureClaim(principal, OpenIddictConstants.Claims.EmailVerified, "true");
}
// 设置 scopes / resources
principal.SetScopes(scopes);
principal.SetResources("resource_server");
// 设置各 Claim 的投递目标(AccessToken / IdentityToken)
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
private static void EnsureClaim(ClaimsPrincipal principal, string type, string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
if (principal.Claims.Any(c => c.Type == type))
{
return;
}
var identity = (ClaimsIdentity)principal.Identity!;
identity.AddClaim(new Claim(type, value));
}
private static string GetGenderValue(VmUserInfo user)
{
// 将 Sex 枚举映射为 OIDC 的 gender 值:male/female/other
return user.Sex switch
{
Sex.Man => "male",
Sex.Wuman => "female",
_ => "other",
};
}
private static List<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
// 默认进入 access_token
var destinations = new List<string> { OpenIddictConstants.Destinations.AccessToken };
// profile 范围:将这些声明放入 id_token
if (principal.HasScope(OpenIddictConstants.Scopes.Profile))
{
if (
claim.Type
is OpenIddictConstants.Claims.Name
or OpenIddictConstants.Claims.Nickname
or OpenIddictConstants.Claims.PreferredUsername
or OpenIddictConstants.Claims.Gender
or OpenIddictConstants.Claims.Picture
)
{
destinations.Add(OpenIddictConstants.Destinations.IdentityToken);
}
}
// email 范围:email 与 email_verified 进入 id_token
if (principal.HasScope(OpenIddictConstants.Scopes.Email))
{
if (
claim.Type
is OpenIddictConstants.Claims.Email
or OpenIddictConstants.Claims.EmailVerified
)
{
destinations.Add(OpenIddictConstants.Destinations.IdentityToken);
}
}
return destinations;
}
#endregion
/// <summary>
/// OIDC RP-Initiated Logout 端点
/// </summary>
[HttpGet("~/connect/logout")]
[HttpPost("~/connect/logout")]
public async Task<IActionResult> Logout()
{
var request =
HttpContext.GetOpenIddictServerRequest()
?? throw new ArgumentException("The request cannot be retrieved.");
// 撤销关联授权与令牌(优先使用当前认证用户,其次尝试从 id_token_hint 提取)
try
{
var subject = User.NameIdentifier;
if (
string.IsNullOrWhiteSpace(subject)
&& !string.IsNullOrWhiteSpace(request.IdTokenHint)
)
{
var (parsedSubject, _) = TryParseIdTokenHint(
request.IdTokenHint!,
serverOptions.CurrentValue
);
if (!string.IsNullOrWhiteSpace(parsedSubject))
{
subject = parsedSubject;
}
}
// 解析可用于精细撤销的 client_id
string? revokeClientId = null;
if (!string.IsNullOrWhiteSpace(request.IdTokenHint))
{
var (_, tokenClientId) = TryParseIdTokenHint(
request.IdTokenHint!,
serverOptions.CurrentValue
);
revokeClientId = tokenClientId;
}
revokeClientId ??= request.ClientId;
if (!string.IsNullOrWhiteSpace(subject))
{
await tokenManager.RevokeAsync(subject, revokeClientId, null, null);
await openIddictAuthorizationManager.RevokeAsync(
subject,
revokeClientId,
null,
null
);
}
}
catch (Exception e)
{
logger.LogError(e, "撤销用户授权/令牌失败");
}
// 校验 post_logout_redirect_uri 是否为客户端已注册
var safeRedirectUri = "/";
try
{
string? tokenClientId = null;
if (!string.IsNullOrWhiteSpace(request.IdTokenHint))
{
(_, tokenClientId) = TryParseIdTokenHint(
request.IdTokenHint!,
serverOptions.CurrentValue
);
}
var clientId = tokenClientId ?? request.ClientId;
if (
!string.IsNullOrWhiteSpace(request.PostLogoutRedirectUri)
&& !string.IsNullOrWhiteSpace(clientId)
)
{
var application = await openIddictApplicationManager.FindByClientIdAsync(clientId);
if (application is not null)
{
var registeredUris =
await openIddictApplicationManager.GetPostLogoutRedirectUrisAsync(
application,
HttpContext.RequestAborted
);
if (
registeredUris.Contains(
request.PostLogoutRedirectUri!,
StringComparer.Ordinal
)
)
{
safeRedirectUri = request.PostLogoutRedirectUri!;
}
}
}
}
catch (Exception e)
{
logger.LogWarning(e, "校验 post_logout_redirect_uri 失败,已回退为默认重定向");
}
// 注销本地 Cookie 会话
await HttpContext.SignOutAsync(ConstValues.DefaultScheme);
// 返回给 OpenIddict 以完成标准的 EndSession 重定向
return SignOut(
new AuthenticationProperties { RedirectUri = safeRedirectUri },
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme
);
}
[NonAction]
private (string? subject, string? clientId) TryParseIdTokenHint(
string idToken,
OpenIddictServerOptions options
)
{
try
{
var handler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = options.Issuer is not null,
ValidIssuer = options.Issuer?.AbsoluteUri,
// RP 已在会话上下文中,登出仅需确认签名与发行者
ValidateAudience = false,
// 允许过期 id_token 用于登出提示
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = options.SigningCredentials.Select(c => c.Key),
};
handler.ValidateToken(idToken, validationParameters, out var securityToken);
if (securityToken is JwtSecurityToken jwt)
{
var sub =
jwt.Claims.FirstOrDefault(x =>
x.Type == OpenIddictConstants.Claims.Subject
)?.Value
?? jwt.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value;
// 客户端标识:优先 azp,其次单一 aud
var azp = jwt.Claims.FirstOrDefault(x => x.Type == "azp")?.Value;
var audClaims = jwt
.Claims.Where(x => x.Type == JwtRegisteredClaimNames.Aud)
.Select(x => x.Value)
.ToList();
var tokenClientId = azp ?? (audClaims.Count == 1 ? audClaims[0] : null);
return (
string.IsNullOrWhiteSpace(sub) ? null : sub,
string.IsNullOrWhiteSpace(tokenClientId) ? null : tokenClientId
);
}
}
catch (Exception e)
{
logger.LogError(e, "解析 id_token 失败");
}
return (null, null);
}
}
上述代码是一个 ASP.NET Core 控制器,主要用于处理 OpenID Connect (OIDC) 授权流程。它实现了用户授权、同意、注销等功能,使用了 OpenIddict 库来管理 OIDC 相关的操作。以下是代码的主要功能和结构的详细解释:
OpenIddictApplicationManager、OpenIddictAuthorizationManager、SignInManager、UserManager 和 IOpenIddictTokenManager。Authorize 方法:
~/connect/authorize。Accept 方法:
~/connect/authorize。ClaimsPrincipal 对象。~/connect/logout。post_logout_redirect_uri 是否为客户端已注册的 URI,并进行重定向。ClaimsPrincipal 中添加特定的声明。id_token_hint,提取用户的 subject 和 clientId。该控制器实现了 OpenID Connect 授权流程的核心部分,包括用户登录、授权、同意、注销等功能。它利用 OpenIddict 库来简化 OIDC 的实现,并通过声明管理来处理用户信息的传递。整体上,这段代码是一个典型的 OIDC 授权服务器的实现示例。
