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

.NET 6中如何用JWT安全处理多组织多权限的登录门户?

多组织用户权限控制的安全实现方案(.NET)

核心思路

放弃给每个组织生成独立令牌的方案,改用**「用户身份+当前组织上下文+实时权限验证」**的模式,结合.NET的授权系统实现无冗余的权限校验,同时彻底解决越权风险。

具体实现步骤

1. 优化JWT令牌内容

令牌只保存用户核心身份信息,不存储各组织的角色:

// 生成JWT时仅存用户ID和关联组织ID列表(供前端展示切换用)
var claims = new List<Claim>
{
    new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
    new Claim("OrgIds", string.Join(",", user.Organisations.Select(o => o.Id)))
};
var token = new JwtSecurityToken(/* 配置签名等参数 */, claims: claims);

令牌仅用于身份认证,不包含任何组织权限信息,从根源避免用户利用高权限令牌越权。

2. 传递组织上下文

前端切换组织时,在所有请求的请求头中携带当前选中的组织ID(比如X-Selected-OrgId),后端通过中间件提取并存入请求上下文:

// 自定义中间件
public class OrgContextMiddleware
{
    private readonly RequestDelegate _next;

    public OrgContextMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Headers.TryGetValue("X-Selected-OrgId", out var orgIdStr) 
            && int.TryParse(orgIdStr, out var orgId))
        {
            context.Items["CurrentOrgId"] = orgId;
        }
        await _next(context);
    }
}

// 注册中间件(放在授权中间件之前)
app.UseMiddleware<OrgContextMiddleware>();

3. 自定义组织权限授权处理器

通过.NET的AuthorizationHandler实现统一的权限校验,避免在控制器中重复写验证逻辑:

首先定义权限要求:

// 组织管理员权限要求
public class OrgAdminRequirement : IAuthorizationRequirement { }

// 组织员工权限要求
public class OrgEmployeeRequirement : IAuthorizationRequirement { }

编写授权处理器,实时验证用户在当前组织的角色:

public class OrgAuthorizationHandler : AuthorizationHandler<OrgAdminRequirement>, 
                                      IAuthorizationHandler<OrgEmployeeRequirement>
{
    private readonly IUserOrgRoleService _userOrgRoleService;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public OrgAuthorizationHandler(IUserOrgRoleService userOrgRoleService, 
                                   IHttpContextAccessor httpContextAccessor)
    {
        _userOrgRoleService = userOrgRoleService;
        _httpContextAccessor = httpContextAccessor;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, 
                                                        OrgAdminRequirement requirement)
    {
        await ValidateOrgRole(context, "Admin");
    }

    public async Task HandleAsync(AuthorizationHandlerContext context, 
                                 OrgEmployeeRequirement requirement)
    {
        await ValidateOrgRole(context, "Employee");
    }

    private async Task ValidateOrgRole(AuthorizationHandlerContext context, string requiredRole)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        if (!httpContext.Items.TryGetValue("CurrentOrgId", out var orgIdObj) 
            || !(orgIdObj is int orgId))
        {
            context.Fail();
            return;
        }

        var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (!int.TryParse(userId, out int userIdInt))
        {
            context.Fail();
            return;
        }

        // 优先从缓存获取用户在该组织的角色,缓存失效时查库
        var userRole = await _userOrgRoleService.GetUserRoleInOrgAsync(userIdInt, orgId);
        
        // 验证接口参数中的OrgId与当前上下文OrgId一致(防止手动改参数越权)
        if (context.Resource is AuthorizationFilterContext filterContext)
        {
            var routeOrgId = filterContext.HttpContext.Request.RouteValues["orgId"];
            if (routeOrgId != null && int.Parse(routeOrgId.ToString()) != orgId)
            {
                context.Fail();
                return;
            }

            // 对于POST/PUT请求,验证DTO中的OrgId
            if (filterContext.HttpContext.Request.Method is "POST" or "PUT")
            {
                var dto = await filterContext.HttpContext.Request.ReadFromJsonAsync<OrgDTO>();
                if (dto != null && dto.OrgId != orgId)
                {
                    context.Fail();
                    return;
                }
            }
        }

        if (userRole == requiredRole)
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail();
        }
    }
}

4. 注册授权策略

在Program.cs中注册自定义授权策略和处理器:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("OrgAdmin", policy =>
        policy.Requirements.Add(new OrgAdminRequirement()));
    options.AddPolicy("OrgEmployee", policy =>
        policy.Requirements.Add(new OrgEmployeeRequirement()));
});

builder.Services.AddScoped<IAuthorizationHandler, OrgAuthorizationHandler>();
builder.Services.AddHttpContextAccessor();

5. 控制器中使用授权属性

直接在接口上标注对应的策略即可,无需手动验证:

[ApiController]
[Route("api/org")]
public class OrgController : ControllerBase
{
    // 员工可访问
    [HttpGet("{orgId}")]
    [Authorize(Policy = "OrgEmployee")]
    public IActionResult GetOrgData(int orgId)
    {
        // 直接处理业务,无需验证权限和OrgId合法性
        return Ok();
    }

    // 仅管理员可访问
    [HttpPut]
    [Authorize(Policy = "OrgAdmin")]
    public IActionResult UpdateOrgData(OrgDTO dto)
    {
        // 直接处理业务
        return Ok();
    }
}

关键优势

  • 安全性高:令牌不携带组织权限,每次请求实时验证用户在当前组织的角色,无法通过本地存储的令牌越权。
  • 无代码冗余:权限验证逻辑统一放在授权处理器中,控制器只需关注业务逻辑。
  • 扩展性强:后续新增组织角色或权限规则,只需新增对应的IAuthorizationRequirement和修改处理器逻辑即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 14:01:35