如何在.NET Core 5.0 + AspNet Core Identity项目中集成Keycloak实现双登录模式?
现有ASP.NET Core 5.0 WebAPI集成Keycloak双登录方案
方案核心思路
不用修改原有账号密码登录、JWT生成、授权校验的任何逻辑,仅扩展双JWT认证能力与新增Keycloak登录入口,实现两种登录方式完全兼容。
第一步:调整认证配置,支持双JWT源校验
修改你的AddCustomAuthentication扩展方法,新增Keycloak专属的JWT认证方案,同时配置默认授权策略可匹配任意一种认证方式通过:
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) { // 原有本地JWT配置 var localKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(configuration.GetSection("AppSettings:Token").Value)); services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer("LocalJwt", opt => { opt.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = localKey, ValidateAudience = false, ValidateIssuer = false, ClockSkew = TimeSpan.Zero }; opt.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { context.Response.Headers.Add("Token-Expired", "true"); } return Task.CompletedTask; } }; }) // 新增Keycloak JWT认证方案 .AddJwtBearer("KeycloakJwt", opt => { opt.Authority = configuration["Keycloak:Authority"]; // 格式:http://keycloak地址/realms/你的realm名称 opt.Audience = configuration["Keycloak:Audience"]; // 你在Keycloak中创建的客户端ID opt.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, ValidateAudience = true, ValidateIssuer = true, ClockSkew = TimeSpan.Zero, // 将Keycloak返回的角色Claim映射为你系统通用的Role Claim RoleClaimType = ClaimTypes.Role }; }); // 配置全局授权策略:任意一种JWT认证通过即可 services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder("LocalJwt", "KeycloakJwt") .RequireAuthenticatedUser() .Build(); }); return services; }
对应在appsettings.json中新增Keycloak配置项即可。
第二步:新增Keycloak登录CQRS入口,复用现有token生成逻辑
前端Angular先完成Keycloak侧的登录流程拿到Keycloak返回的access token后,调用后端新增的Keycloak登录接口,后端逻辑直接复用你现有用户校验、token生成的能力,返回和原有登录完全一致的token结构,前端后续请求无需任何修改:
新增KeycloakLoginCommand处理逻辑
public class KeycloakLoginCommand : IRequest<GetToken> { public string KeycloakAccessToken { get; set; } } public class KeycloakLoginCommandHandler : IRequestHandler<KeycloakLoginCommand, GetToken> { private readonly IConfiguration _config; private readonly UserManager<User> _userManager; private readonly JwtGenerator _jwtGenerator; private readonly ApplicationDbContext _context; public KeycloakLoginCommandHandler(IConfiguration config, UserManager<User> userManager, JwtGenerator jwtGenerator, ApplicationDbContext context) { _config = config; _userManager = userManager; _jwtGenerator = jwtGenerator; _context = context; } public async Task<GetToken> Handle(KeycloakLoginCommand request, CancellationToken cancellationToken) { // 1. 校验Keycloak Token有效性 var handler = new JwtSecurityTokenHandler(); var validationParameters = new TokenValidationParameters { ValidIssuer = _config["Keycloak:Authority"], ValidAudience = _config["Keycloak:Audience"], IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => { var httpClient = new HttpClient(); var keySet = httpClient.GetFromJsonAsync<JsonWebKeySet>($"{_config["Keycloak:Authority"]}/protocol/openid-connect/certs", cancellationToken).Result; return keySet.GetKeyById(kid); }, ValidateIssuer = true, ValidateAudience = true, ValidateIssuerSigningKey = true, ClockSkew = TimeSpan.Zero }; try { var claimsPrincipal = handler.ValidateToken(request.KeycloakAccessToken, validationParameters, out _); // 2. 从Keycloak Token中获取邮箱匹配本地用户 var email = claimsPrincipal.FindFirstValue(ClaimTypes.Email); var user = await _userManager.FindByEmailAsync(email); // 可选逻辑:如果本地不存在该用户,可自动创建对应账号 if (user == null) throw new BadRequestException("User not found in local system"); UserManagement.ForbiddenForLoginUser(user); // 3. 完全复用原有登录的token生成、refresh token存储逻辑 user.IsRoleChanged = false; RefreshToken refreshToken = new RefreshToken { Name = _jwtGenerator.GenerateRefreshToken(), DeviceName = $"{user.UserName}---{_jwtGenerator.GenerateRefreshToken()}", User = user, Expiration = DateTime.UtcNow.AddHours(4) }; await _context.RefreshTokens.AddAsync(refreshToken, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return new GetToken(_jwtGenerator.CreateToken(user), refreshToken.Name); } catch (SecurityTokenException) { throw new BadRequestException("Invalid Keycloak token"); } } }
第三步:授权逻辑零修改适配
你原有全局授权配置、CustomAuthorizationAttribute都是基于ClaimTypes.Role做校验,两种登录方式生成的Claims身份都会携带该字段,无需做任何调整即可同时适配两种登录的权限校验。
场景1适配说明
针对admin@gmail.com同时存在本地数据库和Keycloak的场景,两种登录方式都会匹配到同一个本地用户,最终角色权限以你本地aspnet users表的role字段为准。如果需要以Keycloak侧的角色为准,仅需在Keycloak登录逻辑中用Keycloak返回的角色值更新本地用户的role字段即可。
注意事项
- Keycloak客户端配置需要开启对应登录流(推荐授权码流),并添加Mapper将角色信息映射到Token的
roleClaim中 - 你原有本地账号密码登录的所有逻辑完全保留,没有任何侵入性修改
内容的提问来源于stack exchange,提问作者Arzu Suleymanov
相关产品推荐
相关产品推荐

