杨中科B站视频链接:.NET 6教程,.Net Core 2022视频教程,杨中科主讲_哔哩哔哩_bilibili
1、Authentication对访问者的用户身份进行验证,“用户是否登录成功”
2、Authorization验证访问者的用户身份是否有对资源访问的访问权限,“用户是否有权限访问这个地址”。
1、标识(Identity)框架:采用基于角色的访问控制(Role-Based Access Control,简称RBAC)策略,内置了对用户、角色等表的管理以及相关的接口,支持外部登录、2FA等
2、标识框架使用EF Core对数据库进行操作,因此标识框架支持几乎所有的数据库
1、IdentityUser
2、NuGet安装:Microsoft.AspNetCore.Identity.EntityFrameworkCore。
3、创建继承自IdentityDbContext的类
4、可以通过IdDbContext类来操作数据库,不过框架中提供了RoleManager、UserManager等类来简化数据库的操作
5、部分方法的返回值为Task
6、向依赖注入容器中注册标识框架相关的服务
IServiceCollection services = builder.Services;
services.AddDbContext(opt => {
string connStr = builder.Configuration.GetConnectionString("Default");
opt.UseSqlServer(connStr);});
services.AddDataProtection();
services.AddIdentityCore(options =>{ //注意不是AddIdentity
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()
.AddDefaultTokenProviders().AddRoleManager>()
.AddUserManager>();
7、执行Add-Migration、Update-Database等命令执行EF Core的数据库迁移
8、通过RoleManager、UserManager等来进行数据操作。比如创建角色、创建用户
bool roleExists = await roleManager.RoleExistsAsync("admin");
if (!roleExists)
{
Role role = new Role { Name="Admin"};
var r = await roleManager.CreateAsync(role);
if (!r.Succeeded) return BadRequest(r.Errors);
}
User user = await this.userManager.FindByNameAsync("yzk");
if (user == null)
{
user=new User{UserName="yzk",Email="[email protected]",EmailConfirmed=true};
var r = await userManager.CreateAsync(user, "123456");
if (!r.Succeeded) return BadRequest(r.Errors);
r = await userManager.AddToRoleAsync(user, "admin");
}
9、检查登录用户信息
string userName = req.UserName;
string password = req.Password;
var user = await userManager.FindByNameAsync(userName);
if (user == null)
return NotFound($"用户名不存在{userName}");
if (await userManager.IsLockedOutAsync(user))
return BadRequest("LockedOut");
var success = await userManager.CheckPasswordAsync(user, password);
if (success) {
await userManager.ResetAccessFailedCountAsync(user);
return Ok("Success");
}
else await userManager.AccessFailedAsync(user);
重置密码流程
1、生成重置的Token
2、Token发给客户(邮件、短信),形式:链接、验证码等
3、根据Token完成密码的重置
发送重置密码的请求
var user = await userManager.FindByEmailAsync(email);
string token = await userManager.GeneratePasswordResetTokenAsync(user);
logger.LogInformation($"向邮箱{user.Email}发送Token={token}");
完成重置密码
await userManager.ResetPasswordAsync(user, token, password);
1、对于分布式集群环境,Session数据保存在服务器内存中就不合适了,应该放到一个中心状态服务器上。ASP.NET Core支持Session采用Redis、Memcacheed.
2、中心状态服务器有性能问题
1、JWT把登录信息(也称作令牌)保存在客户端
2、为了防止客户端的数据造假,保存在客户端的令牌经过了签名处理,而签名的密钥只有服务器端才知道,每次服务器端接收到客户端提交过来的令牌的时候都要检查一下签名。
3、基于JWT如何实现“登录”
生成JWT令牌
1、NuGet:System.IdentityModel.Token.Jwt
2、var claims = new List
claims.Add(new Claim(ClaimTypes.NameIdentifier, "6"));
claims.Add(new Claim(ClaimTypes.Name, "yzk"));
claims.Add(new Claim(ClaimTypes.Role, "User"));
claims.Add(new Claim(ClaimTypes.Role, "Admin"));
claims.Add(new Claim("PassPort", "E90000082"));
string key = "fasdfad&9045dafz222#fadpio@0232";
DateTime expires = DateTime.Now.AddDays(1);
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);
解码JWT令牌
string[] segments = jwt.Split('.');
string head = JwtDecode(segments[0]);
string payload = JwtDecode(segments[1]);
Console.WriteLine("---head---");
Console.WriteLine(head);
Console.WriteLine("---payload---");
Console.WriteLine(payload);
string JwtDecode(string s)
{
s = s.Replace('-', '+').Replace('_', '/');
switch (s.Length % 4)
{
case 2:
s += "==";
break;
case 3:
s += "=";
break;
}
var bytes = Convert.FromBase64String(s);
return Encoding.UTF8.GetString(bytes);
}
结论:负载中的内容是明文形式保存的;不要把不能被客户端知道的信息放到JWT中;
string secKey = "fasdfad&9045dafz222#fadpio@0232";
JwtSecurityTokenHandler tokenHandler = new();
TokenValidationParameters valParam = new ();
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secKey));
valParam.IssuerSigningKey = securityKey;
valParam.ValidateIssuer = false;
valParam.ValidateAudience = false;
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwt,
valParam,out SecurityToken secToken);
foreach (var claim in claimsPrincipal.Claims)
{
Console.WriteLine($"{claim.Type}={claim.Value}");
}
随便一个密钥来生成一个用户Id等经过篡改后的JWT令牌,然后分别用上一节和这一节的代码尝试解码
步骤
1、配置JWT节点,节点下创建SigningKey、ExpireSeconds两个配置,分别代表JWT的密钥和过期时间(单位:秒)。再创建配置类JWTOptions。包括SigningKey、ExpireSeconds两个属性
2、NuGet:Microsoft.AspNetCore.Authentication.JwtBearer
3、对JWT进行配置
services.Configure(builder.Configuration.GetSection("JWT"));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
var jwtOpt = builder.Configuration.GetSection("JWT").Get();
byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey);
var secKey = new SymmetricSecurityKey(keyBytes);
x.TokenValidationParameters = new()
{
ValidateIssuer=false, ValidateAudience=false, ValidateLifetime=true,
ValidateIssuerSigningKey=true, IssuerSigningKey=secKey
};
});
4、Program.cs的app.UseAuthorization()这行代码之前加app.UseAuthentication()
5、Controller类中进行登录:
var user = await userManager.FindByNameAsync(userName);
var success = await userManager.CheckPasswordAsync(user, password);
if (!success)
return BadRequest("Failed");
var claims = new List();
claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
claims.Add(new Claim(ClaimTypes.Name, user.UserName));
var roles = await userManager.GetRolesAsync(user);
foreach (string role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
string jwtToken = BuildToken(claims, jwtOptions.Value);
6、在需要登录才能访问的控制器类或者Action方法上添加[Authorize]
public IActionResult Hello()
{
string id = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
string userName = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
IEnumerable roleClaims = this.User.FindAll(ClaimTypes.Role);
string roleNames = string.Join(',', roleClaims.Select(c => c.Value));
return Ok($"id={id},userName={userName},roleNames ={roleNames}");
}
7、测试登录和访问。用PostMan自定义报文头:
Authorization的值为”Bearer JWTToken“;Authorization的值中的”Bearer“和JWT令牌之间一定要通过空格分隔。前后不能多出来额外的空格、换行等。
8、JWTToken在Web、APP、小程序等中保存到哪里?
1、ASP.NET Core中身份验证和授权验证的功能由Authentication、Authorization中间件提供:app.UseAuthtication()、app.UseAuthorization()
2、控制器类上标注[Authorize],则所有操作方法都会被进行身份验证和授权验证;对于标注了[Authorize]的控制器中,如果其中某个操作方法不想被验证,可以在操作方法上添加[AllowAnonymous]。如果没有在控制器类上标注[Authorize],那么这个控制器中的所有操作方法都允许被自由地访问;对于没有标注[Authrize]的控制器中,如果其中某个操作方法需要被验证,我们也可以在操作方法上添加[Authorize].
3、ASP.NET Core会按照HTTP协议的规范,从Authorization取出来令牌,并且进行验证、解析,然后把解析结果填充到User属性中,这一切都是ASP.NET Core完成的,不需要开发人员自己编写代码。但是一旦出现401,没有详细的报错信息,很难排查,这是初学者遇到的难题
对OpenAPI进行配置
builder.Services.AddSwaggerGen(c =>
{
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();
c.AddSecurityRequirement(requirement);
});
JWT的缺点
1、到期前,令牌无法被提前撤回。什么情况下需要撤回?用户被删除了、禁用了;令牌被盗用了;单设备登录。
2、需要JWT撤回的场景用传统Session更合适
3、如果需要在JWT中实现,思路:用Redis保存状态,或者用refresh_token+access_token机制等等
我的思路
在用户表中添加一个整数类型的列JWTVersion,代表最后一次发放出去的令牌的版本号;每次登录、发放令牌的时候,都让JWTVersion的值自增,同时讲JWTVersion的值也放到JWT令牌的负载中;当执行禁用用户、撤回用户的令牌等操作的时候,把这个用户对应的JWTVersion的值自增;当服务器收到客户端提交的JWT令牌后,先把JWT令牌中的JWTVersion值和数据库中JWTVersion的值做一下比较,如果JWT令牌中JWTVersion的值小于数据库中JWTVersion的值,就说明这个JWT令牌过期了
实现
1、为用户实体User类增加一个long类型的属性JWTVersion
2、修改登录并发放令牌的代码,把用户的JWTVersion属性的自增,并且把JWTVersion的值写入到JWT令牌中
3、编写一个操作筛选器,统一实现对所有的控制器的操作方法中JWT令牌的检查操作。把JWTValidationFilter注册到Program.cs中MVC的全局筛选器
优化
每一次客户端和Controller的交互的时候,检查JWTVersion的筛选器都要查询数据库,性能太低,可以用缓存进行优化
1、场景,代码运行在后台。比如服务器启动的时候在后台预先加载数据到缓存,每天凌晨3点把数据导出到备份数据库,每隔5秒钟在两张表之间同步一次数据。
2、托管服务实现IHostedService接口,一般编写从BackgroundService继承的类 测试:延迟若干秒再度取文件,在延迟,在输出
3、services.AddHostedService
1、从,NET 6开始,当托管服务中发生未处理异常的时候,程序就会自动停止并退出。可以把HostOptions.BackgroundServiceExceptionBehavior设置为Ignore,程序会忽略异常,而不是停止程序。不过推荐采用默认的设置,因为”异常应该被妥善的处理,而不是被忽略“
2、要在ExecuteAsync方法中把代码用try.....catch包裹起来,当发生异常的时候,记录日志中或发警报等
1、托管服务是以单例的生命周期注册到依赖注入容器中的,因此不能注入生命周期为范围或者瞬态的服务,比如注入EF Core的上下文的话,程序就会抛出异常
2、可以通过构造方法注入一个IServiceScopeFactory服务,它可以用来创建一个IServiceScope对象,这样我们就可以通过IServiceScope来创建短生命周期的服务了。记得在Dispose中释放IServiceScope
托管服务简介
1、常驻后台得托管服务并不需要特殊的技术,我们只要让ExecuteAsync中的代码一直执行不结束就行了。
2、实现的功能就是每隔五秒钟对数据库中的数据做一些汇总,然后把汇总结果写入一个文本文件
var items = ctx.Users.GroupBy(u => u.CreationTime.Date)
.Select(e => new { Date = e.Key, Count = e.Count() });
1、.NET Core中内置了对数据校验的支持,在System.ComponentModel.DataAnnotations这个命名空间下,比如[Required]、[EmailAddress]、[RegularExpression]。
2、演示其在ASP.NET Core中请求中过的使用
3、内置的校验机制的问题:校验规则都是和模型类耦合在一起,违反“单一职责原则”;很多常用的校验都是需要编写自定义校验规则,而且写起来麻烦
FluentValidation
1、FluentValidation:用类似于EF Core中Fluent API的方式进行校验规则的配置,也就是我们可以把模型类的校验放到单独的校验类中
2、FluentValidation在ASP.NET Core项目中的用法
1)NuGet:FluentValidation.AspNetCore
2)
builder.Services.AddFluentValidation(fv => {
Assembly assembly = Assembly.GetExecutingAssembly();
fv.RegisterValidatorsFromAssembly(assembly);// RegisterValidatorsFromAssemblies
});
3)编写模型类Login2Request
public record Login2Request(string Email, string Password, string Password2);
4)编写继承自AbstractValidator的数据校验类
public class Login2RequestValidator: AbstractValidator
{
public Login2RequestValidator()
{
RuleFor(x=>x.Email).NotNull().EmailAddress()
.Must(v=>v.EndsWith("@qq.com")||v.EndsWith("@163.com"))
.WithMessage("只支持QQ和163邮箱");
RuleFor(x => x.Password).NotNull().Length(3, 10)
.WithMessage("密码长度必须介于3到10之间")
.Equal(x => x.Password2).WithMessage("两次密码必须一致");
}
}
5)用Login2Request做Action方法的参数
FluentValidation中注入服务
FluentValidation+DI
1、可以通过构造方法来向数据校验类中注入服务
2、
RuleFor(x => x.UserName).NotNull()
.Must(name=>dbCtx.Users.Any(u=>u.UserName== name))
.WithMessage(c => $"用户名{c.UserName}不存在");
//或
RuleFor(x => x.UserName).NotNull()
.MustAsync((name,_) => dbCtx.Users.AnyAsync(u => u.UserName == name))
.WithMessage(c => $"用户名{c.UserName}不存在");
1、需求:Web聊天;站内通知
2、传统HTTP:只能客户端主动发送请求
3、传统方案:长轮询(Long Polling) 缺点是?
1、WebSocket基于TCP协议,支持二进制通信,双工通信
2、性能和并发能力更强
3、WebSocket独立于HTTP协议,不过我们一般仍然把WebSocket服务器端部署到Web服务器上,因为可以借助HTTP协议完成初始的握手(可选),并且共享HTTP服务器的端口(主要)
1、ASP.NET Core SignaIR(一下简称SignaIR),是.NET Core平台下对WebSocket的封装
2、Hub(集线器),数据交换中心
基本SignaIR项目
需要分别编写服务器端Hub和前端代码
1、创建Web API项目,创建一个继承自Hub类
public class ChatRoomHub:Hub
{
public Task SendPublicMessage(string message)
{
string connId = this.Context.ConnectionId;
string msg = $"{connId} {DateTime.Now}:{message}";
return Clients.All.SendAsync("ReceivePublicMessage", msg);
}
}
2、
builder.Services.AddSignalR()
app.MapControllers()调app.MapHub(“/Hubs/ChatRoomHub”)。默认还要启用CORS。
builder.Services.AddSignalR();
string[] urls = new[] { "http://localhost:3000" };
builder.Services.AddCors(options =>
options.AddDefaultPolicy(builder => builder.WithOrigins(urls)
.AllowAnyMethod().AllowAnyHeader().AllowCredentials())
);
var app = builder.Build();
//这里省略其他UseXXX代码
app.UseCors();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapHub("/Hubs/ChatRoomHub");
app.MapControllers();
3、编写前端项目
npm install @microsoft/signalr
- {{msg}}
协议协商
1、SignalR支持多种服务器推送方式,WebSocket、Server-Sent Events、长轮询(LongPolling)。默认按顺序尝试
2、F12查看协商过程 看negotiate的请求跟服务器端讨论是使用哪种推送方式
3、WebSocket和HTTP是不同的协议,为什么能用同一个端口
4、在【开发人员工具】的【网络】页签中看WebSocket通信过程
协议协商的问题
1、集群中协议协商的问题:“协商”请求被服务器A处理,而接下来的WebSocket请求却被服务器B处理
2、解决方法:粘性会话和禁用协商
3、“粘性会话”(Sticky Session):把来自同一个客户端的请求都转发给同一台服务器上。缺点:因为共享公网IP等造成请求无法被平均的分配到服务器集群;扩容的自适应性不强
4、“禁用协商”:直接向服务器发出WebSocket请求。WebSocket连接一旦建立之后,在客户端和服务器直接就建立了持续的网络连接通道,在这个WebSocket连接中的后续往返WebSocket通信都是由同一台服务器来处理。缺点:无法降级到“服务发送事件”或“长轮询”,不过不是大问题
禁用协议协商的方式
const options = { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets };
connection = new signalR.HubConnectionBuilder()
.withUrl('https://localhost:7047/Hubs/ChatRoomHub', options)
.withAutomaticReconnect().build();
SignalR的分布式问题
1、四个客户端被连接到不同的两个服务器上,由于是不同的两台服务器所以通信服务端不了
2、解决方案:所有服务器连接到同一个信息中间件。
3、官方方案:Redis Backplane
1)NuGet:Microsoft.AspNetCore.SignalR.StackExchangeRedis
2)
builder.Services.AddSignalR().AddStackExchangeRedis("127.0.0.1", options => {
options.Configuration.ChannelPrefix = "Test1_";
});
身份认证
1、目前SignalR问题:谁都能连。讲JWT方案
2、配置SigningKey、ExpireSeconds。创建配置类JWTOptions
3、NuGet:Microsoft.AspNetCore.Authentication.JwtBearer
var services = builder.Services;
services.Configure(builder.Configuration.GetSection("JWT"));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
var jwtOpt = builder.Configuration.GetSection("JWT").Get();
byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey);
var secKey = new SymmetricSecurityKey(keyBytes);
x.TokenValidationParameters = new() {ValidateIssuer = false,
ValidateAudience = false, ValidateLifetime = true,
ValidateIssuerSigningKey = true, ssuerSigningKey = secKey};
x.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/Hubs/ChatRoomHub"))) context.Token = accessToken;
return Task.CompletedTask;
}
};
});
5、app.UseAuthorization()这行代码之前添加app.UseAuthentication()
6、在控制类Test1Controller中增加登录并且创建JWT令牌的操作方法Login。返回JWT
7、在需要登录才能访问的集线器类上或者方法上添加[Authorize]。也支持角色等设置,可以设置Hub或者方法上
8、
筛选客户端
1、客户都拿筛选的3个参数:ConnectionId、组和用户Id(它对应ClaimTypes.NameIdntifier的Claim)
2、Hub的Groups属性为IGroupManager属性,可以对组成员进行管理。查看类型的成员
3、Hub的Clients属性为IHubCallerClients类型,可以对连接到当前集线器的客户端进行筛选
4、IClientProcy类型。无法知道具体有哪些客户端调用SendAsync()方法向筛选的客户端发送信息 5、实现聊天室私聊
实现
public async Task SendPrivateMessage(string destUserName,string message)
{
User? destUser = Users.FindByName(destUserName);
string destUserId = destUser.Id.ToString();
string srcUserName = this.Context.User!.FindFirst(ClaimTypes.Name)!.Value;
string time = DateTime.Now.ToShortTimeString();
await this.Clients.User(destUserId).SendAsync("ReceivePrivateMessage",
srcUserName, time, message);
return "ok";
}
需求
1、英汉词典ECDICT中导入单词到数据库
2、T_WordItems:Id(主键)、Word(单词)、Phonetic(音标)、Definition(英文解释),Translation(中文翻译)
实现
public class ImportDictHub:Hub
{
private readonly ImportExecutor executor;
public ImportDictHub(ImportExecutor executor)
{
this.executor = executor;
}
public Task Import()
{
_=executor.ExecuteAsync(this.Context.ConnectionId);
return Task.CompletedTask;
}
}
1、ImportExecutor中注入IHubContext
2、暂时用字符串Split解析CSV,或者用更专业的库
3、用SqlBulkCopy进行分批快速导入:
using SqlBulkCopy bulkCopy = new SqlBulkCopy(conn);
bulkCopy.DestinationTableName = "T_WordItems";
bulkCopy.ColumnMappings.Add("Word", "Word");
...
DataTable dataTable = new DataTable();
dataTable.Columns.Add("Word");
...
var dataRow = dataTable.NewRow();
dataRow["Word"] = word;
dataTable.Rows.Add(dataRow);
...
await bulkCopy.WriteToServerAsync(dataTable);
4、前端:
1、不能直接把bin/Debug部署到生产环境的服务器上,性能低。应该创建网站的发布版,用【发布】功能
2、两种部署模式:“框架依赖”和“独立”,两者的优缺点
3、独立模式为什么要选目标操作系统和CPU类型。关于龙芯
1、在Windows(SandBox)和Linux(VMWare Player)里分别运行网站
2、如何在生产服务器中部署.NET Core网站。尽管Kestrel已经强大到足以作为一个独立的Web服务器被使用了,但是一般仍然不会让Kestrel直接面对终端用户的请求。配置域名证书、记录请求日志、Url重写等由反向代理服务器负责
3、多种部署模式:K8S+容器(推荐、难度高);Linux+Nginx;云平台;Windows+IIS
1、启用HTTPS
2、如果如果运维人员需要通过远程桌面或者SSH连接到服务器,那么一定要在服务器的防火墙上设置只允许运维人员的IP段访问相关端口
3、严格区分开发环境和生产环境
4、不要相信客户端请求