ASP.NET Core的依赖注入(DI)和认证授权是现代网站开发中最核心的两大技术支柱。简单来说,依赖注入让你的代码模块之间松耦合、好测试、易维护;认证授权则解决"你是谁"和"你能干什么"这两个安全问题。这两套机制在ASP.NET Core中深度融合,通过内置的IoC容器统一管理服务生命周期,配合Identity体系或JWT令牌实现细粒度的权限控制。下面我会从原理、配置、实战代码到最佳实践,把这套东西给你讲透。
一、ASP.NET Core依赖注入的核心原理依赖注入本质上是一种设计模式,叫做控制反转(IoC)。传统写法是你在类里面自己new一个依赖对象,现在反过来,由外部容器把依赖"注入"进来。ASP.NET Core内置了一个轻量级的IoC容器,在Program.cs启动时就已经配置好了。你只需要在ConfigureServices方法里注册服务,框架在运行时自动帮你解析和创建实例。
服务注册有三种生命周期,这是面试和实战都必须搞清楚的点:
// 单例:整个应用生命周期只创建一次 builder.Services.AddSingleton<IUserService, UserService>(); // 作用域:每个HTTP请求创建一次,同一请求内共享同一个实例 builder.Services.AddScoped<IOrderService, OrderService>(); // 瞬时:每次请求服务时都创建一个新实例 builder.Services.AddTransient<IEmailService, EmailService>();
选错生命周期会导致严重的Bug。比如数据库上下文DbContext必须用Scoped,因为它跟HTTP请求绑定;无状态的工具类用Transient没问题;缓存、配置读取这类东西用Singleton最合适。
二、认证(Authentication)的完整配置流程认证解决的是"用户身份验证"问题。ASP.NET Core支持Cookie认证、JWT Bearer认证、OAuth2.0、OpenID Connect等多种方案。最常见的组合是Cookie做本地登录认证,JWT做API接口认证。
第一步,在Program.cs中注册认证服务:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "your-app",
ValidAudience = "your-app-users",
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("your-secret-key-at-least-32-chars!"))
};
});
第二步,在中间件管道中启用认证和授权:
app.UseAuthentication(); app.UseAuthorization();
注意顺序,UseAuthentication必须在UseAuthorization之前,否则授权中间件拿不到已认证的用户信息。这是很多开发者踩坑的地方。
三、授权(Authorization)的三种核心模式认证通过之后,授权决定用户能访问哪些资源。ASP.NET Core提供了三种主要的授权方式,实际项目中往往混合使用。
1. 基于角色的授权(Role-Based)
最简单直接,给用户分配角色,控制器或方法上标注[Authorize(Roles = "Admin")]。适合权限层级少的系统。
[Authorize(Roles = "Admin")]
[HttpGet("admin/dashboard")]
public IActionResult AdminDashboard()
{
return Ok("Welcome Admin");
}
2. 基于策略的授权(Policy-Based)
更灵活,可以定义复杂的规则组合。比如"必须是管理员且来自内网IP"。策略在Program.cs中注册:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminAndInternal", policy =>
policy.RequireRole("Admin")
.RequireClaim("Department", "IT")
.RequireUserName("zhangsan"));
});
使用时指定策略名:
[Authorize(Policy = "RequireAdminAndInternal")]
[HttpPost("sensitive-operation")]
public IActionResult SensitiveOperation()
{
return Ok("Operation completed");
}
3. 基于资源的授权(Resource-Based / IAuthorizationHandler)
当授权逻辑涉及具体数据对象时,比如"用户只能编辑自己创建的订单",就需要自定义授权处理器。这是最硬核也最实用的方式。
public class OrderOwnerHandler : AuthorizationHandler<OrderOwnerRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OrderOwnerRequirement requirement)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var orderId = context.Resource as int?;
if (userId != null && orderId.HasValue)
{
// 这里调用数据库或服务判断订单归属
if (IsOrderOwner(userId, orderId.Value))
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
private bool IsOrderOwner(string userId, int orderId)
{
// 实际项目中查询数据库
return true; // 简化示例
}
}
然后注册这个Handler:
builder.Services.AddSingleton<IAuthorizationHandler, OrderOwnerHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("OrderOwnerOnly", policy =>
policy.Requirements.Add(new OrderOwnerRequirement()));
});
四、依赖注入与认证授权的深度结合
这才是真正体现ASP.NET Core设计功力的地方。认证授权过程中需要用到的服务,全部通过DI注入,而不是硬编码。比如你自定义了一个UserService来查询用户角色和权限,在授权处理器里直接构造函数注入:
public class OrderOwnerHandler : AuthorizationHandler<OrderOwnerRequirement>
{
private readonly IUserService _userService;
private readonly IOrderRepository _orderRepo;
public OrderOwnerHandler(IUserService userService, IOrderRepository orderRepo)
{
_userService = userService;
_orderRepo = orderRepo;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OrderOwnerRequirement requirement)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var orderId = context.Resource as int?;
if (userId != null && orderId.HasValue)
{
var isOwner = await _orderRepo.CheckOwnershipAsync(userId, orderId.Value);
if (isOwner)
context.Succeed(requirement);
}
}
}
这样做的好处是:测试时可以Mock这些依赖,业务逻辑和数据访问完全解耦。这也是为什么ASP.NET Core的授权体系比老版ASP.NET MVC强大得多的根本原因。
五、JWT令牌生成与刷新的实战方案实际项目中,登录接口需要返回JWT令牌,并且要支持令牌刷新机制。下面是一个完整的TokenService实现:
public class TokenService
{
private readonly IConfiguration _config;
private readonly IUserService _userService;
public TokenService(IConfiguration config, IUserService userService)
{
_config = config;
_userService = userService;
}
public string GenerateToken(User user)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Role, string.Join(",", user.Roles))
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Secret"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public string GenerateRefreshToken()
{
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
}
}
登录Controller调用这个服务,把token和refreshToken一起返回给前端。前端用accessToken访问受保护接口,过期后用refreshToken换新token,这是目前业界标准做法。
六、常见坑点与最佳实践总结第一,不要在Scoped服务中注入Singleton服务时直接使用,因为Singleton是全局唯一的,如果它内部持有了Scoped服务的引用,会导致该Scoped服务变成"伪单例",引发数据混乱。解决方案是注入IServiceProvider,在需要时手动创建作用域。
第二,授权中间件放的位置很关键。如果你的项目同时有MVC页面和API接口,建议在UseRouting之后、UseEndpoints之前统一放置UseAuthentication和UseAuthorization,避免某些路由绕过认证。
第三,Claims不要存太多数据。JWT令牌会随每个请求发送,Claims越多,HTTP头越大,影响性能。只存必要的标识信息,详细数据按需从数据库查。
第四,生产环境必须使用HTTPS,JWT的签名密钥要足够长且定期轮换,不要硬编码在代码里,用环境变量或密钥管理服务来管理。
第五,自定义授权策略时,尽量把复杂逻辑封装成独立的Requirement和Handler,保持Controller干净。一个项目里策略多了之后,集中管理比散落在各处标注要好维护得多。
七、总结ASP.NET Core的依赖注入和认证授权体系是一套高度集成、设计精良的框架能力。依赖注入管"怎么组装代码",认证授权管"怎么保护代码",两者通过IoC容器无缝衔接。掌握这套体系,你就能构建出结构清晰、安全可靠、易于扩展的企业级Web应用。核心要点记住三个:生命周期选对、中间件顺序别搞反、授权逻辑用策略和Handler封装。做到这三点,你的项目架构就已经超过市面上大部分ASP.NET Core项目了。
