首页 > 其他分享 >.NET云原生应用实践(四):基于Keycloak的认证与授权AL

.NET云原生应用实践(四):基于Keycloak的认证与授权AL

时间:2024-10-29 13:47:16浏览次数:4  
标签:KC -- AL --- NET stickers localhost Keycloak

合集 - .NET云原生应用实践(4)1..NET云原生应用实践(二):Sticker微服务RESTful API的实现10-132..NET云原生应用实践(一):从搭建项目框架结构开始10-093..NET云原生应用实践(三):连接到PostgreSQL数据库10-224..NET云原生应用实践(四):基于Keycloak的认证与授权10-28收起

本章目标

  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:
  9. admin-manage-users-permission:基于require-admin-policy,作用在admin.manage_users Scope
  10. stickers-view-permission:基于require-registered-user-policy,作用在stickers.read Scope
  11. stickers-update-permission:基于require-registered-user-policy,作用在stickers.update Scope
  12. 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进行调试运行了。

标签:KC,--,AL,---,NET,stickers,localhost,Keycloak
From: https://www.cnblogs.com/westworldss/p/18512921

相关文章

  • Allegro 设置Spacing间距规则汇总
    废话不多说,直接上图说明:因为allegro约束规则还是比较细致,特别是间距规则设置比较多,所以一定要搞明白。1.差分对的对内线距PrimaryGap 不要理解为对内线中心到中心的距离,就是边缘线的距离2. 线Line与线的间距3.线Line到通孔管脚ThruPin的间距4. 线Line到......
  • Allegro 16.6 布局时,模块如何进行复用
    模块复用:在原理图中有多个模块相似时,在PCB里面把这些模块处理为相同的布局,就可以使用模块复用功能。上图演示:1.在菜单栏打开PlacementEdit2.框选需要的模块,在某个器件上鼠标右键选择placereplicatecreate,再单击鼠标右键,选择done,单击鼠标左键,会弹出另存为对话框。 ......
  • Educational Codeforces Round 171 (Rated for Div. 2)题解记录
    比赛链接:https://codeforces.com/contest/2026A.PerpendicularSegments题目说了必定有答案,直接对角线即可#include<iostream>#include<queue>#include<map>#include<set>#include<vector>#include<algorithm>#include<deque>#include<......
  • Multimodal Embed 3:为人工智能搜索提供动力
    Cohere发布最先进的多模态人工智能搜索模型,为图像数据释放真正的商业价值。Embed3是我们业界领先的人工智能搜索模型,现在已实现多模态化。这一进步使企业能够从存储在图像中的大量数据中挖掘出真正的价值。企业现在可以建立系统,准确、快速地搜索重要的多模态资产,如复......
  • DBeaver如何连接impala
    DBeaver下载地址:https://dbeaver.io/download/1、打开DBeaver,搜impala 2、下载impala驱动,然后解压ClouderaImpalaJDBC-2.6.34.10651)impala驱动下载地址:https://www.cloudera.com/downloads/connectors/impala/jdbc 2)进到ClouderaImpalaJDBC-2.6.34.1065,解压Cl......
  • 基于alpine制作jdk8基础镜像/解决时区问题
    在一次使用镜像部署项目中,发现推送的消息时间不是预设的,检查后发现是基础镜像时区不对导致。下面是自己制作基础镜像的简单记录,可参考制作更好的基础镜像,以支持快速部署。 1、拉取alpinedockerpull openjdk:8-jdk-alpine2、创建Dockerfile文件FROMopenjdk:8-jdk-alpine......
  • 【项目实战】分布式日志搜索系统之数据同步方案(Logstash-input-jdbc、go-mysql-elast
    在构建分布式日志搜索系统时,数据同步是一个核心环节。以下是针对您提出的五种数据同步方案的详细分析:一、Logstash-input-jdbcLogstash是ElasticStack的一部分,用于从各种来源收集数据,并将其发送到Elasticsearch。Logstash-input-jdbc插件允许Logstash从关系型数据库(如My......
  • 面向对象高级-final关键字
    文章目录2.1final修饰符的特点2.2补充知识:常量2.1final修饰符的特点我们先来认识一下final的特点,final关键字是最终的意思,可以修饰类、修饰方法、修饰变量。-final修饰类:该类称为最终类,特点是不能被继承-final修饰方法:该方法称之为最终方法,特点是不能被......
  • Educational Codeforces Round 171 (Rated for Div. 2)
    目录写在前面A签到B暴力C反悔贪心D枚举,分块,推式子E网络流,最大权闭合子图F写在最后写在前面比赛地址:https://codeforces.com/contest/2026。因为太困了决定睡大觉,于是赛时unratedregister只写了DE。然而十一点半上床还是失眠到一点半睡的太搞了呃呃A签到B暴力限......
  • .NET HangFire使用
    1.程序集Install-PackageHangfire.CoreInstall-PackageHangfire.SqlServerInstall-PackageHangfire.AspNet2.web.config加配置<connectionStrings><addname="sqlserver_connection"connectionString="DataSource=.;InitialCatalog=HangFi......