You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Cookie认证用户角色Claims更新不生效问题排查求助

问题描述

我在控制器中实现了两个端点,目标是传入角色列表并将其添加到当前Cookie认证用户的Claims中。该API同时使用AddNegotiate和AddCookie两种认证方式,但调用SignIn前后,Claims端点返回的内容一致,新添加的Claims并未显示。我的目标是让同域下的另一个应用发送新角色列表,应用到用户后,重新登录能在Claims中看到更新。

控制器代码

[HttpPost("updateroles")]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public async Task<IActionResult> UpdateRoles([FromBody] RoleUpdateRequest request)
{
    if (request.Roles == null)
    {
        return BadRequest("Roles cannot be null");
    }
    var currentClaimsIdentity = (ClaimsIdentity)User.Identity!;
    if (currentClaimsIdentity != null)
    {
        try
        {
            var claims = request.Roles!.Select(role => new Claim(ClaimTypes.Role, role)).ToList();
            currentClaimsIdentity.AddClaims(claims);

            var newPrincipal = new ClaimsPrincipal(currentClaimsIdentity);

            var lastAuthenticated = DateTime.UtcNow;

            var authProperties = new AuthenticationProperties()
            {
                IsPersistent = true,
                ExpiresUtc = lastAuthenticated.AddHours(_appConfiguration.CookieTimeout)
            };

            var claimsMessage = string.Join(", ", newPrincipal.Claims.Select(c => c.Value));
            _logger.LogMessage("Updating User with Following Claims : " + claimsMessage);

            // Sign in with the new principal
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, newPrincipal, authProperties);
        }
        catch (Exception ex)
        {
            _logger.LogError("Role Update Error", ex);
            return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while updating roles.");
        }

        return Ok(new { Message = "Roles updated successfully", Roles = request.Roles });
    }

    return BadRequest("Unable to update roles.");
}

[HttpGet("claims")]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public async Task<IActionResult> GetClaims()
{
    if (User.Identity!.IsAuthenticated)
    {
        var userId = User.Identity!.Name;
        return Ok(User.Claims.Select(c => new { c.Type, c.Value }));
    }

    return Unauthorized();
}

认证配置代码

// Cookies stored key ring
services.AddDataProtection()
        .PersistKeysToFileSystem(new DirectoryInfo(PHYSICALPATH))
        .SetApplicationName(config["CookieName"]!);

services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
        .AddNegotiate() // For Windows Authentication to retrieve User Groups
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
        {
            options.Cookie.Name = config["CookieName"];
            options.Cookie.Domain = config["DomainSite"];
            options.Cookie.Path = "/";
            options.Cookie.HttpOnly = true;
            options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
            options.Cookie.SameSite = SameSiteMode.None;
        });

登录方法代码

[AllowAnonymous]
[HttpGet("login")]
public async Task<IActionResult> Login(string returnUrl)
{
    if (User.Identity != null && User.Identity.IsAuthenticated)
    {
        var username = User.Identity.Name;
        var lastAuthenticated = DateTime.UtcNow;
        var roles = _domainUserGroupService.GetRolesFromGroups(User);

        // Create claims with the Claim Name from Identity Name as well as adding roles and Last Authenticated Timestamp
        var claims = _domainUserGroupService.CreateClaims(User.Identity.Name!, roles, lastAuthenticated);
        var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
        var principal = new ClaimsPrincipal(identity);

        var authProperties = new AuthenticationProperties()
        {
            IsPersistent = true,
            ExpiresUtc = lastAuthenticated.AddHours(_appConfiguration.CookieTimeout)
        };

        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, authProperties);

        _logger.LogMessage("Login User Identity Name : " + username);

        return Redirect(returnUrl);
    }
    else
    {
        return Unauthorized();
    }
}
错误分析
  • 直接修改当前请求的ClaimsIdentity无效:当前请求的User.Identity是认证流程生成的实例,直接修改它不会影响后续请求的Claims,SignIn操作生成新Cookie后,当前请求的User对象不会自动更新。
  • SignOut/SignIn后未刷新当前请求上下文:即使执行了SignIn,当前请求的User还是原有实例,必须发起新请求才能读取到新Cookie的内容。
  • 未处理Role Claim重复问题:多次调用UpdateRoles会重复添加相同Role,导致Claims冗余,还可能影响后续登录的角色合并逻辑。
  • 认证Scheme优先级冲突:默认认证Scheme是NegotiateDefaults.AuthenticationScheme,若后续请求通过Negotiate自动登录,会覆盖Cookie中的Claims,导致更新的角色丢失。
改进方案

1. 修正UpdateRoles端点逻辑

避免直接修改当前请求的ClaimsIdentity,基于现有Claims创建新实例,并处理Role去重:

[HttpPost("updateroles")]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public async Task<IActionResult> UpdateRoles([FromBody] RoleUpdateRequest request)
{
    if (request.Roles == null || !request.Roles.Any())
    {
        return BadRequest("Roles cannot be null or empty");
    }

    // 提取当前用户非Role类型的Claims
    var existingNonRoleClaims = User.Claims
        .Where(c => c.Type != ClaimTypes.Role)
        .Select(c => new Claim(c.Type, c.Value))
        .ToList();

    // 生成去重后的新Role Claims
    var newRoleClaims = request.Roles
        .Distinct()
        .Select(role => new Claim(ClaimTypes.Role, role))
        .ToList();

    // 创建新的ClaimsIdentity,指定Cookie认证Scheme
    var newIdentity = new ClaimsIdentity(
        existingNonRoleClaims.Concat(newRoleClaims),
        CookieAuthenticationDefaults.AuthenticationScheme
    );

    var newPrincipal = new ClaimsPrincipal(newIdentity);
    var lastAuthenticated = DateTime.UtcNow;
    var authProperties = new AuthenticationProperties()
    {
        IsPersistent = true,
        ExpiresUtc = lastAuthenticated.AddHours(_appConfiguration.CookieTimeout),
        IssuedUtc = lastAuthenticated
    };

    try
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, newPrincipal, authProperties);

        _logger.LogMessage($"Updated user {User.Identity?.Name} with roles: {string.Join(", ", request.Roles)}");

        return Ok(new { 
            Message = "Roles updated successfully. Send a new request to see changes.", 
            Roles = request.Roles 
        });
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to update user roles");
        return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while updating roles.");
    }
}

2. 强制接口使用Cookie认证

在需要读取更新后Claims的接口上,明确指定认证Scheme,避免Negotiate覆盖Cookie内容:

[HttpGet("claims")]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public IActionResult GetClaims()
{
    if (User.Identity!.IsAuthenticated)
    {
        return Ok(User.Claims.Select(c => new { c.Type, c.Value }));
    }
    return Unauthorized();
}

3. 优化登录逻辑,优先使用Cookie认证

登录时优先检查是否已有有效Cookie,避免Negotiate的Groups覆盖更新后的角色:

[AllowAnonymous]
[HttpGet("login")]
public async Task<IActionResult> Login(string returnUrl)
{
    // 优先检查Cookie认证状态
    var cookieAuthResult = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    if (cookieAuthResult.Succeeded)
    {
        return Redirect(returnUrl);
    }

    // 无有效Cookie时,使用Negotiate获取用户信息生成Cookie
    if (User.Identity != null && User.Identity.IsAuthenticated)
    {
        var username = User.Identity.Name;
        var lastAuthenticated = DateTime.UtcNow;
        var roles = _domainUserGroupService.GetRolesFromGroups(User);

        var claims = _domainUserGroupService.CreateClaims(User.Identity.Name!, roles, lastAuthenticated);
        var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
        var principal = new ClaimsPrincipal(identity);

        var authProperties = new AuthenticationProperties()
        {
            IsPersistent = true,
            ExpiresUtc = lastAuthenticated.AddHours(_appConfiguration.CookieTimeout)
        };

        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, authProperties);

        _logger.LogMessage($"Logged in user: {username}");

        return Redirect(returnUrl);
    }
    else
    {
        return Unauthorized();
    }
}

4. 添加Cookie自动刷新逻辑

配置Cookie认证的OnValidatePrincipal事件,在用户活跃时自动从数据源同步最新角色:

services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
        .AddNegotiate()
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
        {
            // 原有Cookie配置
            options.Cookie.Name = config["CookieName"];
            options.Cookie.Domain = config["DomainSite"];
            options.Cookie.Path = "/";
            options.Cookie.HttpOnly = true;
            options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
            options.Cookie.SameSite = SameSiteMode.None;

            // 自动刷新Claims逻辑
            options.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = async context =>
                {
                    var userService = context.HttpContext.RequestServices.GetRequiredService<IDomainUserGroupService>();
                    var username = context.Principal.Identity?.Name;
                    if (!string.IsNullOrEmpty(username))
                    {
                        var latestRoles = userService.GetRolesFromUser(username);
                        var existingRoles = context.Principal.Claims
                            .Where(c => c.Type == ClaimTypes.Role)
                            .Select(c => c.Value)
                            .ToList();

                        // 角色变化时更新Principal并刷新Cookie
                        if (!latestRoles.SequenceEqual(existingRoles))
                        {
                            var nonRoleClaims = context.Principal.Claims
                                .Where(c => c.Type != ClaimTypes.Role)
                                .Select(c => new Claim(c.Type, c.Value));
                            var newRoleClaims = latestRoles.Select(r => new Claim(ClaimTypes.Role, r));
                            var newIdentity = new ClaimsIdentity(
                                nonRoleClaims.Concat(newRoleClaims),
                                context.Principal.Identity.AuthenticationType
                            );

                            context.ReplacePrincipal(new ClaimsPrincipal(newIdentity));
                            context.ShouldRenew = true;
                        }
                    }
                }
            };
        });

内容的提问来源于stack exchange,提问作者Wolfenstyne

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 22:32:05