ASP.NET Core Identity 与认证授权
大约 10 分钟约 3054 字
ASP.NET Core Identity 与认证授权
简介
ASP.NET Core Identity 是 .NET 官方提供的用户、角色、密码、令牌和登录管理框架,适合做站内账号体系、后台管理登录、企业内部认证等场景。它和 OAuth2 / OIDC 不是同一个层级:Identity 更偏“本地用户管理与认证基础设施”,而 OAuth2 / OIDC 更偏“跨系统授权与统一身份协议”。
特点
实现
基础模型与服务注册
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
public class AppUser : IdentityUser
{
public string? DisplayName { get; set; }
public DateTimeOffset? LastLoginAt { get; set; }
}
public class AppRole : IdentityRole
{
}
public class AppIdentityDbContext : IdentityDbContext<AppUser, AppRole, string>
{
public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options) : base(options)
{
}
}var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppIdentityDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services
.AddIdentity<AppUser, AppRole>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireUppercase = true;
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
options.User.RequireUniqueEmail = true;
options.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=SunnyFanIdentity;Trusted_Connection=True;TrustServerCertificate=True"
}
}用户注册与登录
[ApiController]
[Route("api/account")]
public class AccountController : ControllerBase
{
private readonly UserManager<AppUser> _userManager;
private readonly SignInManager<AppUser> _signInManager;
public AccountController(
UserManager<AppUser> userManager,
SignInManager<AppUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
[HttpPost("register")]
public async Task<IActionResult> Register(RegisterRequest request)
{
var user = new AppUser
{
UserName = request.Email,
Email = request.Email,
DisplayName = request.DisplayName
};
var result = await _userManager.CreateAsync(user, request.Password);
if (!result.Succeeded)
{
return BadRequest(result.Errors.Select(e => e.Description));
}
await _userManager.AddToRoleAsync(user, "User");
return Ok(new { message = "注册成功,请验证邮箱后登录" });
}
[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest request)
{
var result = await _signInManager.PasswordSignInAsync(
request.Email,
request.Password,
isPersistent: request.RememberMe,
lockoutOnFailure: true);
if (result.IsLockedOut) return BadRequest("账户已锁定,请稍后重试");
if (!result.Succeeded) return Unauthorized("用户名或密码错误");
return Ok(new { message = "登录成功" });
}
}
public record RegisterRequest(string Email, string Password, string DisplayName);
public record LoginRequest(string Email, string Password, bool RememberMe);// 创建默认角色(初始化时)
public static class IdentitySeed
{
public static async Task SeedAsync(IServiceProvider services)
{
using var scope = services.CreateScope();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<AppRole>>();
string[] roles = ["Admin", "User"];
foreach (var role in roles)
{
if (!await roleManager.RoleExistsAsync(role))
{
await roleManager.CreateAsync(new AppRole { Name = role });
}
}
}
}Cookie 认证与 JWT 认证
builder.Services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/login";
options.AccessDeniedPath = "/forbidden";
options.Cookie.Name = "sunnyfan_auth";
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.SlidingExpiration = true;
});using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
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 = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});public class JwtTokenService
{
private readonly IConfiguration _configuration;
public JwtTokenService(IConfiguration configuration)
{
_configuration = configuration;
}
public string Generate(AppUser user, IList<string> roles)
{
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty),
new(ClaimTypes.Name, user.UserName ?? string.Empty)
};
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddHours(2),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}授权、角色与 Claim 控制
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdmin", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("CanManageOrders", policy =>
policy.RequireClaim("permission", "orders.manage"));
});[ApiController]
[Route("api/admin")]
[Authorize(Policy = "RequireAdmin")]
public class AdminController : ControllerBase
{
[HttpGet("dashboard")]
public IActionResult Dashboard() => Ok(new { message = "管理员面板" });
}// 为用户追加 Claim
var user = await _userManager.FindByEmailAsync("admin@example.com");
if (user is not null)
{
await _userManager.AddClaimAsync(user, new Claim("permission", "orders.manage"));
}外部登录与安全增强
builder.Services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = builder.Configuration["Authentication:Google:ClientId"]!;
options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]!;
});// 忘记密码 / 重置密码流程核心用法
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var result = await _userManager.ResetPasswordAsync(user, token, "NewStrongPass123!");生产环境建议同时启用:
- 邮箱确认
- 登录失败锁定
- 强密码策略
- 关键后台二步验证(2FA)
- 审计日志自定义用户存储与扩展
自定义 ApplicationUser
// 扩展默认用户模型
public class ApplicationUser : IdentityUser
{
public string? FullName { get; set; }
public string? Avatar { get; set; }
public DateTimeOffset? LastLoginAt { get; set; }
public string? Department { get; set; }
public int LoginCount { get; set; }
}
public class AppDbContext : IdentityDbContext<ApplicationUser>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// 自定义表名前缀
builder.Entity<ApplicationUser>().ToTable("app_users");
builder.Entity<IdentityRole>().ToTable("app_roles");
builder.Entity<IdentityRoleClaim<string>>().ToTable("app_role_claims");
builder.Entity<IdentityUserClaim<string>>().ToTable("app_user_claims");
builder.Entity<IdentityUserLogin<string>>().ToTable("app_user_logins");
builder.Entity<IdentityUserRole<string>>().ToTable("app_user_roles");
builder.Entity<IdentityUserToken<string>>().ToTable("app_user_tokens");
}
}
// 配置 Identity 使用自定义模型
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();自定义 Claim 与权限
// 用户登录时添加自定义 Claim
public class CustomClaimsPrincipalFactory : IUserClaimsPrincipalFactory<ApplicationUser>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IRoleManager _roleManager;
public CustomClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager,
IRoleManager roleManager)
{
_userManager = userManager;
_roleManager = roleManager;
}
public async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
var principal = await _userManager.GenerateClaimsAsync(user);
var identity = (ClaimsIdentity)principal.Identity!;
// 添加自定义 Claim
identity.AddClaim(new Claim("FullName", user.FullName ?? ""));
identity.AddClaim(new Claim("Department", user.Department ?? ""));
identity.AddClaim(new Claim("Avatar", user.Avatar ?? ""));
identity.AddClaim(new Claim("LoginCount", user.LoginCount.ToString()));
// 从角色中提取权限 Claim
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
var roleClaims = await _roleManager.GetClaimsAsync(
await _roleManager.FindByNameAsync(role));
foreach (var claim in roleClaims)
{
identity.AddClaim(claim);
}
}
return principal;
}
}
// 注册自定义 ClaimsFactory
builder.Services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>,
CustomClaimsPrincipalFactory>();Resource-based 授权
// 基于资源的授权(精细到实体级别)
public class Article
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string AuthorId { get; set; } = string.Empty;
public bool IsPublished { get; set; }
}
public class ArticleAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Article>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Article resource)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (requirement.Name == "Edit")
{
// 只有作者或管理员可以编辑
if (resource.AuthorId == userId ||
context.User.IsInRole("Admin"))
{
context.Succeed(requirement);
}
}
if (requirement.Name == "Delete")
{
// 只有管理员可以删除
if (context.User.IsInRole("Admin"))
{
context.Succeed(requirement);
}
}
if (requirement.Name == "View")
{
// 已发布的文章所有人可见,未发布只有作者可见
if (resource.IsPublished || resource.AuthorId == userId)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
// 注册 Handler
builder.Services.AddSingleton<IAuthorizationHandler, ArticleAuthorizationHandler>();
// 在 Controller 中使用
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, UpdateArticleRequest request)
{
var article = await _articleRepository.GetByIdAsync(id);
if (article == null) return NotFound();
var result = await _authorizationService.AuthorizeAsync(
User, article, "Edit");
if (!result.Succeeded) return Forbid();
// 执行更新
return NoContent();
}JWT 与 Identity 集成
生成 JWT Token
public class TokenService
{
private readonly IConfiguration _config;
private readonly UserManager<ApplicationUser> _userManager;
public TokenService(IConfiguration config, UserManager<ApplicationUser> userManager)
{
_config = config;
_userManager = userManager;
}
public async Task<string> GenerateTokenAsync(ApplicationUser user)
{
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email ?? ""),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new("FullName", user.FullName ?? ""),
new("Department", user.Department ?? "")
};
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
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(
_config.GetValue<int>("Jwt:ExpireHours", 2)),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
// 刷新 Token(长期有效)
public string GenerateRefreshToken()
{
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
}
}
// 登录接口
[HttpPost("login")]
public async Task<ActionResult<LoginResponse>> Login(LoginRequest request)
{
var user = await _userManager.FindByEmailAsync(request.Email);
if (user == null || !await _userManager.CheckPasswordAsync(user, request.Password))
return Unauthorized(new { message = "邮箱或密码错误" });
var token = await _tokenService.GenerateTokenAsync(user);
var refreshToken = _tokenService.GenerateRefreshToken();
// 存储 Refresh Token
user.RefreshToken = refreshToken;
user.RefreshTokenExpiry = DateTime.UtcNow.AddDays(7);
await _userManager.UpdateAsync(user);
return Ok(new LoginResponse
{
AccessToken = token,
RefreshToken = refreshToken,
ExpiresIn = 7200
});
}Token 刷新与撤销
[HttpPost("refresh")]
public async Task<ActionResult<LoginResponse>> Refresh(RefreshTokenRequest request)
{
var user = await _userManager.Users
.FirstOrDefaultAsync(u => u.RefreshToken == request.RefreshToken);
if (user == null || user.RefreshTokenExpiry < DateTime.UtcNow)
return Unauthorized(new { message = "Refresh Token 无效或已过期" });
var token = await _tokenService.GenerateTokenAsync(user);
var newRefreshToken = _tokenService.GenerateRefreshToken();
user.RefreshToken = newRefreshToken;
user.RefreshTokenExpiry = DateTime.UtcNow.AddDays(7);
await _userManager.UpdateAsync(user);
return Ok(new LoginResponse
{
AccessToken = token,
RefreshToken = newRefreshToken,
ExpiresIn = 7200
});
}
[HttpPost("revoke")]
[Authorize]
public async Task<IActionResult> Revoke()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _userManager.FindByIdAsync(userId!);
if (user != null)
{
user.RefreshToken = null;
user.RefreshTokenExpiry = null;
await _userManager.UpdateAsync(user);
}
return NoContent();
}登录安全增强
登录审计日志
public class LoginAuditService
{
private readonly AppDbContext _context;
public LoginAuditService(AppDbContext context) => _context = context;
public async Task RecordAsync(
string userId, string ip, string userAgent, bool success)
{
var audit = new LoginAudit
{
UserId = userId,
IpAddress = ip,
UserAgent = userAgent,
Success = success,
Timestamp = DateTimeOffset.UtcNow
};
_context.Set<LoginAudit>().Add(audit);
await _context.SaveChangesAsync();
}
// 检测异常登录(不同地区、短时间内多次失败)
public async Task<bool> IsSuspiciousAsync(string userId, string currentIp)
{
var recentFailures = await _context.Set<LoginAudit>()
.Where(a => a.UserId == userId && !a.Success)
.Where(a => a.Timestamp > DateTimeOffset.UtcNow.AddHours(-1))
.CountAsync();
var previousIps = await _context.Set<LoginAudit>()
.Where(a => a.UserId == userId && a.Success)
.Select(a => a.IpAddress)
.Distinct()
.ToListAsync();
// 1小时内失败超过5次 或 使用了新IP
return recentFailures > 5 || !previousIps.Contains(currentIp);
}
}双因素认证(2FA)
// 启用 2FA
[HttpPost("enable-2fa")]
[Authorize]
public async Task<IActionResult> Enable2Fa(Enable2FaRequest request)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _userManager.FindByIdAsync(userId!);
if (user == null) return NotFound();
// 验证 TOTP 代码
var isValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, request.Code);
if (!isValid)
return BadRequest(new { message = "验证码无效" });
await _userManager.SetTwoFactorEnabledAsync(user, true);
return Ok(new { message = "双因素认证已启用" });
}
// 获取恢复码
[HttpGet("recovery-codes")]
[Authorize]
public async Task<IActionResult> GetRecoveryCodes()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _userManager.FindByIdAsync(userId!);
if (user == null) return NotFound();
var codes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
return Ok(new { codes });
}优点
缺点
缺点
总结
ASP.NET Core Identity 更适合用来解决“我的系统如何管理用户和登录”这个问题,而不是直接解决“多个系统之间如何统一授权”这个问题。对大多数业务后台和站内账号体系来说,Identity 已经足够强大;如果后续需要统一身份中心,再考虑 Duende、OpenIddict、Keycloak 等更偏协议中心的方案。
关键知识点
- Identity 是本地身份管理框架,不等同于 OAuth2 / OIDC 授权服务器。
- 角色适合粗粒度授权,Claim/Policy 更适合细粒度授权。
- Cookie 更适合传统 Web,JWT 更适合前后端分离 API。
- 用户安全策略(锁定、邮箱确认、2FA)比“能不能登录”更重要。
项目落地视角
- 管理后台通常最适合 Identity + Cookie 认证。
- 前后端分离 API 常见做法是 Identity 管用户,JWT 管 API 访问。
- 多租户系统通常会在 Identity 基础上增加 Tenant 维度和权限隔离。
- 企业系统接第三方登录时,要同步考虑本地用户绑定与角色同步。
常见误区
- 把 Identity 当成 OAuth2 身份中心直接硬用到多系统统一认证场景。
- 所有权限都只靠角色控制,导致权限粒度过粗。
- 只完成登录功能,不做邮箱确认、锁定、密码重置和审计。
- 直接把敏感 Claim 放进 JWT,却没有认真考虑泄露范围和过期时间。
进阶路线
- 学习 Policy-based Authorization 与 Resource-based Authorization。
- 研究 OpenIddict、Duende IdentityServer、Keycloak 等统一身份方案。
- 接入 2FA、MFA、Passkey/WebAuthn 等更现代认证机制。
- 建立用户安全审计、登录风控和权限变更追踪体系。
适用场景
- ASP.NET Core 后台管理系统。
- 企业内部系统、本地账号体系。
- 前后端分离应用的用户和权限管理。
- 需要角色、Claim、密码策略、外部登录的业务系统。
落地建议
- 先明确:你需要的是本地身份管理,还是统一身份中心。
- 从角色 + Policy 开始,不要一开始就设计过度复杂的权限模型。
- 所有认证授权相关操作都记录审计日志。
- 敏感配置(JWT Key、外部登录 Secret)必须放到安全配置源里。
排错清单
- 登录失败时先看密码策略、邮箱确认和锁定状态。
- 授权失败时先检查
[Authorize]、角色、Claim 和 Policy 是否一致。 - JWT 不生效时先看 issuer、audience、签名密钥和过期时间。
- 外部登录异常时先检查回调地址、ClientId/Secret 和 HTTPS 配置。
复盘问题
- 你现在更需要“本地身份管理”还是“统一身份中心”?
- 现有权限控制是基于角色、Claim,还是还停留在接口里硬编码判断?
- 登录和授权问题出现时,团队能否快速定位到认证、签名、Claim 还是策略层?
- 如果明天接入 SSO 或外部登录,当前账户体系是否容易演进?
