如何利用Azure AD令牌及自定义声明实现ASP.NET MVC对Web API的认证
方案可行性与实现指南
你的需求方案是完全可行的,但需要注意:Azure AD颁发的原生令牌无法直接修改(修改后签名会失效,API端的Azure AD认证会拒绝),因此需要通过以下两种思路实现自定义声明的传递,结合最佳实践给出具体方案:
核心思路澄清
Azure AD令牌由微软签名,任何修改都会导致验证失败,因此不能直接将自定义声明添加到该令牌中。需通过以下两种方案实现需求:
方案一:MVC应用签发自定义JWT(推荐)
逻辑流程
- 用户登录MVC应用获取Azure AD令牌
- MVC应用通过AD令牌中的用户唯一标识(如
oid)从数据库拉取自定义声明 - MVC应用自行签发包含AD原有核心声明+自定义声明的新JWT令牌
- 使用该自定义JWT调用Web API,API端验证此JWT并获取自定义声明
代码实现
MVC端:签发自定义JWT
using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.IdentityModel.Tokens; public class TokenService { private readonly IConfiguration _config; private readonly IUserClaimsRepository _claimsRepo; public TokenService(IConfiguration config, IUserClaimsRepository claimsRepo) { _config = config; _claimsRepo = claimsRepo; } public async Task<string> GenerateCustomApiToken(ClaimsPrincipal adUser) { // 获取Azure AD用户唯一标识(oid永久不变,比upn更可靠) var userId = adUser.FindFirstValue("oid"); if (string.IsNullOrEmpty(userId)) throw new InvalidOperationException("无法获取用户唯一标识"); // 从数据库拉取自定义声明 var customClaims = await _claimsRepo.GetUserClaimsAsync(userId); // 合并AD原有声明与自定义声明 var allClaims = adUser.Claims.ToList(); allClaims.AddRange(customClaims.Select(c => new Claim(c.ClaimType, c.ClaimValue))); // 配置JWT签名密钥与参数 var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:SecretKey"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _config["Jwt:Issuer"], // MVC应用地址 audience: _config["Jwt:ApiAudience"], // Web API的受众标识 claims: allClaims, expires: DateTime.UtcNow.AddMinutes(30), // 短有效期降低泄露风险 signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } }
Web API端:验证自定义JWT
在Program.cs中配置JWT认证:
builder.Services.AddAuthentication(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:ApiAudience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"])) }; }); // 启用认证与授权中间件 app.UseAuthentication(); app.UseAuthorization();
方案二:两端通过IClaimsTransformation加载声明
逻辑流程
- 用户登录MVC应用后,MVC通过
IClaimsTransformation从数据库加载自定义声明,附加到当前用户的ClaimsPrincipal - MVC调用Web API时,仍使用Azure AD令牌
- Web API收到请求后,同样通过
IClaimsTransformation,根据AD令牌中的用户标识从数据库加载自定义声明,附加到API端的ClaimsPrincipal
代码实现
通用IClaimsTransformation实现
public class CustomClaimsTransformer : IClaimsTransformation { private readonly IUserClaimsRepository _claimsRepo; private readonly IMemoryCache _cache; public CustomClaimsTransformer(IUserClaimsRepository claimsRepo, IMemoryCache cache) { _claimsRepo = claimsRepo; _cache = cache; } public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { // 避免重复加载声明 if (principal.HasClaim(c => c.Type == "CustomClaimType")) return principal; var userId = principal.FindFirstValue("oid"); if (string.IsNullOrEmpty(userId)) return principal; // 缓存自定义声明,减少数据库查询 var cacheKey = $"UserClaims_{userId}"; var customClaims = await _cache.GetOrCreateAsync(cacheKey, async entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); return await _claimsRepo.GetUserClaimsAsync(userId); }); // 创建新身份,保留原有声明并添加自定义声明 var identity = new ClaimsIdentity(principal.Identity); foreach (var claim in customClaims) { identity.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); } return new ClaimsPrincipal(identity); } }
在MVC与API中注册服务
在两端的Program.cs中添加:
builder.Services.AddScoped<IClaimsTransformation, CustomClaimsTransformer>(); builder.Services.AddMemoryCache(); // 用于缓存声明
最佳实践建议
- 优先选择方案一:API无需直接访问数据库,降低系统耦合;可精确控制传递给API的声明范围,避免敏感信息泄露。
- 统一用户标识:始终使用Azure AD的
oid(用户对象ID)作为数据库关联键,oid永久不变,比upn(用户主体名)更可靠。 - 缓存优化:无论哪种方案,都要缓存自定义声明,减少数据库查询次数,提升系统性能。
- 令牌安全:自定义JWT设置较短有效期(如30分钟),并实现刷新令牌机制;避免在令牌中存储敏感信息。
- API端可选双重验证:方案一中,API可额外验证原Azure AD令牌的有效性(通过Microsoft Graph的令牌验证端点),确保用户AD会话合法。
内容的提问来源于stack exchange,提问作者ZCoder
相关产品推荐
相关产品推荐

