ServiceCollection services = new ServiceCollection();
services.AddLogging(log => { log.AddConsole(); }); // 将日志服务注册到容器中
using (var sp = services.BuildServiceProvider())
var logger = sp.GetRequiredService<ILogger<BasicLogger>>(); // 获得服务
logger.LogInformation("普通信息"); // 输入日志
输出日志文件
需要安装log4net
ILoggerRepository repository = LogManager.CreateRepository("MyRepository"); // 创建一个日志仓库
XmlConfigurator.Configure(repository, new FileInfo("YZK/日志系统/config.xml")); // 注册,读取配置文件
ILog log = LogManager.GetLogger(repository.Name, "MyLog"); // 获得服务
log.Info("普通信息"); // 输出日志
第4章 Entity Framework Core基础
它是一个ORM框架,用于提高开发效率,让开发人员减少对数据库的关注,即使不会写SQL也能实现数据的持久化。它的底层是ADO.NET,通过它访问数据库。
本书的作者是提倡使用Code First模式,而我不喜欢使用它,而是使用DataBase First模式。
环境:MySQL5.7,Navicat
1、首先是新建数据库,表结构
2、安装实现了指定数据库的EF Core包,对应MySQL的包我安装的是Pomelo.EntityFrameworkCore,据说Bug比较少。版本是3.1.32,因为它依赖.NETStandard 2.0。
继续安装Pomelo.EntityFrameworkCore.MySql,版本是3.2.7。
3、使用工具生成实体类
在VS 里找到视图 > 其他窗口 > 程序包控制管理台,输入:
Scaffold-DbContext -Force "server=127.0.0.1;Port=3306;database=db_name;uid=root;pwd=123456;" -Provider "Pomelo.EntityFrameworkCore.Mysql" -OutputDir Models
Scaffold-DbContext的作用是生成DbContext的代码,表中必须要有主键,对数据库做了任何修改操作后,使用它都能快速同步到项目中。
下面是微软文档中EF -> EF Core -> 命令行参考 -> 程序包管理控制台中,Scaffold-DbContext的参数和说明:
Scaffold-DbContext
为 DbContext 生成代码,并为数据库生成实体类型。 为了让 Scaffold-DbContext 生成实体类型,数据库表必须具有主键。
参数 | 说明 |
-Connection | 用于连接到数据库的连接字符串。 对于 ASP.NET Core 2.x 项目,值可以是连接字符串>的 name=。 在这种情况下,名称来自为项目设置的配置源。 这是一个位置参数,并且是必需的。 |
-Provider | 要使用的提供程序。 通常,这是 NuGet 包的名称,例如: Microsoft.EntityFrameworkCore.SqlServer。 这是一个位置参数,并且是必需的。 |
-OutputDir | 要在其中放置实体类文件的目录。 路径相对于项目目录。 |
-ContextDir | 要在其中放置 DbContext文件的目录。 路径相对于项目目录。 |
-Namespace | 要用于所有生成的类的命名空间。 默认设置为从根命名空间和输出目录生成。 已在 EF Core 5.0 中添加。 |
-ContextNamespace | 要用于生成的 DbContext类的命名空间。 注意:重写 -Namespace。 已在 EF Core 5.0 中添加。 |
-Context | 要生成的 DbContext类的名称。 |
-Schemas | 要为其生成实体类型的表的架构。 如果省略此参数,则包含所有架构。 |
-Tables | 要为其生成实体类型的表。 如果省略此参数,则包含所有表。 |
-DataAnnotations | 使用属性配置模型(如果可能)。 如果省略此参数,则仅使用 Fluent API。 |
-UseDatabaseNames | 使用与数据库中显示的名称完全相同的表和列名。 如果省略此参数,数据库名称将更改为更符合 C# 名称样式约定。 |
-Force | 覆盖现有文件。 |
-NoOnConfiguring | 不生成 DbContext.OnConfiguring。 已在 EF Core 5.0 中添加。 |
-NoPluralize | 请勿使用复数化程序。 已在 EF Core 5.0 中添加。 |
主键类型的选择并不简单
1、普通自增
自增类型的主键使用起来很简单,大部分主流数据库都支持这个功能,它有着占用磁盘空间小,可读性强,但它在数据库迁移和分布式系统(如分库分表,数据库集群)使用起来很麻烦。而且在高并发插入的时候性能比较差。
2、Guid算法
使用Guid作为主键时,虽然能保证唯一性,但会遇到性能问题,因为在使用Guid类型作为主键的时候,不能把主键设置为聚集索引。因为聚集索引是按照顺序保存主键的,在插入Guid类型主键的时候,它将会导致新插入的每条数据都要经历查找合适插入位置的过程,在数据量大的时候将会导致非常糟糕的数据插入性能。
在SQL Server中,可以设置主键为非聚集索引,但是在MySQL中,如果我们使用InnoDB引擎,那么主键是强制使用聚集索引的。
在SQL Server中,如果我们使用Guid类型(也就是uniqueidentifier类型)作为主键,一定不能把主键设置为聚集索引;在MySQL中,如果使用InnoDB引擎,并且数据插入频繁,那么一定不要用Guid类型作为主键,如果确实需要用Guid类型作为主键的话,我们只能把这个主键字段作为逻辑主键,而不是作为物理主键;
3、自增 + Guid算法
目前,还有一种主键使用策略是把自增主键和Guid结合起来使用,也就是表有两个主键(注意不是复合主键),用自增列作为物理主键,而用Guid列作为逻辑主键。物理主键是在进行表结构设计的时候把自增列设置为主键,而从表结构上我们是看不出来Guid列是主键的,但是在和其他表关联及和外部系统通信的时候(比如前端显示数据的标识的时候),我们都使用Guid列。这样不仅保证了性能,利用了Guid的优点,而且减少了主键自增导致主键值可被预测带来的安全性问题。
4、Hi/Lo算法
对于普通自增列来讲,每次获取新ID的时候都要锁定自增资源,因此在并发插入数据频繁的情况下,使用普通自增列的数据插入效率相对来讲比较低。EF Core支持使用Hi/Lo算法来优化自增列的性能。
Hi/Lo算法生成的主键值由两部分组成:高位(Hi)和低位(Lo)。高位由数据库生成,两个高位之间相隔若干个值;由程序在本地生成低位,低位的值在本地自增生成。
比如,数据库的两个高位之间相隔10,程序向数据库请求获得一个高位值50。程序在本地获取主键的时候,会首先获得Hi=50,再加上本地的Lo=0,因此主键值为50;程序再获取主键的时候,会继续使用之前获得的Hi=50,再加上本地的低位自增,Lo=1,因此主键值为51,以此类推。当Lo=9之后,再获取主键值,程序发现Hi=50的低位值已经用完了,因此就再向数据库请求一个新的高位值,数据库也许再返回一个Hi=80(因为也许Hi=60和Hi=70已经被其他服务器获取了),然后加上本地的Lo=0,最终获取主键值80,以此类推。
Hi/Lo算法的高位由服务器生成,因此保证了不同进程或者集群中不同服务器获取的高位值不会重复,而本地进程计算的低位则可以保证在本地高效率地生成主键值。
打印SQL语句
在Context类的OnConfiguring方法中,添加optionsBuilder.LogTo(Console.WriteLine)后,每次执行操作都会将SQL语句打印到控制台。
第五章 EF Core高级技术
既生IEnumerable,何生IQueryable
它们之间有什么不同呢?
IQueryable是继承与IEnumerable,其用法相同,这里要引述两个概念:服务器端评估和客户端评估。
服务器端评估:使用SQL语句在数据库服务器上完成数据筛选的过程、
客户端评估:把数据首先加载到应用程序的内存中,然后在内存中进行数据筛选的过程。
很显然,对于大部分情况来讲,“客户端评估”性能比较低,我们要尽量避免“客户端评估”。
Enumerable类中定义的供普通集合用的Where等方法都是“客户端评估”,因此微软创造了IQueryable类型,它使用的是“服务器端评估”。如果打开了EF Core的日志,可以查看到生成SQL语句也不同,IQueryable生成的语句代入了Where条件,Enumerable没有代入该条件。
DataReader和DataTable的区别
ADO.NET中有DataReader和DataTable两种读取数据库查询结果的方式。如果查询结果有很多条数据,DataTable会把所有数据一次性地从数据库服务器加载到客户端内存中,而DataReader则会分批从数据库服务器读取数据。DataReader的优点是客户端内存占用小,缺点是如果遍历读取数据并进行处理的过程缓慢的话,会导致程序占用数据库连接的时间较长,从而降低数据库服务器的并发连接能力;DataTable的优点是数据被快速地加载到了客户端内存中,因此不会较长时间地占用数据库连接,缺点是如果数据量大的话,客户端的内存占用会比较大。
第六章 ASP.NET Core Web API基础
在.NET Framework中,ASP.NET MVC是用来进行基于视图的MVC模式开发的框架,而ASP.NET Web API 2是用来进行Web API开发的框架,这是两个不同的框架。而在ASP.NET Core中,不再做这样的区分,严格来讲,只有ASP.NET Core MVC这一个框架,ASP.NET Core MVC既支持基于视图的MVC模式开发,也支持Web API开发和Razor Pages开发等。不过在Visual Studio中创建项目的时候,仍然存在“ASP.NET Core Web API”和“ASP.NET Core应用(模型-视图-控制器)”这两种向导,分别用来创建Web API项目和传统的基于视图的MVC项目。
ASP.NET Core MVC的优点与流程
在MVC模式中,视图和控制器不直接交互、不互相依赖,彼此之间通过模型进行数据传递。使用MVC模式的优点是视图和控制器降低了耦合,系统的结构更清晰。
浏览器端提交的请求会被封装到模型类的对象中并传递给控制器,控制器中对浏览器端的请求进行处理,然后将处理结果放到模型类的对象中传递给视图,而视图则解析模型对象,然后将其渲染成HTML内容输出给浏览器。
ASP.NET Core MVC的新工具:热重载
从.NET 6开始,.NET中增加了热重载(hot reload)功能,它允许我们在以调试方式运行程序的时候,也无须重启程序而让修改的代码生效。它的用法很简单,只要在修改完代码以后单击Visual Studio工具栏中的热重载图标,修改的代码就会立即生效。
建议的开发模式
在开发的时候,作者建议平时使用【启动(不调试)】的方式运行程序,这样在修改完代码后重新生成程序就能让修改的代码生效。在需要调试程序的时候,再以调试的方式运行程序,并且使用热重载功能来应用修改后的代码。
连续做相同的操作,返回的结果是相同的,例如连续两次插入相同的操作,数据库中只会插入一条数据。
如何实现?
每个请求带上唯一的标识,服务查询该标识是否存在,存在则创建一个对象,否则告知已存在。
但这种方法无法处理以下情况:
1、两次请求的频率很高,第一次请求还在判断,创建对象的过程中,第二次请求就已经来了,这时就会创建重复对象的情况。
2、分布式的环境中,两次请求可能在不同的服务器,这时对象锁,分布式事务就失效了。
解决方法是使用Redis,因为它现成、简单、易用。使用redis的incr方法可以帮我们解决这个重复创建问题。创建时,把唯一标识作为key并incr一下,并获取返回值,如果是1,那就说明没有创建过此对象,如果大于1,那就说明已经创建过了。同时key缓存时间保证对象保存到数据库即可。
返回错误码:200派与4XX派的“对决”
200派:业务逻辑的错误,如创建用户失败时,服务器会返回200状态码
理由是:对于数据库连接失败,内存不足,请求格式错误等问题返回4XX和5XX是合理的,但对于用户已存在这种业务逻辑错误,返回这种错误码,会让服务器的错误信息被淹没掉。
而且业务逻辑的错误返回200,服务器的问题返回500,这样也便于区分,减少了工作量。
4XX派:业务逻辑的错误,如创建用户失败时,服务器会返回4XX状态码
理由是:由于网关等中间件可以监测HTTP状态码,对于频繁出现的4XX和5XX错误可以发出警告,帮助运维人员及时发现问题。
RPC风格
控制器上添加的[Route("[controller]")]改为[Route("[controller]/[action]")],这样[controller]就会匹配控制器的名字,而[action]就会匹配操作方法的名字
Restful风格
1、微软提供的WebAPI控制器默认就是Restful风格
2、Get操作可以通过缓存提高访问速度,添加对于冥等操作使用PUT请求。
3、参数统一化
对于保存,更新的操作使用Post,Put请求,参数全部放到请求体中,对于查询,删除的操作使用Get,Delete请求,参数全部放到QueryString中。
为了避免打开swagger时由于方法未被[HttpGet]和[HttpPost]等标记,而报错,所以需要添加[ApiExplorerSettings(IgnoreApi = true)]标记
在ASP.NET Core Web API中,我们应该使用ActionResult来作为操作方法的返回值;如果操作方法可以声明为异步方法,那么我们就用async Task>XXX()这样的声明方式。
第七章 ASP.NET Core 基础组件
ASP.NET Core中的依赖注入
1、高频注入(基于控制器类)
实现步骤:
1.1 Startup类的ConfigureServices方法注入服务
1.2 目标控制器类中添加一个类变量,类型是注入服务;添加一个构造函数,参数是注入服务,给那个类变量赋值。
2、低频注入(基于行为方法)
2.1 Startup类的ConfigureServices方法注入服务
2.2 目标行为方法中添加一个参数,类型是注入服务,参数添加一个[FromServices]标记。
public ActionResult<string> Login([FromServices]LoginService loginService)
注意:第一种方法使用范围更广,第二种方法适用于调用频率低,资源消耗高的情况。
配置系统与ASP.NET Core的集成
在ASP.NET Core项目中,WebApplication类的CreateBuilder方法会按照下面的顺序来提供默认的配置。.NET会按照“后面的提供者覆盖之前的提供者”的方式进行加载。
(1)加载现有的IConfiguration。
(2)加载项目根目录下的appsettings.json。
(3)加载项目根目录下的appsettings.Environment.json,其中Environment代表当前运行环境的名字,7.2.2小节将会详细介绍这一点。
(4)当程序运行在开发环境下,程序会加载“用户机密”配置。
(5)加载环境变量中的配置。
(6)加载命令行中的配置。
环境变量中的配置
在开发环境下,如图7-3所示,我们可以看到Visual Studio自动为项目的调试属性中的环境变量设置了ASPNETCORE_ENVIRONMENT=Development,这就是我们以调试模型启动项目的时候,会加载开发环境相关配置的原因。
配置文件中的配置
在测试、开发环境下,我们还可以分别再创建appsettings.Staging.json、appsettings.Production.json文件。一般来讲,我们在appsettings.json中编写开发、测试、生产环境下都共用的配置,然后在appsettings.Development.json等文件中编写开发环境等的特有配置。
在项目中右键 - 点击【管理用户机密】后会在项目文件中添加用户机密配置,同事也会打开一个机密文件,由于这文件不存在于项目目录中,而是在当前系统的用户文件夹下,所以不会出现数据库连接字符串随着配置文件被提交到互联网上的情况。
但是由于该配置项是唯一的,多个项目用到同一个机密也需要手动修改,所以如果是团队开发,使用起来会比较麻烦。
使用配置中心可以解决上面的问题。
性能优化“万金油”:缓存
客户端缓存
给客户端返回cache-control请求头赋值max-age=xxx后,再次发送相同参数的请求后,后端接口不会接收新的请求,而是浏览器直接返回结果。
这样做的好处是提高了系统的性能,减轻了服务器的负担。
在ASP.NET Core Web API中只需要在方法上添加ResponseCache特性,并设置Duration就可以实现,如下图所示:
[HttpGet]
[ResponseCache(Duration = 60)]
public async Task<ActionResult<ReceiveObject<TOrder>>> GetOrder(int id)
// 业务逻辑代码
我们可以在方法内打断点,首次访问该接口后断点会命中,之后马上再次访问该接口断点不会命中,按F12打开浏览器的开发者模式,可以看到第一次传输了266字节,第二次是已缓存的数据。
服务端缓存
先说结论,不建议开发人员使用服务端缓存,因为开发调试比较麻烦(浏览器无法调试,只能使用PostMan并关闭Cache-Control头),限制比较大(响应状态码为200的GET或者HEAD响应才可能被缓存;报文头中不能含有Authorization、Set-Cookie)
服务器端响应缓存的使用也比较复杂,如果设置不当的话,会导致缓存的数据错误,比如发送给用户A的响应被缓存起来,然后发送给用户B,导致数据安全风险。
开启它的方式很简单,只需要在MapControllers方法前面,UseCors方法后面添加UseResponseCaching方法即可。
// 启用中间件缓存
app.UseResponseCaching();
app.UseEndpoints(endpoints =>
endpoints.MapControllers();
除了响应缓存中间件这样自动化的服务器端缓存机制之外,ASP.NET Core还提供了允许开发人员手动进行缓存管理的机制,内存缓存就是一种把缓存数据放到应用程序内存中的机制。
内存缓存中保存的是一系列的键值对,就像Dictionary类型一样,每个不同的缓存内容具有不同的“缓存键”,每个缓存键对应一个“缓存值”。
对于ASP.NET Core MVC项目,框架会自动地注入内存缓存服务;对于ASP.NET Core Web API等没有自动注入内存缓存服务的项目,我们需要在Program.cs的builder.Build之前添加builder.Services.AddMemoryCache来把内存缓存相关服务注册到依赖注入容器中。
我们一般使用依赖注入的方式来获得IMemoryCache服务,因此我们为控制器类注入IMemoryCache。
// 获取或创建一个名为GetAppointList的缓存键值对
var item = await this._cache.GetOrCreateAsync("GetAppointList",
async (e) =>
// 设置缓存过期时间的两种策略
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30); // 固定缓存(一到过期时间,缓存就过期)
e.SlidingExpiration = TimeSpan.FromSeconds(10); // 滑动缓存(在缓存期间如果命中,会续期)
// 查询数据库...
// 将查询结果返回,这一步会将数据记录进去
return result;
// 返回结果,如果命中缓存,就不会再查库,如果未命中会查库
return item;
两种缓存过期策略
1、绝对过期时间
2、滑动过期时间
绝对过期时间是到了指定的时间后,缓存会过期。
滑动过期时间是在过期时间内命中了缓存,它的有效期会续期,直到超过过期时间就会过期。
我们在选择内存缓存的过期时间策略的时候,如果缓存项的条数不多或者大部分缓存数据被访问的频率都差不多的话,我们可以使用绝对过期时间策略;
如果只有部分数据访问频率比较高并且数据库中的数据不会被更新的话,我们可以使用滑动过期时间策略;
如果缓存项的数据量比较大且只有其中一部分会被频繁访问,而且数据库中的数据会被更新的话,用绝对过期时间和滑动过期时间混合的策略更合适。
当然,无论用哪种过期时间策略,程序中都会存在缓存数据不一致的情况。对于有的系统,这种数据不一致的情况是可以接受的,比如我们把文章的点击量放到缓存中,就会存在文章被访问后没有立即显示新的点击量,而是几秒后等对应缓存项过期之后才更新显示,这个一般来讲是可以接受的。但是在有的系统中,这种延时是无法接受的,比如银行系统中用户的余额如果在用户转账后没有立即更新,则会有非常大的影响。对于这种无法接受缓存延时的系统,如果对应的从数据源获取数据的频率不高的话,可以不用缓存;
当访问的数据在数据库中不存在时,会查询数据库,如果有人恶意高频用这种数据访问,有可能造成数据库的奔溃。
缓存雪崩是在短时间内缓存大量过期,导致数据库被频繁访问,从而导致数据库服务器被压垮。规避的方法是在写入缓存时,在基础过期时间上,再随机添加一个过期时间,这样过期时间就会均匀地分布在一个时间段内。
分布式缓存
在分布式环境下,每次查询数据都会将将数据缓存在内存中。如果只有几台服务器问题还不大,但如果是多台服务器会造成成本的上升。这时候就可以考虑单独将一台服务器作为缓存服务器,专门用来保存缓存数据。
常用的分布式缓存服务器有Redis、Memcached等,当然我们也可以把SQL Server等关系数据库当作分布式缓存服务器使用。.NET Core中提供了统一的分布式缓存服务器的操作接口IDistributedCache,无论用什么类型的分布式缓存服务器,我们都可以统一使用IDistributedCache接口进行操作。
在使用分布式缓存的时候,我们还要选择合适的缓存服务器。微软官方提供了用SQL Server作为缓存服务器的DistributedSqlServerCache,但是用关系数据库来保存缓存的性能并不好。
Memcached是一个专门的缓存服务器,在缓存数据量比较小的时候,性能非常高,但是Memcached在集群、高可用等方面比较弱,而且有“缓存键的最大长度为250B”等限制。如果要使用Memcached作为分布式缓存服务器,我们可以安装EnyimMemcachedCore这个第三方NuGet包。
Redis是一个键值对数据库,提供了丰富的数据类型,它不仅可以被当作缓存服务器,也可以用来保存列表、字典、集合、地理坐标等数据类型,更可以用来作为消息队列。在某些情况下,Redis作为缓存服务器比Memcached性能稍差,但是Redis在高可用、集群等方面非常强大,非常适合在数据量大、需要高可用性等场合使用。微软官方也提供了用Redis作为缓存服务器的NuGet包,本书中将会使用Redis作为分布式缓存服务器。
首先下载并启动Redis服务,在命令提示符中输入redis-cli确认连接正常,我使用的是3.2版本。
接着,因为我们要连接的缓存服务器是Redis,所以需要通过NuGet安装Microsoft.Extensions.Caching.StackExchangeRedis。建议使用3.x版本,与redis的版本对应,否则在运行时redis保存数据可能会出现错误。
然后再Startup文件中的ConfigureServices方法中注册分布式缓存服务:
// 注册分布式缓存服务
services.AddStackExchangeRedisCache(option =>
option.Configuration = "localhost"; // 设置连接配置
option.InstanceName = "pms_"; // 设置缓存前缀
最后使用该服务:
private readonly IDistributedCache _distributedCache;
public RoomController(db_hotelContext context, IDistributedCache distributedCache)
_context = context;
_distributedCache = distributedCache;
[HttpGet]
public async Task<ActionResult> GetCount()
ReceiveObject<string> model = new ReceiveObject<string>();
// 从缓存数据库中取值
var count = this._distributedCache.GetString("GetCount");
if(count == null)
// 如果取不到值,则从数据库中查询,并写入到缓存中.
count = xxx;
var opt = new DistributedCacheEntryOptions();
opt.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30);
this._distributedCache.SetString("GetCount", count, opt);
model.code = 0;
model.data = count;
catch (Exception ex)
model.code = 999999;
model.msg = "系统异常";
return Ok(model);
使用Redis客户端,我们可以查看到Redis中确实创建了一个Hash类型的数据,20秒后过期。如果缓存不设置过期时间,那么它就不会过期TTL的值会是-1。
经过前面的学习,我们知道,.NET中的缓存分为客户端响应缓存、服务器端响应缓存、内存缓存、分布式缓存等。缓存可以极大地提升系统的性能,在进行系统设计的时候,我们要根据系统的特点选择合适的缓存方式。
客户端响应缓存能够充分利用客户端的缓存机制,它不仅可以降低服务器端的压力,也能够提升客户端的操作响应速度并且降低客户端的网络流量。但是我们需要合理设置缓存相关参数,以避免客户端无法及时刷新到最新数据的问题。
服务器端响应缓存能够让我们几乎不需要编写额外的代码就轻松地降低服务器的压力。但是由于服务器端响应缓存的启用条件比较苛刻,因此要根据项目的情况决定是否使用它。
内存缓存能够降低数据库以及后端服务器的压力,而且内存缓存的存取速度非常快;分布式缓存能够让集群中的多台服务器共享同一份缓存,从而降低数据源的压力。如果集群节点的数量不多,并且数据库服务器的压力不大的话,推荐读者使用内存缓存,毕竟内存的读写速度比网络快很多;如果集群节点太多造成数据库服务器的压力很大的话,可以采用分布式缓存。无论是使用内存缓存还是分布式缓存,我们都要合理地设计缓存键,以免出现数据混乱。
这些缓存方式并不是互斥的,我们在项目中可以组合使用它们。比如对于论坛系统,论坛首页中的版块信息变动不频繁,我们可以为版块信息的客户端响应缓存设置24h的过期时间;对于所有的帖子详情信息,我们同时启用内存缓存和分布式缓存,当加载帖子详情页面的数据的时候,我们先到内存缓存中查找,内存缓存中找不到再到分布式缓存中查找,这样就既可以利用内存缓存读取速度快的优点,也能利用分布式缓存的优点。
第八章 ASP.NET Core 高级组件
Authentication与Authorization
Authentication的音标:[ɔˌθentɪˈkeɪʃ(ə)n]
Authorization的音标:[ˌɔθərɪˈzeɪʃ(ə)n]
Authentication与Authorization区别在于中间的entication和rization
Authentication的意思是授权,验证,它是用来验证是否登录成功
Authorization的意思是授权,它是用来判断用户是否有权限访问,它应该是基于Authentication之上的。
Identity(标识)框架
Identity框架的作用:大部分系统中都需要通过数据库保存用户、角色等信息,并且需要注册、登录、密码重置、角色管理等功能。ASP.NET Core提供了标识(identity)框架,它采用RBAC(role-based access control,基于角色的访问控制)策略,内置了对用户、角色等表的管理及相关的接口,从而简化了系统的开发。
Identity框架的适用范围:标识框架使用EF Core对数据库进行操作,由于EF Core屏蔽了底层数据库的差异,因此标识框架支持几乎所有数据库。
具体实现步骤:
第1步,创建ASP.NET Core Web API项目,并通过NuGet安装Microsoft.AspNetCore.Identity.EntityFrameworkCore。
第2步,创建用户实体类User和角色实体类Role。在这个演示中,我们使用自增标识列类型的主键,因此我们编写分别继承自IdentityUser、IdentityRole的User类和Role类
public class User: IdentityUser<long>
public DateTime CreationTime { get; set; }
public string? NickName { get; set; }
public class Role:IdentityRole<long>
第3步,创建继承自IdentityDbContext的类,这是一个EF Core中的上下文类,我们可以通过这个类操作数据库。IdentityDbContext是一个泛型类,有3个泛型参数,分别代表用户类型、角色类型和主键类型。
public class IdDbContext : IdentityDbContext<User, Role, long>
public IdDbContext(DbContextOptions<IdDbContext> options)
: base(options)
protected override void OnModelCreating(ModelBuilder modelBuilder)
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
第4步,我们需要向依赖注入容器中注册与标识框架相关的服务,并且对相关的选项进行配置
IServiceCollection services = builder.Services;
services.AddDbContext<IdDbContext>(opt => {
string connStr = builder.Configuration.GetConnectionString("Default"); // 需要在配置文件中添加数据库的连接字符串
opt.UseSqlServer(connStr); // 如果使用的是其他数据库,需要修改这里
services.AddDataProtection();
services.AddIdentityCore<User>(options =>
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
var idBuilder = new IdentityBuilder(typeof(User), typeof(Role), services);
idBuilder.AddEntityFrameworkStores<IdDbContext>()
.AddDefaultTokenProviders()
.AddRoleManager<RoleManager<Role>>()
.AddUserManager<UserManager<User>>();
第5步,通过执行Add-Migration、Update-database等命令执行EF Core的数据库迁移,然后程序就会在数据库中生成多张数据库表。这些数据库表都由标识框架负责管理,开发人员一般不需要直接访问这些表。
我在执行迁移时由于存在多个DbContext类,所以需要用到-Context参数指定实体类
Add-Migration init -Context IdDbContext
Update-Database -Context IdDbContext
第6步,编写控制器的代码。我们在控制器中需要对角色、用户进行操作,也需要输出日志,因此通过控制器的构造方法注入相关的服务
private readonly IConfiguration _configuration;
private readonly UserManager<User> userManager;
private readonly RoleManager<Role> roleManager;
public UserController(IConfiguration configuration, RoleManager<Role> roleManager, UserManager<User> userManager)
this._configuration = configuration;
this.roleManager = roleManager;
this.userManager = userManager;
第7步,获取验证邮箱有效性的令牌
var flag = await roleManager.RoleExistsAsync("admin");
if (flag == false)
// 如果admin角色不存在,则创建它
Role role = new Role()
Name = "admin"
var result = await roleManager.CreateAsync(role);
if (result.Succeeded == false)
return Ok(new ReceiveObject<string>()
code = 999999,
msg = "创建角色失败"
User user = await this.userManager.FindByNameAsync(userName);
if (user == null)
// 如果该用户不存在,则创建它
user = new User()
UserName = userName,
Email = emailName,
EmailConfirmed = false,
CreateTime = DateTime.Now
var result = this.userManager.CreateAsync(user);
if (result.Result.Succeeded == false)
return Ok(new ReceiveObject<string>()
code = 999999,
msg = "创建用户失败"
// 创建一个令牌用于确认邮件的有效性
var result_confirm = this.userManager.GenerateEmailConfirmationTokenAsync(user);
return Ok(new ReceiveObject<string>()
code = 999999,
data = string.Format("已向邮箱{0}发送验证码,验证码为{1}", emailName, result_confirm.Result)
第8步,编写创建角色和用户的方法CreateUserRole
var flag = await roleManager.RoleExistsAsync("admin");
if(flag == false)
// 如果admin角色不存在,则创建它
Role role = new Role()
Name = "admin"
var result = await roleManager.CreateAsync(role);
if (result.Succeeded == false)
return Ok(new ReceiveObject<string>()
code = 999999,
msg = "创建角色失败"
User user = await this.userManager.FindByNameAsync(userName);
if(user == null)
return Ok(new ReceiveObject<string>()
code = 999999,
msg = "请先获取邮箱验证码"
// 该用户已创建
// 验证邮件令牌的有效性
var result = this.userManager.ConfirmEmailAsync(user, token);
if (result.Result.Succeeded == false)
return Ok(new ReceiveObject<string>()
code = 999999,
msg = "邮箱验证码无效"
user.EmailConfirmed = true;
result = this.userManager.UpdateAsync(user);
result = this.userManager.AddPasswordAsync(user, password);
// 给该用户赋予admin角色权限
var result_list = this.userManager.GetRolesAsync(user);
if(result_list.Result.Count(m => m.Equals("admin")) > 0)
// 已经给该用户赋予角色权限
return Ok(new ReceiveObject<string>()
code = 0,
msg = "成功"
var result_role = this.userManager.AddToRoleAsync(user, "admin");
if (result_role.Result.Succeeded == false)
return Ok(new ReceiveObject<string>()
code = 999999,
msg = "给用户赋予角色失败"
return Ok(new ReceiveObject<string>()
code = 0,
msg = "成功"
第9步,编写处理登录请求的操作方法LoginAlpla
var user = await userManager.FindByNameAsync(username);
if (user == null)
return Ok(new ReceiveObject<string>()
code = 999999,
msg = $"用户名不存在{username}"
if (await userManager.IsLockedOutAsync(user))
return Ok(new ReceiveObject<string>()
code = 999999,
msg = "用户名已锁定"
var success = await userManager.CheckPasswordAsync(user, password);
if (success)
return Ok(new ReceiveObject<string>()
code = 0,
msg = "成功"
await userManager.AccessFailedAsync(user);
return Ok(new ReceiveObject<string>()
code = 999999,
msg = "用户名或密码错误"
JSON Web Token (JWT) 是一个开放标准 ( RFC 7519 ),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全传输信息。此信息可以验证和信任,因为它是数字签名的。
使用JWT的场景:
授权:这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够在不同的域中轻松使用。
除了JWT之外还有一种鉴权授权方式Session,这是一种有状态的登录方式,而JWT是无状态登录,常用来做单点登录系统。
在日常的使用中Session有以下痛点:
在分布式应用中如果有多个后台Web服务,需要实现共享Session,增加服务器的负担。
由于Session需要配合Cookie使用,容易遭到CSRF的攻击。
如果令牌在Authorization标头中发送,则跨域资源共享 (CORS) 不会成为问题,也不会遭到CSFR攻击,因为它不使用 cookie。
JWT编码规则
JWT由三个部分组成:头部信息(head),有效载荷(payload),签名(secret),前面两者使用Base64URL编码,它与Base64编码之前有一定的区别,转换流程如下:
BASE64URL编码的流程:1、明文使用BASE64进行加密 2、在BASE64的基础上进行一下的编码:2.1)去除尾部的"=" 2.2)把"+"替换成"-" 2.3)把"/"替换成"_"
BASE64URL解码的流程:1)把"-"替换成"+". 2)把"_"替换成"/" . 3)(计算BASE64URL编码长度)%4 a)结果为0,不做处理 b)结果为2,字符串添加"==" c)结果为3,字符串添加"="
签名的作用:防止信息被篡改,在JWT的发放方和接收方需要定义一个密钥,发放方用它加密数据,接收方使用密钥验证是否被篡改。
下图是如何获取并使用JWT的流程图:
1、应用程序或客户端向授权服务器请求授权。
2、当授权被授予时,授权服务器向应用程序返回一个访问令牌。
3、应用程序使用访问令牌访问受保护的资源(如 API)。
(JWT中文网:JWT中文文档网)
(C#技术栈入门到精通系列19——鉴权授权IdentityServer JWT:C#技术栈入门到精通系列19——鉴权授权IdentityServer JWT - BigBox777 - 博客园)
JWT实现登录的流程
1、客户端向服务器端发送用户名、密码等请求登录。
2、服务器端校验用户名、密码,如果校验成功,则从数据库中取出这个用户的ID、角色等用户相关信息。
3、服务器端采用只有服务器端才知道的密钥来对用户信息的JSON字符串进行签名,形成签名数据。
4、服务器端把用户信息的JSON字符串和签名拼接到一起形成JWT,然后发送给客户端。
5、客户端保存服务器端返回的JWT,并且在客户端每次向服务器端发送请求的时候都带上这个JWT。
6、每次服务器端收到浏览器请求中携带的JWT后,服务器端用密钥对JWT的签名进行校验,如果校验成功,服务器端则从JWT中的JSON字符串中读取出用户的信息。
由此可以看出,在JWT机制下,登录用户的信息保存在客户端,服务器端不需要保存数据,这样我们的程序就天然地适合分布式的集群环境,而且服务器端从客户端请求中就可以获取当前登录用户的信息,不需要再去状态服务器中获取,因此程序的运行效率更高。虽然用户信息保存在客户端,但是由于有签名的存在,客户端无法篡改这些用户信息,因此可以保证客户端提交的JWT的可信度。
在ASP.NET Web API中使用JWT
第1步,我们先在配置系统中配置一个名字为JWT的节点,并在节点下创建SigningKey、ExpireSeconds两个配置项,分别代表JWT的密钥和过期时间(单位为秒)。我们再创建一个对应JWT节点的配置类JWTOptions,类中包含SigningKey、ExpireSeconds这两个属性。
做这件事的目的是方便从配置文件中取值,当然也可以使用通过字符串的方式取值。
第2步,通过NuGet为项目安装Microsoft.AspNetCore.Authentication.JwtBearer包,这个包封装了简化ASP.NET Core中使用JWT的操作。
第3步,编写代码对JWT进行配置,把以下内容添加到Program.cs的builder.Build之前。
// 配置JWT
services.Configure<JWTOptions>(Configuration.GetSection("JWT")); // 注册JWT对象
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
var jwtOption = Configuration.GetSection("JWT").Get<JWTOptions>();
var secretByte = Encoding.UTF8.GetBytes(jwtOption.SigningKey);
options.TokenValidationParameters = new TokenValidationParameters()
//只有配置的发布者donesoft.cn才会被接受
ValidateIssuer = false,
//只有配置的使用者donesoft.cn才会被接受
ValidateAudience = false,
//验证token是否过期
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
//对密码进行加密
IssuerSigningKey = new SymmetricSecurityKey(secretByte)
第4步,在Program.cs的Configure方法中的app.UseAuthorization之前添加app.UseAuthentication。
// 能否登录成功
app.UseAuthentication();
// 有哪些访问权限
app.UseAuthorization();
第5步,在UserController类中修改LoginAlpha方法,将JWT的逻辑添加进去。
var user = await userManager.FindByNameAsync(username);
if (user == null)
return Ok(new ReceiveObject<string>()
code = 999999,
msg = $"用户名不存在{username}"
if (await userManager.IsLockedOutAsync(user))
return Ok(new ReceiveObject<string>()
code = 999999,
msg = "用户名已锁定"
var success = await userManager.CheckPasswordAsync(user, password);
if (success)
// 验证通过,返回令牌
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
claims.Add(new Claim(ClaimTypes.Name, user.UserName));
var roles = await userManager.GetRolesAsync(user);
foreach (var item in roles)
claims.Add(new Claim(ClaimTypes.Role, item));
var jwtOption = _configuration.GetSection("JWT").Get<JWTOptions>();
string jwtToken = BuildToken(claims, jwtOption);
return Ok(new ReceiveObject<string>()
code = 0,
msg = "成功",
data = jwtToken
await userManager.AccessFailedAsync(user);
return Ok(new ReceiveObject<string>()
code = 999999,
msg = "用户名或密码错误"
BuildToken方法:
private string BuildToken(List<Claim> claims, JWTOptions jwtOption)
string key = jwtOption.SigningKey;
DateTime expires = DateTime.Now.AddSeconds(jwtOption.ExpireSeconds);
byte[] secBytes = Encoding.UTF8.GetBytes(key);
var secKey = new SymmetricSecurityKey(secBytes);
var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
var tokenDescriptor = new JwtSecurityToken(claims: claims,
expires: expires, signingCredentials: credentials);
string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
return jwt;
BuildToken方法的作用是根据登录用户信息,JWT的密钥,有效期生成令牌。
Claim对象可以看成是身份证上面的一个个键值对,比如:姓名-张三,出生日期-1999年9月9日。
Claim还有一个列表对象ClaimsIdentity,它是Claim对象的集合,比如:身份证包含了个人的姓名,出生日期,证件号码这些信息。
ClaimsIdentity也有一个列表对象ClaimsPrincipal,它是ClaimsIdentity的集合,比如一个人除了拥有身份证外也可以有其他证件,比如护照,驾照,户口薄。
参考:https://www.cnblogs.com/jlion/p/12543486.html
第6步,验证令牌是否有效,在需要登录才能访问的控制器类上添加[Authorize]这个ASP.NET Core内置的Attribute。
var user = this.User;
var id = user.FindFirst(m => m.Type == ClaimTypes.NameIdentifier);
var name = user.FindFirst(m => m.Type == ClaimTypes.Name);
var list_role = user.FindAll(m => m.Type == ClaimTypes.Role);
var result = string.Format("编号:{0},用户名:{1},角色:{2}", id, name, string.Join(',', list_role));
return Ok(new ReceiveObject<string>()
code = 0,
data = result
User对象是控制器类ControllerBase的属性,通过它我们可以快速拿到当前用户的授权信息。
[Authorize]标注
它的作用:[Authorize]这个Attribute既可以被添加到控制器类上,也可以被添加到操作方法上。我们可以在控制器类上标注[Authorize],那么这个控制器类中的所有操作方法都会被进行身份验证和授权验证;对于标注了[Authorize]的控制器类,如果其中某个操作方法不想被验证,我们可以在这个操作方法上添加[AllowAnonymous]。如果没有在控制器类上标注[Authorize],那么这个控制器类中的所有操作方法都允许被自由地访问;对于没有标注[Authorize]的控制器类,如果其中某个操作方法需要被验证,我们也可以在操作方法上添加[Authorize]。
使用令牌调用接口:在发送请求的时候,我们只要按照HTTP的要求,把JWT按照“Bearer JWT”格式放到名字为Authorization的请求报文头中即可。ASP.NET Core会按照HTTP的规范,从Authorization中取出令牌,并且进行校验、解析,然后把解析结果填充到User属性中,这一切都是ASP.NET Core完成的,不需要开发人员自己编写代码。但是,如果由于设置或者代码错误导致校验失败,服务器端只会给出状态码为401的响应,开发人员很难得知问题到底出在哪里。
在Swagger中调试JWT
修改Startup.cs的注册Swagger服务的AddSwaggerGen方法
// 通过对OpenAPI的配置实现从Swagger中发送Authorization报文头
var scheme = new OpenApiSecurityScheme()
Description = "Authorization header. \r\nExample: 'Bearer 12345abcdef'",
Reference = new OpenApiReference
Type = ReferenceType.SecurityScheme,
Id = "Authorization"
Scheme = "oauth2",
Name = "Authorization",
In = ParameterLocation.Header,Type = SecuritySchemeType.ApiKey,
c.AddSecurityDefinition("Authorization", scheme);
var requirement = new OpenApiSecurityRequirement();
requirement[scheme] = new List<string>();
c.AddSecurityRequirement(requirement);
然后在Swagger的首页中点击【Authorize】按钮后输入Bearer + 令牌后保存,令牌从登陆接口获取。这样调用需要授权的接口后系统会自动带入令牌,获取访问权限。
(待续未完)
上一章实现了最基础的应用层和接口层,但是从.net core开始设计讲耦合度太高。,本文主要使用autofac来解耦。
一、操作步骤
1.使用nuget添加Autofac和Autofac.Extensions.DependencyInjection
2.仓储层改造
2.1添加AutofacModule类 YudianPSIEntityFrameworkCoreModule并注册仓储服务
BasicDictionaryRepository
public class YudianPSIEntityFr
ASP.NET-CoreAndEntityFramework-Core_Learn
从零开始学ASP.NET Core与EntityFramework Core_课程练习笔记作者:梁桐铭-微软最杰出专家(Microsoft MVP)网址: ://www.52abp.com/yoyomooc/aspnet-core-for-beginners-
一,使用VS2019创建ASP.NET Core Web程序
在配置新项目菜单栏中,键入项目的名称。我将其命名为StudentManagement 。我们将创建一个ASP.NET核心Web应用程序,在这个程序中,我们将创建,读取,更新,删除学生。
取消删除“为HTTPS配置”替换,如上图所示,关闭身份验证。
空:名称暗示的“空”模板不包含任何内容。这是我们將使用的模板,并从头开始手动设置所有内容,杀死我们清楚地了解不同部分如何组合在一起。
架构设计:ASP.Net Core 3.1 WebApi+Swagger+Jwt+Autofac。
分享初衷:最近在做前后端分离项目,用到了文件上传下载功能,找了很多类似的案例,基本上都是相互转载,很少有原创的(总之找了很久没找到合适的),最后没有达到我想要的预期效果,故写此篇博客。
需求背景:以前在做文件上传下载,都是基于FrameWork项目或者Core Web项目实现的文件上传或下载,基于web的文件下载及权限比较好实现的。但是现在很多公司技术领导层,都在推行前后端分离理念,所以这一次我承担了Core Api 架构设计,采用的是Vue+Core 3.1 WebApi去实现前后端分离.
实现效果图:https://blog.csdn.net/qq_15632461/article/details/108626802
本文开始前先简单介绍一下DDD分层的概念
表现层(Presentation Layer):图中的用户界面层包括用户接口层,用户输入和数据展示。
应用层(Application Layer):应用层定义系统的业务功能,并指挥领域层中的领域对象实现这些功能。
领域层(Domain Layer):核心层,实现所有业务逻辑。
基础设施层(Infrastructure Layer):提供整个业务系统的基础服务。
使用.NET Core和DDD使用Clean Architecture的示例Web API实现。
解决方案设计
解决方案设计着重于基本的领域驱动设计技术和实现,同时使事情尽可能简单,但可以根据需要进行扩展。 使用多个程序集来分离关注点,以使逻辑与其他组件隔离。 .NET 5 C#是此应用程序的默认框架和语言。
KCTest.Domain-此程序集包含公用,实体和接口。
KCTest.Application-该程序集包含所有服务实现。
KCTest.Infrastructure-此程序集包含数据持久性基础结构。
KCTest.API-此程序集是Web api主机。
KCTest.Tests-该程序集包含基于测试框架的单元测试类。
:thought_balloon: 如果您是开源世界的新手,请随时查看我们的“
使用数据验证
如何运行应用程序:
创建一个空数据库,名称: KCTest 。
设置连接字符串(在或通过用户机密机制)。
跑 .. 。
请按照文件中的说明进行操作。
给个星星!
如果您喜欢这个项目,请学习或在应用程序中
Git提交时提示“Please make sure you have the correct access rights and the repository exists.”的解决方法
90799