【ASP.NET CORE】7.JWT认证授权与策略权限管理

介绍

当前多数项目采用前后端分离架构,权限控制是保障系统安全的核心环节。JWT(JSON Web Token)作为一种无状态认证方案,无需在服务器端保存传统 Session,仅需客户端携带 Token 即可完成身份认证,天然适配跨域、多端、分布式部署等场景。

本次完善框架的 JWT 认证授权机制,将 Token 配置、Token 生成、当前登录用户上下文、接口权限注解、策略授权统一封装到 Common 层。业务接口只需要通过 [Authorize][AllowAnonymous][Permission("xxx")] 即可完成接口级权限控制,同时也为角色权限、接口权限、按钮权限、数据权限等后续扩展保留统一入口。

实现

1.JWT配置

首先在配置文件中增加 JWT 节点,将签发方、接收方、密钥、过期时间等核心参数统一管理,避免硬编码散落在业务代码中。

"Jwt": {
  "Issuer": "HQServer",
  "Audience": "HQServer.Client",
  "SecurityKey": "HQServer-development-secret-key-change-me",
  "ExpireMinutes": 120,
  "RefreshExpireDays": 7,
  "ClockSkewSeconds": 30
}
namespace HQ.Common.Authentication;

public class JwtOptions
{
    public string Issuer { get; set; } = "HQServer";
    public string Audience { get; set; } = "HQServer.Client";
    public string SecurityKey { get; set; } = string.Empty;
    public int ExpireMinutes { get; set; } = 120;
    public int RefreshExpireDays { get; set; } = 7;
    public int ClockSkewSeconds { get; set; } = 30;
}

2.Token生成服务

Token 生成逻辑统一封装为 IJwtTokenService,登录成功后业务层只需要传入用户编号、用户名、角色、权限等基础信息,不直接关心 Claim 组装和签名细节。

public interface IJwtTokenService
{
    string GenerateAccessToken(LoginUserDto user);
}

public class LoginUserDto
{
    public string UserId { get; set; } = string.Empty;
    public string? UserName { get; set; }
    public string? NickName { get; set; }
    public List<string> Roles { get; set; } = new();
    public List<string> Permissions { get; set; } = new();
}
public class JwtTokenService : IJwtTokenService
{
    private readonly JwtOptions _options;

    public JwtTokenService(IOptions<JwtOptions> options)
    {
        _options = options.Value;
    }

    public string GenerateAccessToken(LoginUserDto user)
    {
        var claims = new List<Claim>
        {
            new(ClaimTypes.NameIdentifier, user.UserId),
            new(ClaimTypes.Name, user.UserName ?? string.Empty),
            new(HQClaimTypes.NickName, user.NickName ?? string.Empty)
        };

        claims.AddRange(user.Roles
            .Where(role => !string.IsNullOrWhiteSpace(role))
            .Distinct()
            .Select(role => new Claim(ClaimTypes.Role, role)));

        claims.AddRange(user.Permissions
            .Where(permission => !string.IsNullOrWhiteSpace(permission))
            .Distinct()
            .Select(permission => new Claim(HQClaimTypes.Permission, permission)));

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.SecurityKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _options.Issuer,
            audience: _options.Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(_options.ExpireMinutes),
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

3.当前登录用户上下文

接口中经常需要读取当前登录用户信息,如果每个接口都手动解析 HttpContext.User 会产生大量重复代码。因此封装 ICurrentUser,统一暴露用户编号、用户名、角色和权限。

public interface ICurrentUser
{
    bool IsAuthenticated { get; }
    string? UserId { get; }
    string? UserName { get; }
    IReadOnlyList<string> Roles { get; }
    IReadOnlyList<string> Permissions { get; }
}

4.注解式权限控制

框架保留 ASP.NET Core 原生 [Authorize][AllowAnonymous] 能力,同时增加 [Permission] 权限注解,让业务接口可以用更直观的方式声明权限点。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class PermissionAttribute : AuthorizeAttribute
{
    public const string PolicyPrefix = "Permission:";

    public PermissionAttribute(string permission)
    {
        if (string.IsNullOrWhiteSpace(permission))
            throw new ArgumentException("权限标识不能为空", nameof(permission));

        Permission = permission;
        Policy = PolicyPrefix + permission;
    }

    public string Permission { get; }
}

5.策略授权模式

策略授权的核心是 Requirement + Handler。Requirement 描述授权要求,Handler 负责判断当前用户是否满足要求。这样权限判断逻辑可以集中维护,不需要写在 Controller 或 Service 中。

public class PermissionRequirement : IAuthorizationRequirement
{
    public PermissionRequirement(string permission)
    {
        Permission = permission;
    }

    public string Permission { get; }
}
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PermissionRequirement requirement)
    {
        if (context.User.Identity?.IsAuthenticated != true)
            return Task.CompletedTask;

        var permissions = context.User
            .FindAll(HQClaimTypes.Permission)
            .Select(x => x.Value)
            .Where(x => !string.IsNullOrWhiteSpace(x))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        if (context.User.IsInRole("Admin")
            || permissions.Contains("*:*:*")
            || permissions.Contains(requirement.Permission))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

6.动态权限策略

权限点通常很多,如果每个权限都手动注册 Policy,会导致配置膨胀。因此通过 IAuthorizationPolicyProvider 动态识别 Permission: 前缀,按需生成授权策略。

public class PermissionPolicyProvider : DefaultAuthorizationPolicyProvider
{
    public PermissionPolicyProvider(IOptions<AuthorizationOptions> options)
        : base(options)
    {
    }

    public override async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
    {
        if (policyName.StartsWith(PermissionAttribute.PolicyPrefix, StringComparison.OrdinalIgnoreCase))
        {
            var permission = policyName[PermissionAttribute.PolicyPrefix.Length..];

            return new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .AddRequirements(new PermissionRequirement(permission))
                .Build();
        }

        return await base.GetPolicyAsync(policyName);
    }
}

7.统一注册入口

最后将 JWT 认证、当前用户上下文、权限策略提供器、授权处理器统一注册到 DI 容器中,业务项目只需要在启动管线中调用一次即可。

public static IServiceCollection AddHQJwtAuthentication(
    this IServiceCollection services,
    IConfiguration configuration)
{
    services.Configure<JwtOptions>(configuration.GetSection("Jwt"));

    var options = configuration.GetSection("Jwt").Get<JwtOptions>()
        ?? throw new InvalidOperationException("缺少 Jwt 配置节点");

    if (string.IsNullOrWhiteSpace(options.SecurityKey) || options.SecurityKey.Length < 32)
        throw new InvalidOperationException("Jwt:SecurityKey 长度不能小于 32 位");

    services.AddHttpContextAccessor();
    services.AddScoped<ICurrentUser, CurrentUser>();
    services.AddScoped<IJwtTokenService, JwtTokenService>();

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(config =>
        {
            config.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidIssuer = options.Issuer,
                ValidateAudience = true,
                ValidAudience = options.Audience,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.SecurityKey)),
                ValidateLifetime = true,
                ClockSkew = TimeSpan.FromSeconds(options.ClockSkewSeconds)
            };
        });

    services.AddHQAuthorization();
    return services;
}
builder.Services.AddHQJwtAuthentication(builder.Configuration);

app.UseAuthentication();
app.UseAuthorization();

特点

特性说明

无状态认证:服务端不需要保存传统 Session,适合前后端分离、多端登录、分布式部署。

统一封装:Token 生成、用户上下文、认证注册、授权策略全部沉淀在 Common 层,业务项目开箱即用。

注解控制:接口通过 [Authorize][AllowAnonymous][Permission("xxx")] 即可完成认证与权限控制。

策略扩展:基于 ASP.NET Core Policy 模型,可灵活扩展角色权限、接口权限、按钮权限、数据权限。

动态权限:通过自定义 IAuthorizationPolicyProvider 动态创建权限策略,无需为每个权限点手动注册。

安全防御:校验 Issuer、Audience、签名密钥、过期时间,并限制 ClockSkew,避免 Token 长时间误放行。

使用示例

[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login(LoginInput input)
{
    // 1. 校验用户名密码
    // 2. 查询角色和权限
    var user = new LoginUserDto
    {
        UserId = "10001",
        UserName = input.UserName,
        NickName = "管理员",
        Roles = new List<string> { "Admin" },
        Permissions = new List<string> { "user:list", "user:create", "user:update" }
    };

    var token = _jwtTokenService.GenerateAccessToken(user);
    return Ok(new { accessToken = token });
}
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile([FromServices] ICurrentUser currentUser)
{
    return Ok(new
    {
        currentUser.UserId,
        currentUser.UserName,
        currentUser.Roles,
        currentUser.Permissions
    });
}
[Permission("user:create")]
[HttpPost("user")]
public async Task<IActionResult> CreateUser(CreateUserInput input)
{
    // 当前用户必须拥有 user:create 权限,或拥有 Admin / *:*:* 超级权限
    return Ok();
}

完整代码

建议在 Common 层按下面结构存放核心文件:

HQ.Common
 ├── Authentication
 │   ├── JwtOptions.cs
 │   ├── LoginUserDto.cs
 │   ├── IJwtTokenService.cs
 │   ├── JwtTokenService.cs
 │   ├── ICurrentUser.cs
 │   ├── CurrentUser.cs
 │   ├── HQClaimTypes.cs
 │   └── JwtServiceCollectionExtensions.cs
 └── Authorization
     ├── PermissionAttribute.cs
     ├── PermissionRequirement.cs
     ├── PermissionAuthorizationHandler.cs
     ├── PermissionPolicyProvider.cs
     └── AuthorizationServiceCollectionExtensions.cs

以上封装完成后,业务项目无需关心 JWT 的底层校验细节,也无需在每个接口中重复编写权限判断代码。Controller 只负责声明访问规则,具体认证与授权逻辑由框架统一处理。

总结

本次完善后,框架具备了完整的 JWT 认证授权能力:登录后签发 Token,请求时自动校验 Token,接口通过注解控制访问权限,复杂权限通过策略授权统一扩展。后续如果需要接入菜单权限、按钮权限、租户隔离、数据范围控制,也可以继续沿用当前策略授权入口进行扩展,保证权限体系清晰、稳定、可维护。

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容