网站首页 网站源码
website
站点相关全部源代码,隐藏了一些关于服务器的信息
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);
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

上述代码是一个 ASP.NET Core 控制器,主要用于处理 OpenID Connect (OIDC) 授权流程。它实现了用户授权、同意、注销等功能,使用了 OpenIddict 库来管理 OIDC 相关的操作。以下是代码的主要功能和结构的详细解释:

1. 控制器概述

  • AuthorizationController:这是一个控制器类,负责处理与用户授权相关的请求。它依赖于多个服务,如 OpenIddictApplicationManagerOpenIddictAuthorizationManagerSignInManagerUserManagerIOpenIddictTokenManager

2. 授权流程

  • Authorize 方法

    • 处理 GET 请求,路径为 ~/connect/authorize
    • 首先获取 OpenID 请求,如果用户未登录,则重定向到登录页面。
    • 如果用户已登录,检查用户是否已经授权该应用程序。如果没有,则显示同意页面。
    • 如果用户已授权,则构建并返回一个签名结果。
  • Accept 方法

    • 处理 POST 请求,路径为 ~/connect/authorize
    • 该方法在用户同意授权后被调用,构建并返回签名结果。

3. 构建签名结果

  • BuildSignInResultAsync 方法
    • 该方法根据用户信息和请求的 scopes 构建一个 ClaimsPrincipal 对象。
    • 为用户添加必要的声明(如性别、头像、用户名等)。
    • 设置声明的目标(AccessToken 或 IdentityToken),并返回一个签名结果。

4. 注销功能

  • Logout 方法
    • 处理 GET 和 POST 请求,路径为 ~/connect/logout
    • 撤销与用户相关的授权和令牌。
    • 校验 post_logout_redirect_uri 是否为客户端已注册的 URI,并进行重定向。
    • 注销本地 Cookie 会话,并返回 OpenIddict 的注销重定向。

5. 辅助方法

  • EnsureClaim 方法:确保在 ClaimsPrincipal 中添加特定的声明。
  • GetGenderValue 方法:将用户的性别枚举转换为 OIDC 的 gender 值。
  • GetDestinations 方法:根据声明的类型和请求的 scopes 确定声明的投递目标。
  • TryParseIdTokenHint 方法:解析 id_token_hint,提取用户的 subject 和 clientId。

6. 依赖注入

  • 控制器通过构造函数注入了多个服务,这些服务用于处理用户身份验证、授权、令牌管理等功能。

总结

该控制器实现了 OpenID Connect 授权流程的核心部分,包括用户登录、授权、同意、注销等功能。它利用 OpenIddict 库来简化 OIDC 的实现,并通过声明管理来处理用户信息的传递。整体上,这段代码是一个典型的 OIDC 授权服务器的实现示例。

loading