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项目了。