Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)

好吧,这个题目我也想了很久,不知道如何用最简单的几个字来概括这篇文章,原本打算取名《Angular单页面应用基于Ocelot API网关与IdentityServer4+ASP.NET Identity实现身份认证与授权》,然而如你所见,这样的名字实在是太长了。所以,我不得不缩写“单页面应用”几个字,然后去掉ASP.NET Identity的描述,最后形成目前的标题。

不过,这也就意味着这篇文章会涵盖很多内容和技术,我会利用这些技术来走通一个完整的流程,这个流程也代表着在微服务架构中单点登录的一种实现模式。在此过程中,我们会使用到如下技术或框架:

  • Angular 8

  • Ocelot API Gateway

  • IdentityServer4

  • ASP.NET Identity

  • Entity Framework Core

  • SQL Server

本文假设读者具有上述技术框架的基础知识。由于内容比较多,我还是将这篇文章分几个部分进行讲解和讨论。

场景描述

在微服务架构下的一种比较流行的设计,就是基于前后端分离,前端只做呈现和用户操作流的管理,后端服务由API网关同一协调,以从业务层面为前端提供各种服务。大致可以用下图表示:

在这个结构中,我没有将Identity Service放在API Gateway后端,因为考虑到Identity Service本身并没有承担任何业务功能。从它所能提供的端点(Endpoint)的角度,它也需要做负载均衡、熔断等保护,但我们暂时不讨论这些内容。

流程上其实也比较简单,在上图的数字标识中:

  1. Client向Identity Service发送认证请求,通常可以是用户名密码

  2. 如果验证通过,Identity Service会向Client返回认证的Token

  3. Client使用Token向API Gateway发送API调用请求

  4. API Gateway将Client发送过来的Token发送给Identity Service,以验证Token的有效性

  5. 如果验证成功,Identity Service会告知API Gateway认证成功

  6. API Gateway转发Client的请求到后端API Service

  7. API Service将结果返回给API Gateway

  8. API Gateway将API Service返回的结果转发到Client

只是在这些步骤中,我们有很多技术选择,比如Identity Service的实现方式、认证方式等等。接下来,我就在ASP.NET Core的基础上使用IdentityServer4、Entity Framework Core和Ocelot来完成这一流程。在完成整个流程的演练之前,需要确保机器满足以下条件:

  • 安装Visual Studio 2019 Community Edition。使用Visual Studio Code也是可以的,根据自己的需要选择

  • 安装Visual Studio Code

  • 安装Angular 8

IdentityServer4结合ASP.NET Identity实现Identity Service

创建新项目

首先第一步就是实现Identity Service。在Visual Studio 2019 Community Edition中,新建一个ASP.NET Core Web Application,模板选择Web Application (Model-View-Controller),然后点击Authentication下的Change按钮,再选择Individual User Accounts选项,以便将ASP.NET Identity的依赖包都加入项目,并且自动完成基础代码的搭建。

然后,通过NuGet添加IdentityServer4.AspNetIdentity以及IdentityServer4.EntityFramework的引用,IdentityServer4也随之会被添加进来。接下来,在该项目的目录下,执行以下命令安装IdentityServer4的模板,并将IdentityServer4的GUI加入到当前项目:

1

2

dotnet new -i identityserver4.templates

dotnet new is4ui --force

然后调整一下项目结构,将原本的Controllers目录删除,同时删除Models目录下的ErrorViewModel类,然后将Quickstart目录重命名为Controllers,编译代码,代码应该可以编译通过,接下来就是实现我们自己的Identity。

定制Identity Service

为了能够展现一个标准的应用场景,我自己定义了User和Role对象,它们分别继承于IdentityUser和IdentityRole类:

1

2

3

4

5

6

7

8

9

public class AppUser : IdentityUser

{

    public string DisplayName { get; set; }

}

 

public class AppRole : IdentityRole

{

    public string Description { get; set; }

}

当然,Data目录下的ApplicationDbContext也要做相应调整,它应该继承于IdentityDbContext<AppUser, AppRole, string>类,这是因为我们使用了自定义的IdentityUser和IdentityRole的实现:

1

2

3

4

5

6

7

public class ApplicationDbContext : IdentityDbContext<AppUser, AppRole, string>

{

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)

        : base(options)

    {

    }

}

之后修改Startup.cs里的ConfigureServices方法,通过调用AddIdentity、AddIdentityServer以及AddDbContext,将ASP.NET Identity、IdentityServer4以及存储认证数据所使用的Entity Framework Core的依赖全部注册进来。为了测试方便,目前我们还是使用Developer Signing Credential,对于Identity Resource、API Resource以及Clients,我们也是暂时先写死(hard code):


public void ConfigureServices(IServiceCollection services)

{

    services.AddDbContext<ApplicationDbContext>(options =>

        options.UseSqlServer(

            Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<AppUser, AppRole>()

        .AddEntityFrameworkStores<ApplicationDbContext>()

        .AddDefaultTokenProviders();

    services.AddIdentityServer().AddDeveloperSigningCredential()

      .AddOperationalStore(options =>

      {

          options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),

              sqlServerDbContextOptionsBuilder =>

              sqlServerDbContextOptionsBuilder.MigrationsAssembly(typeof(Startup).Assembly.GetName().Name));

          options.EnableTokenCleanup = true;

          options.TokenCleanupInterval = 30; // interval in seconds

      })

      .AddInMemoryIdentityResources(Config.GetIdentityResources())

      .AddInMemoryApiResources(Config.GetApiResources())

      .AddInMemoryClients(Config.GetClients())

      .AddAspNetIdentity<AppUser>();

 

    services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin()

       .AllowAnyMethod()

       .AllowAnyHeader()));

 

    services.AddControllersWithViews();

    services.AddRazorPages();

    services.AddControllers();

}

然后,调整Configure方法的实现,将IdentityServer加入进来,同时配置CORS使得站点能够被跨域访问:


public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

    if (env.IsDevelopment())

    {

        app.UseDeveloperExceptionPage();

        app.UseDatabaseErrorPage();

    }

    else

    {

        app.UseExceptionHandler("/Home/Error");

        app.UseHsts();

    }

 

    app.UseCors("AllowAll");

    app.UseHttpsRedirection();

    app.UseStaticFiles();

 

    app.UseRouting();

    app.UseIdentityServer();

 

    app.UseAuthentication();

    app.UseAuthorization();

 

    app.UseEndpoints(endpoints =>

    {

        endpoints.MapControllerRoute(

            name: "default",

            pattern: "{controller=Home}/{action=Index}/{id?}");

        endpoints.MapRazorPages();

    });

}

完成这部分代码调整后,编译是通不过的,因为我们还没有定义IdentityServer4的IdentityResource、API Resource和Clients。在项目中新建一个Config类,代码如下:


public static class Config

{

    public static IEnumerable<IdentityResource> GetIdentityResources() =>

        new IdentityResource[]

        {

            new IdentityResources.OpenId(),

            new IdentityResources.Email(),

            new IdentityResources.Profile()

        };

 

    public static IEnumerable<ApiResource> GetApiResources() =>

        new[]

        {

            new ApiResource("api.weather", "Weather API")

            {

                Scopes =

                {

                    new Scope("api.weather.full_access", "Full access to Weather API")

                },

                UserClaims =

                {

                    ClaimTypes.NameIdentifier,

                    ClaimTypes.Name,

                    ClaimTypes.Email,

                    ClaimTypes.Role

                }

            }

        };

 

    public static IEnumerable<Client> GetClients() =>

        new[]

        {

            new Client

            {

                RequireConsent = false,

                ClientId = "angular",

                ClientName = "Angular SPA",

                AllowedGrantTypes = GrantTypes.Implicit,

                AllowedScopes = { "openid", "profile", "email", "api.weather.full_access" },

                RedirectUris = {"http://localhost:4200/auth-callback"},

                PostLogoutRedirectUris = {"http://localhost:4200/"},

                AllowedCorsOrigins = {"http://localhost:4200"},

                AllowAccessTokensViaBrowser = true,

                AccessTokenLifetime = 3600

            },

            new Client

            {

                ClientId = "webapi",

                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

                ClientSecrets =

                {

                    new Secret("mysecret".Sha256())

                },

                AlwaysSendClientClaims = true,

                AllowedScopes = { "api.weather.full_access" }

            }

        };

}

大致说明一下上面的代码。通俗地讲,IdentityResource是指允许应用程序访问用户的哪些身份认证资源,比如,用户的电子邮件或者其它用户账户信息,在Open ID Connect规范中,这些信息会被转换成Claims,保存在User Identity的对象里;ApiResource用来指定被IdentityServer4所保护的资源,比如这里新建了一个ApiResource,用来保护Weather API,它定义了自己的Scope和UserClaims。Scope其实是一种关联关系,它关联着Client与ApiResource,用来表示什么样的Client对于什么样的ApiResource具有怎样的访问权限,比如在这里,我定义了两个Client:angular和webapi,它们对Weather API都可以访问;UserClaims定义了当认证通过之后,IdentityServer4应该向请求方返回哪些Claim。至于Client,就比较容易理解了,它定义了客户端能够以哪几种方式来向IdentityServer4提交请求。

至此,我们的源代码就可以编译通过了,成功编译之后,还需要使用Entity Framework Core所提供的命令行工具或者Powershell Cmdlet来初始化数据库。我这里选择使用Visual Studio 2019 Community中的Package Manager Console,在执行数据库更新之前,确保appsettings.json文件里设置了正确的SQL Server连接字符串。当然,你也可以选择使用其它类型的数据库,只要对ConfigureServices方法做些相应的修改即可。在Package Manager Console中,依次执行下面的命令:

1

2

3

4

Add-Migration ModifiedUserAndRole -Context ApplicationDbContext

Add-Migration ModifiedUserAndRole –Context PersistedGrantDbContext

Update-Database -Context ApplicationDbContext

Update-Database -Context PersistedGrantDbContext

效果如下:

打开SQL Server Management Studio,看到数据表都已成功创建:

由于IdentityServer4的模板所产生的代码使用的是mock user,也就是IdentityServer4里默认的TestUser,因此,相关部分的代码需要被替换掉,最主要的部分就是AccountController的Login方法,将该方法中的相关代码替换为:


if (ModelState.IsValid)

{

    var user = await _userManager.FindByNameAsync(model.Username);

 

    if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))

    {

        await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.DisplayName));

 

        // only set explicit expiration here if user chooses "remember me".

        // otherwise we rely upon expiration configured in cookie middleware.

        AuthenticationProperties props = null;

        if (AccountOptions.AllowRememberLogin && model.RememberLogin)

        {

            props = new AuthenticationProperties

            {

                IsPersistent = true,

                ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)

            };

        };

 

        // issue authentication cookie with subject ID and username

        await HttpContext.SignInAsync(user.Id, user.UserName, props);

 

        if (context != null)

        {

            if (await _clientStore.IsPkceClientAsync(context.ClientId))

            {

                // if the client is PKCE then we assume it's native, so this change in how to

                // return the response is for better UX for the end user.

                return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });

            }

 

            // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null

            return Redirect(model.ReturnUrl);

        }

 

        // request for a local page

        if (Url.IsLocalUrl(model.ReturnUrl))

        {

            return Redirect(model.ReturnUrl);

        }

        else if (string.IsNullOrEmpty(model.ReturnUrl))

        {

            return Redirect("~/");

        }

        else

        {

            // user might have clicked on a malicious link - should be logged

            throw new Exception("invalid return URL");

        }

    }

 

    await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId: context?.ClientId));

    ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);

}

这样才能通过注入的userManager和EntityFramework Core来访问SQL Server,以完成登录逻辑。

新用户注册API

由IdentityServer4所提供的默认UI模板中没有包括新用户注册的页面,开发者可以根据自己的需要向Identity Service中增加View来提供注册界面。不过为了快速演示,我打算先增加两个API,然后使用curl来新建一些用于测试的角色(Role)和用户(User)。下面的代码为客户端提供了注册角色和注册用户的API:

18

119

public class RegisterRoleRequestViewModel

{

    [Required]

    public string Name { get; set; }

 

    public string Description { get; set; }

}

 

public class RegisterRoleResponseViewModel

{

    public RegisterRoleResponseViewModel(AppRole role)

    {

        Id = role.Id;

        Name = role.Name;

        Description = role.Description;

    }

 

    public string Id { get; }

 

    public string Name { get; }

 

    public string Description { get; }

}

 

public class RegisterUserRequestViewModel

{

    [Required]

    [StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]

    [Display(Name = "DisplayName")]

    public string DisplayName { get; set; }

 

    public string Email { get; set; }

 

    [Required]

    [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]

    [DataType(DataType.Password)]

    [Display(Name = "Password")]

    public string Password { get; set; }

 

    [Required]

    [StringLength(20)]

    [Display(Name = "UserName")]

    public string UserName { get; set; }

 

    public List<string> RoleNames { get; set; }

}

 

public class RegisterUserResponseViewModel

{

    public string Id { get; set; }

    public string UserName { get; set; }

    public string DisplayName { get; set; }

    public string Email { get; set; }

 

    public RegisterUserResponseViewModel(AppUser user)

    {

        Id = user.Id;

        UserName = user.UserName;

        DisplayName = user.DisplayName;

        Email = user.Email;

    }

}

 

// Controllers\Account\AccountController.cs

[HttpPost]

[Route("api/[controller]/register-account")]

public async Task<IActionResult> RegisterAccount([FromBody] RegisterUserRequestViewModel model)

{

    if (!ModelState.IsValid)

    {

        return BadRequest(ModelState);

    }

 

    var user = new AppUser { UserName = model.UserName, DisplayName = model.DisplayName, Email = model.Email };

     

 

    var result = await _userManager.CreateAsync(user, model.Password);

 

    if (!result.Succeeded) return BadRequest(result.Errors);

 

    await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.NameIdentifier, user.UserName));

    await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Name, user.DisplayName));

    await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, user.Email));

 

    if (model.RoleNames?.Count > 0)

    {

        var validRoleNames = new List<string>();

        foreach(var roleName in model.RoleNames)

        {

            var trimmedRoleName = roleName.Trim();

            if (await _roleManager.RoleExistsAsync(trimmedRoleName))

            {

                validRoleNames.Add(trimmedRoleName);

                await _userManager.AddToRoleAsync(user, trimmedRoleName);

            }

        }

 

        await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Role, string.Join(',', validRoleNames)));

    }

 

    return Ok(new RegisterUserResponseViewModel(user));

}

 

// Controllers\Account\AccountController.cs

[HttpPost]

[Route("api/[controller]/register-role")]

public async Task<IActionResult> RegisterRole([FromBody] RegisterRoleRequestViewModel model)

{

    if (!ModelState.IsValid)

    {

        return BadRequest(ModelState);

    }

 

    var appRole = new AppRole { Name = model.Name, Description = model.Description };

    var result = await _roleManager.CreateAsync(appRole);

    if (!result.Succeeded) return BadRequest(result.Errors);

 

    return Ok(new RegisterRoleResponseViewModel(appRole));

}

在上面的代码中,值得关注的就是register-account API中的几行AddClaimAsync调用,我们将一些用户信息数据加入到User Identity的Claims中,比如,将用户的角色信息,通过逗号分隔的字符串保存为Claim,在后续进行用户授权的时候,会用到这些数据。

创建一些基础数据

运行我们已经搭建好的Identity Service,然后使用下面的curl命令创建一些基础数据:

1

2

3

4

5

6

7

8

9

curl -X POST https://localhost:7890/api/account/register-role \

  -d '{"name":"admin","description":"Administrator"}' \

  -H 'Content-Type:application/json' --insecure

curl -X POST https://localhost:7890/api/account/register-account \

  -d '{"userName":"daxnet","password":"P@ssw0rd123","displayName":"Sunny Chen","email":"daxnet@163.com","roleNames":["admin"]}' \

  -H 'Content-Type:application/json' --insecure

curl -X POST https://localhost:7890/api/account/register-account \

  -d '{"userName":"acqy","password":"P@ssw0rd123","displayName":"Qingyang Chen","email":"qychen@163.com"}' \

  -H 'Content-Type:application/json' --insecure

完成这些命令后,系统中会创建一个admin的角色,并且会创建daxnet和acqy两个用户,daxnet具有admin角色,而acqy则没有该角色。

使用浏览器访问https://localhost:7890,点击主页的链接进入登录界面,用已创建的用户名和密码登录,可以看到如下的界面,表示Identity Service的开发基本完成:

小结

一篇文章实在是写不完,今天就暂且告一段落吧,下一讲我将介绍Weather API和基于Ocelot的API网关,整合Identity Service进行身份认证。

源代码

访问以下Github地址以获取源代码:

https://github.com/daxnet/identity-demo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/311073.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【翻译】.NET 5 Preview 1 发布

.NET 5 Preview 1 发布去年年底,我们发布了.NET Core 3.0和3.1.这些版本添加了桌面应用程序模型Windows Forms(WinForms)和WPF,ASP.NET Blazor用于构建SPA应用程序和用于构建分布式应用和服务的gRPC模板、用于与gRPC对话丰富的客户端代码生成、REST API服务等等.我们很高兴看到…

Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(二)

上文已经介绍了Identity Service的实现过程。今天我们继续&#xff0c;实现一个简单的Weather API和一个基于Ocelot的API网关。回顾《Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权&#xff08;一&#xff09;》Weather APIWeather API实现非常简单&#xf…

编程 音量键_盘点市面上那些千元级高逼格的键盘 灯光炫酷 多宏编程

随着科技的变化&#xff0c;以及电竞被更多人熟知&#xff0c;也带动了电竞外设的进步&#xff0c;现在更多的人喜欢选择机械键盘。首先机械键盘可以给我们带来超棒的敲击感&#xff0c;无论是玩游戏还是日常办公打字&#xff0c;都绝对是一等一的好。再者机械键盘在高强度使用…

DotNetCore Web应用程序中的Cookie管理

原文来自互联网&#xff0c;由长沙DotNET技术社区编译。如译文侵犯您的署名权或版权&#xff0c;请联系小编&#xff0c;小编将在24小时内删除。限于译者的能力有限&#xff0c;个别语句翻译略显生硬&#xff0c;还请见谅。作者简介&#xff1a;Jon&#xff08;Jonathan&#x…

逆向so_记一次APP的so层算法逆向(七)

“ 前言&#xff1a;初学逆向 请多多指教 好累 感觉每天这样肝 人有点受不了了...”学习到的内容—1、新学习到IDA的一些分析时候的小技巧2、算法还原代码实现的练习(有个参数没有分析出来&#xff0c;后面知道了会补上的)3、在Frida中使用命令行调试的方便方法分析过程—APP登…

C++实现Huffman树

代码如下&#xff1a; #include <iostream> using namespace std; int s1, s2;typedef struct {int weight;int parent, lch, rch; } HTNode, *HuffmanTree;void Select(HuffmanTree &HT, int n, int &s1, int &s2) {int minv;//定义一个临时变量存储最小值…

.NET Core开发实战(第25课:路由与终结点:如何规划好你的Web API)--学习笔记(下)...

25 | 路由与终结点&#xff1a;如何规划好你的Web API自定义约束实现了路由约束接口&#xff0c;它只有一个 Match 方法&#xff0c;这个方法传入了 Http 当前的 httpContext&#xff0c;route&#xff0c;routeKey这个 routeKey 就是我们要验证的 key 值后面两个参数 RouteVal…

微软 Visual Studio 2019 16.5 发布:.NET 移动开发、生产力

微软最新发布了 Visual Studio 2019 16.5 版本&#xff0c;下面来看看主要更新内容&#xff1a;.NET 移动开发首先要讨论的特性是 XAML Hot Reload for Xamarin.Forms。此功能可加快开发速度&#xff0c;并使开发者可以更轻松地在移动应用的用户界面上进行构建、实验和迭代。且…

chrome主题_谷歌Chrome将很快允许用户创建自定义主题

站长之家(ChinaZ.com) 7月31日 消息:据9to5google报道&#xff0c;虽然用户可以通过Chrome Web Store定制主题&#xff0c;但用户要根据自己的独特喜好定制主题却不是一个简单的事。谷歌正寻求通过在Chrome内置一个自定义主题生成器来解决这个问题。Chrome Web Store中有许多传…

使用Magicodes.IE.Excel完成Excel图片的导入和导出

说明本章教程主要说明如何使用Magicodes.IE.Excel进行图片的导入导出。要点配置DTO进行Excel图片导出配置DTO进行Excel图片导入图片导入导出特性说明ExportImageFieldAttributeHeight&#xff1a;高度(默认15)Width&#xff1a;宽度(默认50)Alt&#xff1a;图片不存在时替换文本…

C++未定义行为-数组越界

我们先来看看下面的代码&#xff1a; #include <iostream> using namespace std; const int N 100010; int a[N]; int main() {for (int i 1;i<N;i) a[i] 2;return 0; }当我们写这段代码的时候&#xff0c;编译器就会发生这样的问题。 这是为什么呢&#xff1f;&a…

SuperBenchmarker一个用.NET编写的压测工具

0x01 前言在这之前想必大家对ab(http)与abs(https)也有一些了解,我们今天不去看ab和abs,SuperBenchmarker(sb.exe)是一个压测工具,他是一个受Apache Benchmark的启发,他会在终端窗口为我们显示最终的结果,同时也会在web界面生成一个动态结果。SuperBenchmarker(sb.exe)可以在Wi…

mysql文献综述_文献综述随笔(二十)

一、基本信息标题&#xff1a;中小型酒店管理系统的设计与实现时间&#xff1a;2013来源&#xff1a;厦门大学关键词&#xff1a;MVC;B/S;JAVA EE;JSP;MySQL;瀑布开发模型二、研究内容1.主要内容&#xff1a;系统业务需求、功能需求、系统架构设计、数据库设计1.1功能模块设计&…

五分钟完成 ABP vNext 通讯录 App 开发

ABP vNext&#xff08;后文简称Abp&#xff09;是 Volo 公司堪称艺术品级的应用开发框架&#xff0c;它基于领域驱动设计&#xff08;DDD&#xff09;的思维&#xff0c;创新地采用了模块化的设计。Abp 目前无疑是 ASP.NET Core 开发框架中最先进和最优雅的存在。笔者认为&…

mysql 5.74安装教程_MySQL安装、基本账户安全(5.0以后版本)

-----------MySQL 5.0以后版本的安装-----------MySQL安装安装包学习的必杀绝技——就是阅读包的安装说明(readme & install)文档。----------# rm /etc/my.cnf (安装前执行一下)----------1.Mysql-5.0.40.tar.gz1.1.Source Installation Overview(lines 74 of …

使用GUI工具Portainer.io管控Docker容器

背景5年前容器技术扑面而来&#xff0c;如今已经成为面向云原生开发的基础架构&#xff0c;基于微服务的设计需要部署大量容器&#xff0c;同时强调了友好快速的管理容器。是时候推荐一个轮子Portainer.io&#xff1a;提供GUI界面的容器管理工具&#xff0c;给开发者的工具箱又…

【项目升级】集成Quartz.Net Job实现(一)

这两天的新闻也是越来越多了&#xff0c;不仅Github接手了NPM&#xff0c;还有.NET 5也要新鲜出炉了&#xff08;11月正式发布&#xff09;&#xff0c;当然还有MVP峰会也正在如火如荼的展开&#xff0c;会有哪些好的东西被碰撞出来&#xff0c;也是很期待的。这些天我也简单的…

DevC++如何安装自定义头文件并使用

首先我们打开DevC&#xff0c;然后点击新建。 新建一个控制台应用程序 取一个喜欢的文件名。 然后会出现如下界面。 点击新建单元 将头文件源码放入 找到空白位置&#xff0c;右键&#xff0c;然后点关闭并保存 保存的文件名要为头文件的名字 使用这个头文件时&#xff0c;只…

论ORM之EFCore初篇(快速基于本地数据库实现数据操作)

欢迎大家阅读《朝夕Net社区技术专刊》第6期我们致力于.NetCore的推广和落地&#xff0c;为更好的帮助大家学习&#xff0c;方便分享干货&#xff0c;特创此刊&#xff01;很高兴你能成为忠实读者&#xff0c;文末福利不要错过哦&#xff01;前言&#xff1a;今天准备带大家一站…

C++变量的初始化问题及列表初始化

在C中&#xff0c;初始化是一个异常复杂的问题&#xff0c;很多人认为初始化是赋值的一种&#xff0c;事实上&#xff0c;初始化和赋值是两个完全不同的操作。 列表初始化 要定义一个名为haif的int变量并初始化为0&#xff0c;以下4条语句都可以做到。 int haif 0; int hai…