Keycloak & Asp.net core webapi 整合跳坑之旅
前言
之前,一直使用IdentityServer4作为.net core程序的外部身份认证程序,ID4的优点自不必说了,缺点就是缺乏完善的管理界面。
后来,学习java quarkus框架时,偶然遇到了keycloak,具备完善的管理界面,并且支持多个realms,和quarkus oidc结合非常完美,于是就思考能否用keycloak来控制.net core程序的身份认证。
准备工作
dotnet new webapi,创建一个默认的webapi项目
安装keycloak的docker版本,我这里使用mariadb来持久化keycloak的数据,贴出docker-compose文件如下:
version: '3' services: keycloak: image: jboss/keycloak:9.0.3 environment: KEYCLOAK_USER: admin KEYCLOAK_PASSWORD: admin DB_USER: keycloak DB_PASSWORD: password ports: - 8180:8080 mariadb: image: mariadb:10.4 command: ['--character-set-server=utf8','--collation-server=utf8_general_ci','--default-time-zone=+8:00'] environment: MYSQL_ROOT_PASSWORD: example MYSQL_DATABASE: keycloak MYSQL_USER: keycloak MYSQL_PASSWORD: password volumes: - mariadata:/var/lib/mysql volumes: mariadata:
docker-compose up 启动keycloak,然后可以在 http://localhost:8180 访问管理界面。
不要使用默认的realm,新建一个realm,比如“test2”。
然后新建client,比如“webapi”,地址填写 http://localhost:5000, 就是asp.net core webapi程序即将运行的地址。
然后创建角色和用户。
代码编写
修改Controllers/WeatherForcastController.cs
在控制器类前面增加[Authorize], 并且修改反馈的内容,方便调试。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Security.Claims; 5 using System.Threading.Tasks; 6 using Microsoft.AspNetCore.Authorization; 7 using Microsoft.AspNetCore.Mvc; 8 using Microsoft.Extensions.Logging; 9 10 namespace WebApi1.Controllers 11 { 12 [Authorize] 13 [ApiController] 14 [Route("[controller]")] 15 public class WeatherForecastController : ControllerBase 16 { 17 private static readonly string[] Summaries = new[] 18 { 19 "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 20 }; 21 22 private readonly ILogger<WeatherForecastController> _logger; 23 24 public WeatherForecastController(ILogger<WeatherForecastController> logger) 25 { 26 _logger = logger; 27 } 28 29 [HttpGet] 30 public IEnumerable<string> Get() 31 { 32 var result = new List<string>(); 33 foreach (var claim in User.Claims) 34 result.Add(claim.Type+": "+claim.Value); 35 36 result.Add("username: " + User.Identity.Name); 37 result.Add("IsAdmin: " + User.IsInRole("admin").ToString()); 38 return result; 39 } 40 } 41 }
注意12行。
修改startup.cs
1 namespace WebApi1 2 { 3 public class Startup 4 { 5 public Startup(IConfiguration configuration) 6 { 7 Configuration = configuration; 8 } 9 10 public IConfiguration Configuration { get; } 11 12 // This method gets called by the runtime. Use this method to add services to the container. 13 public void ConfigureServices(IServiceCollection services) 14 { 15 services.AddControllers(); 16 17 services.AddAuthentication(options => 18 { 19 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 20 options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 21 }).AddJwtBearer(options => 22 { 23 options.Authority = "http://localhost:8180/auth/realms/test2"; 24 options.RequireHttpsMetadata = false; 25 options.Audience = "account"; 26 options.TokenValidationParameters = new TokenValidationParameters{ 27 NameClaimType = "preferred_username" 28 }; 29 30 options.Events = new JwtBearerEvents{ 31 OnTokenValidated = context =>{ 32 var identity = context.Principal.Identity as ClaimsIdentity; 33 var access = context.Principal.Claims.FirstOrDefault(p => p.Type == "realm_access"); 34 var jo = JObject.Parse(access.Value); 35 foreach (var role in jo["roles"].Values()){ 36 identity.AddClaim(new Claim(ClaimTypes.Role, role.ToString())); 37 } 38 return Task.CompletedTask; 39 } 40 }; 41 }); 42 } 43 44 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 45 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 46 { 47 if (env.IsDevelopment()) 48 { 49 app.UseDeveloperExceptionPage(); 50 } 51 52 //app.UseHttpsRedirection(); 53 54 IdentityModelEventSource.ShowPII = true; 55 56 app.UseRouting(); 57 58 app.UseAuthentication(); 59 app.UseAuthorization(); 60 61 app.UseEndpoints(endpoints => 62 { 63 endpoints.MapControllers(); 64 }); 65 } 66 } 67 }
这里的代码是遇到几个坑并解决之后的结果,下面列举遇到的坑和解决方法:
1、使用postman获取token之后,访问资源仍提示401,查看具体错误信息是audience=account,但是我们根据各种教程设置为webapi(同client-id)
第25行,设置audience=account后解决。
到现在也不知道为啥keycloak返回的是account而不是client-id。
2、控制器中User.Identity.Name=null
这主要源于ClaimType名称的问题,keycloak返回的claims中,使用preferred_username来表示用户名,和asp.net core identity默认的不同
第26行,修改默认的Claim名称后,User.Identity.Name可以正常返回用户名。
3、控制器中无法获取角色信息
和用户名类似,也是因为ClaimType问题,keycloak返回的角色信息claim名称是realm_access,而且内容是一段json文本,需要解析处理。
第30行,OnTokenValidated 事件中对角色Claim进行转换,然后角色信息正常。
修改后就可以使用[Authorize(Roles="admin")]来保护控制器或者方法了。
最后列举WeatherForecastController 的Get方法返回的各种claims和其他信息
[ "exp: 1587544810", "iat: 1587544510", "jti: 72648e7f-3bb4-4db1-b866-33cc26a5e5a1", "iss: http://localhost:8180/auth/realms/test2", "aud: account", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: 8811d051-52a6-40fc-b7f3-15d949fb25cd", "typ: Bearer", "azp: webapi", "session_state: a9fb6a90-368b-4619-8789-43e26c7f2b85", "http://schemas.microsoft.com/claims/authnclassreference: 1", "allowed-origins: http://localhost:5000", "realm_access: {\"roles\":[\"offline_access\",\"admin\",\"uma_authorization\"]}", "resource_access: {\"account\":{\"roles\":[\"manage-account\",\"manage-account-links\",\"view-profile\"]}}", "scope: email profile", "email_verified: false", "preferred_username: admin", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role: offline_access", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role: admin", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role: uma_authorization", "username: admin", "IsAdmin: True" ]