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"
]

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM