ASP.NET Core 认证鉴权实战:JWT、Policy 与权限边界怎么落地
实现场一个后台退款接口原本只允许财务角色调用但线上排查发现普通运营账号只要拿到有效 token也能调用成功。根因并不复杂接口加了[Authorize]系统只校验“是否登录”没有继续校验角色、权限和资源归属结果就是认证做了授权却只做了一半。这也是很多系统的共性问题。认证只是在回答“你是谁”授权回答的是“你能做什么”。如果这两件事没有拆开设计接口表面安全实际边界会很模糊。原理解析认证解决身份确认认证的目标是确认当前请求对应的是哪个用户、哪个客户端常见做法就是校验 JWT 的签名、过期时间、签发方和受众。这一步做完后系统拿到的是一个ClaimsPrincipal。它说明“请求身份可信”但并不说明这个身份就有所有权限。授权解决操作范围授权是在认证之后对用户能力做进一步判断。在 ASP.NET Core 里最常见的落点是 Policy。你可以按角色、权限声明、租户、部门或业务规则定义策略而不是在控制器里到处手写 if 判断。角色不等于权限模型很多系统一开始只有Admin、Operator、User这几个角色后来业务一复杂就会发现角色粒度太粗。更稳妥的方式通常是角色用于粗粒度分组权限声明用于精细操作控制例如“财务”和“运营”都属于后台用户但是否允许退款、导出、调价应该由权限声明决定而不是只靠角色名硬编码。资源级授权才是真正的边界就算用户具备orders.refund权限也不代表他可以操作所有订单。很多越权问题出在这里接口只校验了功能权限没有校验资源归属比如租户是否匹配、门店是否匹配、是否只能操作自己负责的数据。所以完整授权通常分两层功能级你有没有这个动作权限资源级你能不能对这条具体数据执行这个动作示例代码下面用一个“订单退款接口”来说明一套常见落地方式。先配置 JWT 认证using Microsoft.AspNetCore.Authentication.JwtBearer;using Microsoft.IdentityModel.Tokens;using System.Text;builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options {options.TokenValidationParameters new TokenValidationParameters{ValidateIssuer true,ValidateAudience true,ValidateIssuerSigningKey true,ValidateLifetime true,ValidIssuer builder.Configuration[Jwt:Issuer],ValidAudience builder.Configuration[Jwt:Audience],IssuerSigningKey new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration[Jwt:SigningKey]!)),ClockSkew TimeSpan.FromSeconds(30)};});再定义基于权限声明的授权策略builder.Services.AddAuthorization(options {options.AddPolicy(OrdersRefund, policy {policy.RequireAuthenticatedUser();policy.RequireClaim(permission, orders.refund);});});如果 token 里的声明长这样{sub: 1001,name: alice,tenant_id: t-01,permission: [orders.read, orders.refund]}那么接口级功能授权可以这样写app.MapPost(/api/orders/{id:long}/refund,async (long id,RefundRequest request,IAuthorizationService authorizationService,ClaimsPrincipal user,OrderRefundService service,CancellationToken ct) {var result await service.RefundAsync(id, request, user, ct);return result ? Results.Ok() : Results.Forbid();}).RequireAuthorization(OrdersRefund);但这样还不够。因为用户即使有退款权限也未必能退任意租户、任意门店的订单。所以业务层还要做资源级校验public sealed class OrderRefundService{private readonly AppDbContext _db;public OrderRefundService(AppDbContext db){_db db;}public async Taskbool RefundAsync(long orderId,RefundRequest request,ClaimsPrincipal user,CancellationToken ct){var tenantId user.FindFirst(tenant_id)?.Value;if (string.IsNullOrWhiteSpace(tenantId)){return false;}var order await _db.Orders.FirstOrDefaultAsync(x x.Id orderId, ct);if (order is null){return false;}if (!string.Equals(order.TenantId, tenantId, StringComparison.Ordinal)){return false;}if (order.Status ! OrderStatus.Paid){return false;}order.Status OrderStatus.Refunded;order.RefundReason request.Reason;order.RefundedAt DateTime.UtcNow;await _db.SaveChangesAsync(ct);return true;}}如果你希望把这类判断进一步收敛到授权层也可以自定义 Requirement 和 Handlerpublic sealed class SameTenantRequirement : IAuthorizationRequirement{}public sealed class SameTenantHandler : AuthorizationHandlerSameTenantRequirement, Order{protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,SameTenantRequirement requirement,Order resource){var tenantId context.User.FindFirst(tenant_id)?.Value;if (!string.IsNullOrWhiteSpace(tenantId) tenantId resource.TenantId){context.Succeed(requirement);}return Task.CompletedTask;}}这种方式的价值在于功能权限和资源权限都能被组织成一致的授权模型而不是散落在各个接口里。工程实践建议不要把[Authorize]当成权限治理的终点[Authorize]只能说明这个接口需要登录不能说明权限模型已经设计正确。真正需要明确的是这个接口到底限制到角色、权限、租户、组织还是具体资源。Claim 设计要稳定不要随业务字段漂移JWT 里的声明一旦进入多个服务就会变成契约。建议优先保留稳定字段例如用户 ID、租户 ID、权限编码不要把频繁变化的展示信息和大块业务数据塞进 token。权限编码要业务化与其用Admin、Manager这种泛化概念不如直接定义orders.refund、orders.export、products.adjust-price这类权限编码。这样做的好处是边界清晰也更适合做前后端联动和审计。认证失败、授权失败要能区分401 和 403 不是一回事。401 表示身份无效或缺失
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2466402.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!