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"
]
原文地址:https://www.cnblogs.com/wjsgzcn/p/12753529.html