本章目标

  1. 完成Keycloak的本地部署与配置
  2. 在Stickers RESTful API层面完成与Keycloak的集成
  3. 在Stickers RESTful API上实现认证与授权

Keycloak的本地部署

Keycloak的本地部署最简单的方式就是使用Docker。可以根据官方文档构建Dockerfile,然后使用Docker Compose直接运行。由于Keycloak也是基础设施的一部分,所以可以直接加到我们在上一讲使用的docker-compose.dev.yaml文件中。同样,在docker文件夹下新建一个keycloak的文件夹,然后新建一个Dockerfile,内容如下:

FROM quay.io/keycloak/keycloak:26.0 AS builder

# Enable health and metrics support
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true # Configure a database vendor
ENV KC_DB=postgres WORKDIR /opt/keycloak
# for demonstration purposes only, please make sure to use proper certificates in production instead
RUN keytool -genkeypair -storepass password -storetype PKCS12 -keyalg RSA -keysize 2048 -dname "CN=server" -alias server -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -keystore conf/server.keystore
RUN /opt/keycloak/bin/kc.sh build FROM quay.io/keycloak/keycloak:26.0
COPY --from=builder /opt/keycloak/ /opt/keycloak/ ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]

然后修改docker-compose.dev.yaml文件,加入一个名为stickers-keycloak的新的service:

stickers-keycloak:
image: daxnet/stickers-keycloak:dev
build:
context: ./keycloak
dockerfile: Dockerfile
environment:
- KC_DB=postgres
- KC_DB_USERNAME=postgres
- KC_DB_PASSWORD=postgres
- KC_DB_SCHEMA=public
- KC_DB_URL=jdbc:postgresql://stickers-pgsql:5432/stickers_keycloak?currentSchema=public
- KC_HOSTNAME=localhost
- KC_HOSTNAME_PORT=5600
- KC_HTTP_ENABLED=true
- KC_HOSTNAME_STRICT=false
- KC_HOSTNAME_STRICT_HTTPS=false
- KC_PROXY=edge
- KC_BOOTSTRAP_ADMIN_USERNAME=admin
- KC_BOOTSTRAP_ADMIN_PASSWORD=admin
- QUARKUS_TRANSACTION_MANAGER_ENABLE_RECOVERY=true
command: [
'start',
'--optimized'
]
depends_on:
- stickers-pgsql
ports:
- "5600:8080"

在这些环境变量中,KC_DB指定了Keycloak所使用的数据库类型,我们打算复用上一讲中所使用的PostgreSQL数据库,所以这里填写postgresKC_DB_USERNAMEKC_DB_PASSWORDKC_DB_SCHEMAKC_DB_URL指定了数据库的用户名、密码、schema名称以及数据库连接字符串。KC_HOSTNAMEKC_HOSTNAME_PORT指定了Keycloak运行的主机名和端口号,这个端口号需要跟ports里指定的对外端口号一致。KC_BOOTSTRAP_ADMIN_USERNAMEKC_BOOTSTRAP_ADMIN_PASSWORD指定了Keycloak默认的管理员名称和密码。

在启动Keycloak之前,还需要准备好PostgreSQL数据库,Keycloak启动后会自动连接数据库并创建数据库对象(表、字段、关系等等)。准备数据库也非常简单,继续沿用上一讲介绍的方法,在构建PostgreSQL数据库镜像的时候,将创建数据库的SQL文件复制到镜像中的/docker-entrypoint-initdb.d文件夹中即可。SQL文件包含以下内容:

SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off; CREATE DATABASE stickers_keycloak WITH TEMPLATE = template0 ENCODING = 'UTF8' LOCALE_PROVIDER = libc LOCALE = 'en_US.utf8';
ALTER DATABASE stickers_keycloak OWNER TO postgres;

然后重新构建并运行PostgreSQL和Keycloak容器:

$ docker compose -f docker-compose.dev.yaml build
$ docker compose -f docker-compose.dev.yaml up

强烈建议在重建和运行容器之前,清除本地的stickers-pgsql:dev镜像,并且删除docker_stickers_postgres_data卷,以确保旧数据不会影响新的部署。

在成功启动容器之后,打开浏览器访问http://localhost:5600,应该可以打开Keycloak的主页面并用admin/admin进行登录。

在Keycloak中配置Stickers Realm

限于篇幅,这里就不把配置Keycloak的整个过程一一展示出来了,请移步到我之前写的几篇文章查看详细步骤:

请根据上面两篇文章的步骤,进行如下的配置:

  1. 新建一个名为stickers的Realm
  2. 切换到stickers Realm,新建一个名为public的Client
  3. public Client下启用Direct access grants(暂时启用,用作测试)
  4. 新建一个名为usergroups的Client Scope,在这个client scope中,添加一个类型为Group Membership的client scope。将其Token Claim Name设置为groups。然后将这个client scope添加到public Client下
  5. public Client下新建两个角色:administratorregular_user,然后新建三个用户:daxnetnobodysuper并设置密码,然后创建一个名为public的group(名称与Client的名称一致),在public group下,新建users group,再在users group下,新建administrators group。将daxnet添加到users group,将super添加到administrators group,并将users group赋予regular_user角色,将administrators group赋予administrator角色
  6. public Client的Authorization配置中,创建四个Scope:admin.manage_usersstickers.readstickers.updatestickers.delete;然后创建两个resource:admin-api,它具有admin.manage_users scope,以及stickers-api,它具有stickers.readstickers.updatestickers.delete这三个scope
  7. 在public Client下,创建两个基于角色的Policy:require-admin-policy,它分配了administrator角色,以及require-registered-user-policy,它分配了regular_user角色
  8. 在Permissions下,创建四个Permission:
    1. admin-manage-users-permission:基于require-admin-policy,作用在admin.manage_users Scope
    2. stickers-view-permission:基于require-registered-user-policy,作用在stickers.read Scope
    3. stickers-update-permission:基于require-registered-user-policy,作用在stickers.update Scope
    4. stickers-delete-permission:基于require-registered-user-policy,作用在stickers.delete Scope

你可以参考上面列出的两篇文章和这些步骤来配置Keycloak,也可以使用本章的代码直接编译Keycloak Docker镜像然后直接运行容器,Keycloak容器运行起来之后,所有的配置都会自动导入,此时就可以使用根据界面上的设置,比对上面的步骤进行学习了。

在完成Keycloak端的配置之后,就可以开始修改Stickers.WebApi项目,使我们的API支持认证与授权了。

在Stickers.WebApi中启用认证机制

关于什么是认证,什么是授权,这里就不多作讨论了,网上相关文章很多,也可以通过ChatGPT获得详细的解释和介绍。我们首先实现一个目标,就是只允许注册用户可以访问Stickers微服务,而不管这些用户是不是真的具有访问其中的某些API的权限。我这里用粗体字强调了“注册用户”和“权限”两个概念,也就可以区分出什么是认证,什么是授权了,通俗地说:认证就是该用户是否被允许使用网站的服务,授权就是在允许使用网站服务的前提下,该用户是否可以对其中的某些功能进行操作。

在ASP.NET Core中,集成认证与授权机制是非常容易的,首先,向Stickers.WebApi项目添加Microsoft.AspNetCore.Authentication.JwtBearer NuGet包,然后在Program.cs中,加入如下代码:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "http://localhost:5600/realms/stickers";
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
NameClaimType = "preferred_username",
RoleClaimType = ClaimTypes.Role,
ValidateIssuer = true,
ValidateAudience = false
};
});

上面的代码用来初始化ASP.NET Core的认证机制,我们使用Jwt Bearer Token的认证模块,在配置中,指定认证机构Authority为stickers Realm的Base URL,然后对token的认证进行参数配置。这里的NameClaimType指定了在解析access token的时候,应该将哪个Claim看成是用户名称,同理,RoleClaimType指定了应该将哪个Claim看成是用户角色。在启动了PostgreSQL和Keycloak容器之后,可以使用类似下面的cURL命令获得access token:

$ curl --location 'http://localhost:5600/realms/stickers/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=public' \
--data-urlencode 'client_secret=B2REunrXWN57KtQoJWoP2Dhr7gqKJrol' \
--data-urlencode 'username=daxnet' \
--data-urlencode 'password=daxnet'

然后打开jwt.io,将这个access token复制到Debugger的Encoded部分,在Decoded部分可以看到,用户名是在preferred_username字段指定的,这就是NameClaimType指定为preferred_username的原因:

当然,还需要在Program.cs文件中加入Authentication和Authorization的Middleware:

app.UseAuthentication();
app.UseAuthorization();

并在StickersController上启用Authorize特性:

[ApiController]
[Authorize]
[Route("[controller]")]
public class StickersController(ISimplifiedDataAccessor dac) : ControllerBase
{
// ...
}

此时如果启动Stickers API,然后使用cURL获取所有的“贴纸”,则会返回401 Unauthorized:

$ curl --location 'http://localhost:5141/stickers?asc=true&size=20&page=0' -v
* Host localhost:5141 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:5141...
* Connected to localhost (::1) port 5141
> GET /stickers?asc=true&size=20&page=0 HTTP/1.1
> Host: localhost:5141
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Content-Length: 0
< Date: Sat, 26 Oct 2024 13:05:21 GMT
< Server: Kestrel
< WWW-Authenticate: Bearer
<
* Connection #0 to host localhost left intact

但如果将刚刚获得的access token加到cURL命令中,就可以正常访问API了(access token太长,这里先把它截断了):

$ curl --location 'http://localhost:5141/stickers?asc=true&size=20&page=0' \
--header 'Authorization: Bearer eyJh...' -v
* Host localhost:5141 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:5141...
* Connected to localhost (::1) port 5141
> GET /stickers?asc=true&size=20&page=0 HTTP/1.1
> Host: localhost:5141
> User-Agent: curl/8.5.0
> Accept: */*
> Authorization: Bearer eyJh...
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sat, 26 Oct 2024 13:08:06 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"items":[],"pageIndex":0,"pageSize":20,"totalCount":0,"totalPages":0}

在Stickers.WebApi中启用授权机制

在我之前写的《ASP.NET Core Web API下基于Keycloak的多租户用户授权的实现》一文中,已经详细介绍了如何基于Keycloak完成授权,在Stickers案例中,我会采用相同的实现方式,因此这里就不再赘述具体的实现过程了,仅介绍Stickers微服务所特有的部分。

上面我们已经在Keycloak中配置了授权,这里大致总结一下与授权相关的配置。首先,我们定义了四个scope,分别是:admin.manage_users、stickers.read、stickers.update以及stickers.delete。所谓的scope,其实就是对资源的操作类型;然后,我们定义了两种资源:admin-api和stickers-api,分别表示两组不同的API:admin-api表示与站点管理相关的API(虽然暂时我们还没有实现管理API),而stickers-api则表示与“贴纸”相关的API(也就是StickersController所提供的API);接下来,我们又定义了两个Policy:require-admin-policy和require-registered-user-policy,分别表示“干某件事需要管理员角色”和“干某件事需要注册用户角色”。可以看到,其实基于角色的授权,在Keycloak的整个授权体系中,只是其中的一种特例,Keycloak所支持的Policy类型,并不仅只有基于角色这一种策略;最后,定义了四个Permission:admin-manage-users-permission、stickers-delete-permission、stickers-update-permission和stickers-view-permission,这些permission都关联了对应的策略(这里都是基于角色的策略)和对资源的操作类型scope,而这些操作类型又进一步被资源所引用。所以,总的来说,Permission就定义了符合某种策略(Policy)的访问者对某种资源(Resource)具有完成何种操作类型(Scope)的权限。

仔细思考你会发现,我们其实根本不关心当前登录用户是什么角色,我们只关心该用户的某些特质是否达到访问某种资源并完成相应操作的需求,角色只不过是这些特质中的一种。所以,一方面在API上,我们定义该API是什么资源,它支持什么操作,而另一方面,当认证用户访问该API时,我们从用户的Claims中读取该用户在该资源上所能完成的操作名称,两者进行比对即可,而至于认证用户是否满足访问该资源并完成该操作的需求,在Keycloak的授权模块中就已经完成计算了,Keycloak只是在发送的token中带上计算结果就可以了。

下图展示了在Keycloak中,针对daxnet这个用户所进行的权限评估,从评估结果可以看到,该用户在stickers-api资源上的stickers.read、stickers.update以及stickers.delete操作是具有权限的;而在admin-api资源上的admin.manage_users上是没有权限的。所以,我们只需要在Stickers.WebApi上实现这个判断就可以了。

完成这个判断逻辑,大致会需要两个步骤:首先,使用access token,通过将grant_type设置成urn:ietf:params:oauth:grant-type:uma-ticket并再次调用/realms/stickers/protocol/openid-connect/token接口,以获得包含授权信息的user claims,然后,在API被访问时,根据该API所支持的操作列表,从带有授权信息的user claims中查找,看是否API所支持的操作在user claims中能被找到,如果能找到,就说明该用户可以访问API,否则就返回403 Forbidden

完整代码这里就不详细介绍了,还是强烈建议移步阅读《ASP.NET Core Web API下基于Keycloak的多租户用户授权的实现》这篇博文,并配套本章节的源代码以了解细节。

这里还是涉及到user claims缓存的问题,因为在获取用户授权信息的时候,存在两次Keycloak的调用,这样做并特别高效,后续会考虑引入缓存机制来解决这个问题。

在完成代码的实现之后,就可以进行测试了,使用daxnet用户获取access token:

$ curl --location 'http://localhost:5600/realms/stickers/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=public' \
--data-urlencode 'client_secret=B2REunrXWN57KtQoJWoP2Dhr7gqKJrol' \
--data-urlencode 'username=daxnet' \
--data-urlencode 'password=daxnet'

然后使用这个access token来访问GET /stickers API,可以看到,能够成功返回结果:

$ curl --location 'http://localhost:5141/stickers' \
--header 'Authorization: Bearer eyJhbGci......' \
-v && echo
* Host localhost:5141 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:5141...
* Connected to localhost (::1) port 5141
> GET /stickers HTTP/1.1
> Host: localhost:5141
> User-Agent: curl/8.5.0
> Accept: */*
> Authorization: Bearer eyJhbGci......
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Mon, 28 Oct 2024 13:15:58 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"items":[],"pageIndex":0,"pageSize":20,"totalCount":0,"totalPages":0}

重新使用nobody用户获取access token:

$ curl --location 'http://localhost:5600/realms/stickers/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=public' \
--data-urlencode 'client_secret=B2REunrXWN57KtQoJWoP2Dhr7gqKJrol' \
--data-urlencode 'username=nobody' \
--data-urlencode 'password=nobody'

然后使用这个access token来访问GET /stickers API,可以看到,API返回403 Forbidden

$ curl --location 'http://localhost:5141/stickers' \
--header 'Authorization: Bearer eyJhbGci......' -v && echo
* Host localhost:5141 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:5141...
* Connected to localhost (::1) port 5141
> GET /stickers HTTP/1.1
> Host: localhost:5141
> User-Agent: curl/8.5.0
> Accept: */*
> Authorization: Bearer eyJhbGci......
>
< HTTP/1.1 403 Forbidden
< Content-Length: 0
< Date: Mon, 28 Oct 2024 13:18:31 GMT
< Server: Kestrel
<
* Connection #0 to host localhost left intact

总结

本文简单介绍了在Stickers.WebApi上基于Keycloak实现认证与授权的步骤,由于一些原理性的内容和具体实现细节在之前我的博文中都有详细介绍,所以这里也就不再重复了,建议可以结合这些文章来阅读本章代码,相信会有不少的收获。下一章会基于.NET Web Assembly实现前端,并在开发环境中调通整个前后端流程。

源代码

本章源代码在chapter_4这个分支中:https://gitee.com/daxnet/stickers/tree/chapter_4/

下载源代码前,请先删除已有的stickers-pgsql:devstickers-keycloak:dev两个容器镜像,并删除docker_stickers_postgres_data数据卷。

下载源代码后,进入docker目录,然后编译并启动容器:

$ docker compose -f docker-compose.dev.yaml build
$ docker compose -f docker-compose.dev.yaml up

现在就可以直接用Visual Studio 2022或者JetBrains Rider打开stickers.sln解决方案文件,并启动Stickers.WebApi进行调试运行了。

.NET云原生应用实践(四):基于Keycloak的认证与授权的更多相关文章

  1. “行业客户云原生最佳实践日” 亮相KubeCon上海

    2018年11月13日至15日,由CNCF主办的KubeCon + CloudNativeCon将首次登陆中国上海,这是全球范围内规模最大的Kubernetes和云原生技术盛会. 唯一聚焦客户实践的分 ...

  2. ​第3届云原生技术实践峰会(CNBPS 2020)重磅开启,“原”力蓄势待发!

    CNBPS 2020将在11月19-21日全新启动!作为国内最有影响力的云原生盛会之一,云原生技术实践峰会(CNBPS)至今已举办三届. 在2019年的CNBPS上,灵雀云CTO陈恺喊出"云 ...

  3. 阿里云 CDN 业务基于边缘容器的云原生转型实践

    导读:本文基于边缘容器的阿里云 CDN 云原生实践, 涵盖了边缘容器的背景和趋势,边缘托管集群 ACK Managed Edge K8s(文中简称“Edge@ACK”) 的能力.架构,以及基于边缘容器 ...

  4. Sentry 后端云原生中间件实践 ClickHouse PaaS ,为 Snuba 事件分析引擎提供动力

    目录(脑图) ClickHouse PaaS 云原生多租户平台(Altinity.Cloud) 官网:https://altinity.cloud PaaS 架构概览 设计一个拥有云原生编排能力.支持 ...

  5. 敢为人先,从阿里巴巴云原生团队实践Dapr案例,看分布式应用运行时前景

    背景 Dapr是一个由微软主导的云原生开源项目,国内云计算巨头阿里云也积极参与其中,2019年10月首次发布,到今年2月正式发布V1.0版本.在不到一年半的时间内,github star数达到了1.2 ...

  6. 云原生项目实践DevOps(GitOps)+K8S+BPF+SRE,从0到1使用Golang开发生产级麻将游戏服务器—第1篇

    项目初探 项目地址: 原项目:https://github.com/lonng/nanoserver 调过的:https://github.com/Kirk-Wang/nanoserver 这将是一个 ...

  7. k8s 基于RBAC的认证、授权介绍和实践

    在K8S中,当我们试图通过API与集群资源交互时,必定经过集群资源管理对象入口kube-apiserver.显然不是随随便便来一个请求它都欢迎的,每个请求都需要经过合规检查,包括Authenticat ...

  8. gRPC四种模式、认证和授权实战演示,必赞~~~

    前言 上一篇对gRPC进行简单介绍,并通过示例体验了一下开发过程.接下来说说实际开发常用功能,如:gRPC的四种模式.gRPC集成JWT做认证和授权等. 正文 1. gRPC四种模式服务 以下案例演示 ...

  9. openstack私有云布署实践【8.1 身份认证keystone的API创建(科兴环境)】

    其中一台controller上面加入环境变量,我选kxcontroller1,关注的是endpoint的名称不一样,其它创建的参数与测试环境一致 export OS_TOKEN=venicchina ...

  10. openstack私有云布署实践【8.2 身份认证keystone的API创建(办公网环境)】

    其中一台controller上面加入环境变量,我选controller1,关注的是endpoint的名称不一样,其它创建的参数与生产环境一致 export OS_TOKEN=venicchina ex ...

随机推荐

  1. Salesforce Sales Cloud 零基础学习(五) My Labels的使用

    本篇参考: https://help.salesforce.com/s/articleView?id=sf.sales_core_record_labels.htm&type=5 在公司中,S ...

  2. abc366

    E 解题思路 这题求的是满足\(\sum^n_{i=1}(|x-x_i|+|y-y_i|)\leq D\) 的坐标\((x,y)\) 的数目,由于是求和,所以\(x,y\) 之间是相互独立的 第一步, ...

  3. css flex属性

    css学的不咋熟,搞一个复杂一点的水平居中,用display 属性 + position属性 + float属性,搞了好久居然没搞出来,然后我去翻资料,发现我最不常用的flex能解决这个问题,于是我就 ...

  4. Adobe Photoshop cc2018 Mac中文破解版下载

    下载地址在文章最末,下载之前,先看下安装教程. 前面有说过,2015年以前的老Mac电脑可以安装PS2018的版本,Adobe Photoshop cc2018最低系统需求:10.13以上就可以了,但 ...

  5. Go 匿名字段介绍

    在 Go 语言中,结构体(struct)是一种用于将多个不同类型的数据组合在一起的数据结构.你提到的语法: type RiderNode struct { service.SimpleService ...

  6. 倾斜摄影osgb格式文件,进行坐标转换

    倾斜摄影OSGB格式的文件,很棘手,今天需要把osgb放到UE中渲染.碰到的问题如下: 1.osgb文件导进去后,Z轴不想上,是歪的,小人放进去后,就斜站在马路上. 2.根本原因是坐标系,UE的插件c ...

  7. 【ETL工具】DataX + DataXWeb 初使用过程记录

    版本:DataX v202309  DataXWeb 2.1.3预发布版 DataX: Github:https://github.com/alibaba/DataX 功能介绍文档:https://g ...

  8. IDEA - ruoyi - srpingboot - 离线运行

    前提:有项目对应的repository文件,RY的DB配置正常(mysql新增schema ry, 执行 /sql下的sql文件,同步调整ruoyi-admin下的application-druid. ...

  9. JS处理html的编码(encode)与解码(decode)

    一.用浏览器内部转换器实现转换 代码: var HtmlUtil = { // 1.用浏览器内部转换器实现html编码 htmlEncode: function(html) { // 创建一个元素容器 ...

  10. linux操作系统和文件系统,命令(上)

    Linux是一个类似于windows的操作系统 Linux操作系统的一种主要使用方式是通过终端软件:终端软件里只能使用键盘不能使用鼠标,在终端软件里通过输入命令完成各种任务 clear命令可以删除终端 ...