zoukankan      html  css  js  c++  java
  • 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"
    ]
  • 相关阅读:
    $.ajax 中的contentType
    如何能让MAC和PC都能读写移动硬盘
    bootstrap中的明星属性
    SQL Server2012如何导出sql脚本并且还原数据库
    Http 请求头中 X-Requested-With 的含义
    jquery实现模拟select下拉框效果
    ASP.NET应用技巧:非托管COM组件的使用
    COM和.NET的互操作
    NET调用Com组件事例
    com组件
  • 原文地址:https://www.cnblogs.com/wjsgzcn/p/12753529.html
Copyright © 2011-2022 走看看