--- typora-root-url: ./ --- # 1、什么是领域驱动 领域驱动,Domain Driven Design。 一张桌子有4条腿。程序员可能的第一反应是通过`Table`类和`Leg`类描述两者之间的关系。一个人戴着一顶帽子,程序员可能的第一反应是一个`Person`类有一个`Hat`属性。 这似乎是顺其自然的、合乎事理的。但是,大家是否会觉得这是一种`Code Driven Implementation`?的确,领域驱动最终会落实到代码层面,**但领域驱动的意义在于:在写代码之前,我们先理解领域,理解业务概念、约束、行为、规则等等**。 领域驱动似乎传递着*问题第一代码第二*、*市场第一代码第二*、*业务第一代码第二*的理念。程序员不再是拿着锤子到处找钉子的人,而是让自己首先成为一个领域专家,或者成为一个领域专家的倾听者。程序员不仅对代码负责,还对利益相关方负责,对业务负责。程序员关注的焦点不仅在解决问题本身,而是是否真的解决了问题。 当我们谈领域驱动,其实与编程语言、代码、数据库、微服务都无关,而是一种更高层面的设计。 # 2、分层结构 ## 2.1 领域抽象层 > 聚合根标记接口 ``` public interface IAggregateRoot{} ``` 什么是聚合根?理解聚它首先要理解聚合`Aggregate`。假设有一个银行转账场景,程序员A和领域专家B正在进行对话: A:我们目前想解决的问题是什么? B:做一个客户转账的功能。 A:能具体说一下吗? B: 客户X给客户Y转账100元,X的账户上少了100元,Y的账户上增加100元。我们希望所有的转账记录都可以被追溯。 接着,A和B在白板上进行了一次事件风暴`Event Storming`。首先,在列出了影响系统状态的事件。 ![](/ddd1.png) 事件作用在哪里?谁触发事件?触发什么事件? ![](/ddd2.png) 显然,事件围绕账户而进行。在`DDD`中适用聚合`Aggregate`这个概念对相关性事件进行逻辑划分。 有两种做法。**一种是把聚合看作名词**,比如在这里定义`Account`类。 ``` public class Account : IAggregateRoot{} ``` 以上的`Account`就是聚合,可以被持久化到数据库,我们目前的架构采用的就是这种方式。 **另一种做法是把聚合看作动词**。其中有两个关键的视角:组成和规则。 **聚合组成:** - 转账的唯一性:每次转账都有其唯一编号。创建`TransferNumber`用来生成唯一编号 - 出账: 从一个账户中扣除转账金额。创建`Debit`类用来描述出账 - 入账: 把转账金额存入某个账户。创建`Credit`类用来描述入账 - 账户:出账和入账都用到了账户。创建`AccountNumber`类用来描述账户 **聚合规则:** - 出账:转账的金额必须大于零,出账账户不能为空 - 入账:入账的金额必须大于零,入账账户不能为空 ``` //每次转账唯一编号的封装 public class TranferNumber { public string Value {get; private set;} public Guid EntityId {get; private set;} public TranferNumber(string value, Guid entityId) { Value = value; EntityId = entityId; } } //出账 public class Debit { public decimal Value {get; private set; } public AccountNumber Account {get; private set; } public Debit(decimal value, AccountNumber account) { Value = value; Account = account; value.Must(v => v >=0);//business rule account.MustNotBeNull();//ensure value object is valid } } //入账 public class Credit { public decimal Value {get; private set; } public AccountNumber Account {get; private set; } public Credit(decimal value, AccountNumber account) { value.Must(v => v >=0);//business rule account.MustNotBeNull();//ensure value object is valid Value = value; Account = account; } } //账号的封装 public class AccountNumber : IEquatable { public string Number {get; private set;} public AccountNumber(string number) { Number = number; } public bool Equals(AccountNumber other) => other !=null && Number == other.Number; } public class TransferedRegistered { public Guid EntityId {get; set;} public string TransferNumber {get; set;} public string DebitAccountNo {get;set;} public string CreditAccountNo {get;set;} public DateTimeOffset CreatedOn {get;set;} = DateTimeOffset.Now; } //Aggregate Root public static class TransferAccount { public static TransferRegistered Create(TransferNumber number, Debit debit, Credit credit) { number.MustNotBeNull(); debit.MustNotBeNull(); credit.MustNotBeNull(); debit.Account.MustNotBeNull(credit.Account); var ev = new TransferedRegistered(); ev.EntityId = number.EntityId; ev.TransferNumber = number.Value; en.Amount = debit.Value; ev.DebitAccountNo = debit.Account.Number; ev.CreditAccountNo = credit.Account.Number; return ev; } } ``` 以上`TransferAccount`就是聚合,聚合无需持久化到数据库,状态的改变需要持久化到数据库。 > 领域接口和抽象实现 ``` public interface IEntity{} public interface IEntity : IEntity{} public abstract class Entity : IEntity { //保存所有的事件,针对领域的事件在领域内部调用 private List _domainEvents; public IReadOnlyCollection DomainEvents => _domainEvents?.AsReadOnly(); public void AddDomainEvent(IDomainEvent eventItem) { _domainEvents = _domainEvents ?? new List(); _domainEvents.Add(eventItem); } public void RemoveDomainEvent(IDomainEvent eventItem) { _domainEvents?.Remove(eventItem); } public void ClearDomainEvents() { _domainEvents?.Clear(); } } ``` > 领域事件接口和领域事件处理接口 ``` using MediatR; public interface IDomainEvent : INotification{} pubic interface IDomainEventHandler : INotificationHandler where TDomainEvent : IDomainEvent{} ``` > 值对象: 如果两个值对象的值都一样,这两个值对象可以相互替换 ``` public abstract class ValueObject{} ``` ## 2.2 基础设施抽象层 > 工作者单元接口 ``` public interface IUnitOfWork : IDisposable { Task SaveChangesAsync(CancellationToken cancellationToken = default); Task SaveEntitiesAsync(CancellationToken cancellationToken = default); } ``` > 事务接口 ``` public interface ITransaction { IDbContextTransaction GetCurrentTransaction(); bool HasActiveTransaction { get; } Task BeginTransactionAsync(); Task CommitTransactionAsync(IDbContextTransaction transaction); void RollbackTransaction(); } ``` > 自定义上下文 ``` public class EFContext : DbContext, IUnitOfWork, ITransaction { protected IMediator _mediator; ICapPublisher _capBus; public EFContext(DbContextOptions options, IMediator mediator, ICapPublisher capBus) : base(options) { _mediator = mediator; _capBus = capBus; } public async Task SaveEntitiesAsync(CancellationToken cancellationToken = default) { var result = await base.SaveChangesAsync(cancellationToken); await _mediator.DispatchDomainEventsAsync(this); return true; } ... } ``` > 仓储接口 ``` public interface IRepository where TEntity : Entity, IAggregateRoot{} public interface IRepository : IRepository where TEntity : Entity, IAggregateRoot{} ``` > 仓储抽象实现 ``` public abstract class Repository : IRepository where TEntity : Entity, IAggregateRoot where TDbContext : EFContext { } ``` > 管道行为 在管道内提交事务 ``` //在MedatR的IPipelineBehavior基础上注入上下文 public class TransactionBehavior : IPipelineBehavior where TDbContext : EFContext { ILogger _logger; TDbContext _dbContext; public TransactionBehavior(TDbContext dbContext, ILogger logger) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) { var response = default(TResponse); var typeName = request.GetGenericTypeName();//object类型的TRequest的扩展方法,获取泛型类型名称 try { if (_dbContext.HasActiveTransaction) { return await next(); } var strategy = _dbContext.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => { Guid transactionId; using (var transaction = await _dbContext.BeginTransactionAsync()) using (_logger.BeginScope("TransactionContext:{TransactionId}", transaction.TransactionId)) { _logger.LogInformation("----- 开始事务 {TransactionId} ({@Command})", transaction.TransactionId, typeName, request); response = await next(); _logger.LogInformation("----- 提交事务 {TransactionId} {CommandName}", transaction.TransactionId, typeName); await _dbContext.CommitTransactionAsync(transaction); transactionId = transaction.TransactionId; } }); return response; } catch (Exception ex) { _logger.LogError(ex, "处理事务出错 {CommandName} ({@Command})", typeName, request); throw; } } } ``` 扩展方法,获取泛型类型名称: ``` public static class GenericTypeExtensions { public static string GetGenericTypeName(this Type type) { var typeName = string.Empty; if (type.IsGenericType) { var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray()); typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>"; } else { typeName = type.Name; } return typeName; } public static string GetGenericTypeName(this object @object) { return @object.GetType().GetGenericTypeName(); } } ``` ## 2.3 共用层 - 异常处理 异常处理接口:`IknownException` ``` public interface IKnownException { string Message { get; } int ErrorCode { get; } object[] ErrorData { get; } } ``` 异常处理接口实现:`KnownException` ``` public class KnownException : IKnownException { public string Message { get; private set; } public int ErrorCode { get; private set; } public object[] ErrorData { get; private set; } public readonly static IKnownException Unknown = new KnownException { Message = "未知错误", ErrorCode = 9999 }; public static IKnownException FromKnownException(IKnownException exception) { return new KnownException { Message = exception.Message, ErrorCode = exception.ErrorCode, ErrorData = exception.ErrorData }; } } ``` ## 2.4 领域层 > 引用 引用领域抽象层。 > 聚合 ``` public class Order : Entity, IAggregateRoot { public string UserId {get; private set;} public string UserName {get; private set;} public int ItemCount {get; private set;} public Address Address {get; private set;} public Order (string userId, string userName, int itemCount, Address address) { this.UserName = userName; this.UserId = userId; this.ItemCount = itemCount; this.AddDomainEvent(new OrderCreatedDomainEvent(this)); } public void ChangeAddress(Address address) { this.Address = address; //TODO:这里也可以添加事件 } } ``` > 领域事件 ``` public class OrderCreatedDomainEvent : IDomainEvent { public Order Order {get; private set;} public OrderCreatedDomainEvent(Order order) { this.Order = order; } } ``` ## 2.5 基础设施层 > 引用 - 基础设施抽象层 - 领域层 > 上下文 ``` public class OrderingContext : EFContext { public OrderingContext(DbContextOptions options, IMediator mediator, ICapPublisher capBus) : base(options, mediator, capBus){} public DbSet Orders { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder){ modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration()); base.OnModelCreating(modelBuilder); } } ``` > 领域约束 ``` class OrderEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasKey(p => p.Id); builder.ToTable("order"); builder.Property(p => p.UserId).HasMaxLength(20); builder.Property(p => p.UserName).HasMaxLength(30); builder.OwnsOne(o => o.Address, a => { a.WithOwner(); a.Property(p => p.City).HasMaxLength(20); a.Property(p => p.Street).HasMaxLength(50); a.Property(p => p.ZipCode).HasMaxLength(10); }); } } ``` > 管道 ``` public class OrderingContextTransactionBehavior : TransactionBehavior { public OrderingContextTransactionBehavior(OrderingContext dbContext, ILogger> logger) : base(dbContext, logger) { } } ``` ## 2.6 应用层和接口层 > 引用 - 基础设施层 > `IServiceCollection`的扩展 > 领域事件的处理链路 - 在接口层或应用层定义`IRequest` ``` public class CreateOrderCommand : IRequest { public CreateOrderCommand(int itemCount) { ItemCount = itemCount; } public long ItemCount {get; private set;} } ``` - 在接口层把`IRequest`传入 ``` [HttpPost] public async Task CreateOrder([FromBody]CreateorderCommand cmd) { return await _medator.Send(cmd, HttpContext.RequestAborted); } ``` - 在接口层或应用层的`IRequestHandler`对`IRequest`进行处理 ``` public class CreateOrderCommandHandler : IRequestHandler { IOrderRepository _orderRepository; ICapPublisher _capPublisher; public CreateOrderCommandHandler(IOrderRepository orderRepository, ICapPublisher capPublisher) { _orderRepository = orderRepository; _capPublisher = capPublisher; } public async Task Handle(CreateOrderCommand request, CancellationToken cancellationToken) { var address = new Address("wen san lu", "hangzhou", "310000"); var order = new Order("xiaohong1999", "xiaohong", 25, address); _orderRepository.Add(order); await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); return order.Id; } } ``` - 领域层的领域在创建`Order`的同时添加了领域事件 ``` public class Order : Entity, IAggregateRoot { public Order(string userId, string userName, int itemCount, Address address) { this.UserId = userId; this.UserName = userName; this.Address = address; this.ItemCount = itemCount; this.AddDomainEvent(new OrderCreatedDomainEvent(this));//添加领域事件 } } ``` - 领域抽象层的`Entity`抽象基类定义了对事件的处理 ``` public abstract class Entity : IEntity { private List _domainEvents; public IReadOnlyCollection DomainEvents => _domainEvents?.AsReadOnly(); public void AddDomainEvent(IDomainEvent eventItem) { _domainEvents = _domainEvents ?? new List(); _domainEvents.Add(eventItem); } public void RemoveDomainEvent(IDomainEvent eventItem) { _domainEvents?.Remove(eventItem); } public void ClearDomainEvents() { _domainEvents?.Clear(); } } ``` - 基础设施抽象层的`EFContext`在把数据持久化到数据库的同时会发布领域事件 ``` public class EFContext : DbContext, IUnitOfWork, ITransaction { public async Task SaveEntitiesAsync(CancellationToken cancellationToken = default) { var result = await base.SaveChangesAsync(cancellationToken); await _mediator.DispatchDomainEventsAsync(this);//把事件发布出去 return true; } } ``` - 基础设施抽象层定义了对`MedatR`的扩展方法,本质上是把上下文所有领域的事件发布出去。 ``` public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx) { var domainEntities = ctx.ChangeTracker .Entries() .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()); var domainEvents = domainEntities .SelectMany(x => x.Entity.DomainEvents) .ToList(); domainEntities.ToList() .ForEach(entity => entity.Entity.ClearDomainEvents()); foreach (var domainEvent in domainEvents) await mediator.Publish(domainEvent); } ``` - 应用层和接口中定义对领域事件的处理 ``` public class OrderCreatedDomainEventHandler : IDomainEventHandler { ICapPublisher _capPublisher; public OrderCreatedDomainEventHandler(ICapPublisher capPublisher) { _capPublisher = capPublisher; } public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken) { await _capPublisher.PublishAsync("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id)); } } ``` 这样,领域事件就被发布到`Event Bus`上,与外界产生了交互,很有可能与外界的另外一个微服务产生交互。 # 3、集成事件 ![ddd3](/ddd3.png) > 在各个微服务的数据库中一般有2张表,一张用来记录发布事件,另一张用来记录订阅事件。这些事件会随着领域事件的发布持久化到数据库,CAP可以做到业务逻辑和发布订阅事件的一致性。 ![](/ddd4.png) 在应用层或接口层,定义一个集成事件。 ``` public class OrderCreatedIntegrationEvent { public OrderCreatedIntegrationEvent(long orderId) => OrderId = orderId; public long OrderId { get; } } public class OrderPaymentSucceededIntegrationEvent { public OrderPaymentSucceededIntegrationEvent(long orderId) => OrderId = orderId; public long OrderId { get; } } ``` 在应用层或接口层,定义`IDomainEventHandler`发布集成事件。 ``` public class OrderCreatedDomainEventHandler : IDomainEventHandler { //中国开源社区,用于消息的发布和订阅,可以发布到RabbitMQ或者Kafa等 ICapPublisher _capPublisher; public OrderCreatedDomainEventHandler(ICapPublisher capPublisher) { _capPublisher = capPublisher; } public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken) { await _capPublisher.PublishAsync("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id)); } } ``` 在应用层或接口层,定义订阅接口。 ``` public interface ISubscriberService { void OrderPaymentSucceeded(OrderPaymentSucceededIntegrationEvent @event); } ``` 订阅接口实现。 ``` public class SubscriberService : ISubscriberService, ICapSubscribe { IMediator _mediator; public SubscriberService(IMediator mediator) { _mediator = mediator; } [CapSubscribe("OrderPaymentSucceeded")] public void OrderPaymentSucceeded(OrderPaymentSucceededIntegrationEvent @event) { //Do SomeThing } [CapSubscribe("OrderCreated")] public void OrderCreated(OrderCreatedIntegrationEvent @event) { //Do SomeThing } } ``` # 4、使用RabbitMQ实现EventBus 安装`RabbitMQ`。 CAP的事件如何与业务逻辑放在同一个事务呢?在基础设施抽象层的`EFContext`中定义如下: ``` public class EFContext : DbContext, IUnitOfWork, ITransaction { protected IMediator _mediator; ICapPublisher _capBus; public EFContext(DbContextOptions options, IMediator mediator, ICapPublisher capBus) : base(options) { _mediator = mediator; _capBus = capBus; } ...... public Task BeginTransactionAsync() { if (_currentTransaction != null) return null; _currentTransaction = Database.BeginTransaction(_capBus, autoCommit: false); return Task.FromResult(_currentTransaction); } ...... } ``` 这样保证了CAP事件的提交回滚与业务事务的提交回滚保持一致。 在应用层或接口层,注册CAP使用`RabbitMQ`。 ``` public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) { services.AddTransient();//有关CAP的接口和实现 services.AddCap(options => { options.UseEntityFramework(); options.UseRabbitMQ(options => { configuration.GetSection("RabbitMQ").Bind(options); }); //options.UseDashboard(); }); return services; } ``` 在`appsettings.json`中: ``` "Mysql": "server=localhost;port=3306;user id=root;password=123456;database=geektime;charset=utf8mb4;ConnectionReset=false;", "RabbitMQ": { "HostName": "localhost", "UserName": "guest", "Password": "guest", "VirtualHost": "/",//将RabbitMQ空间划分为不同的空间,每个空间可以被认为是一个租户。相同的VirtualHost可以被认为是一个集群 "ExchangeName": "queue" } ``` 在`Startup.cs`中注册: ``` services.AddEventBus(Configuration); ``` # 5、`gRPC`内部服务间的通讯利器 `gRPC`是一个远程调用框架,由`Google`公司发起并开源。通过`gRPC`让我们可以像调用本地类一样调用远程的服务。提供几乎所有语言的实现。基于HTTP/2,开放协议,受到广泛支持,易于实现和集成。默认使用`Protocol Buffers`序列化,性能较于`RESTful Json`好很多。工具链成熟,代码生成便捷,开箱即用。支持双向流式的请求和响应,对批处理、低延时场景友好。`gRPC`使用自制证书,使用非加密的`HTTP2`。 `.NET`提供基于`HttpClient`的原生框架实现。提供原生的`ASP.NET Core`集成库。提供完整的代码生成工具。`Visual Studio`和`Visual Studio Code`提供`proto`文件的智能提示。可以通过命令行工具创建服务。 服务端引用包: - `Grpc.AspNetCore` 客户端核心包: - `Google.Protobuf` - `Grpc.Net.Client` - `Grpc.Net.ClientFactory` - `Grpc.Tools`:提供命令行工具使用的包,用来基于`protocol`文件生成服务代码。`.proto`文件定义包、库名,定义服务`service`,定义输入输出模型`message`。然后借助`Grpc.Tools`生成服务端和客户端代码。 `gRPC`异常处理引用包: - `Grpc.Core.RpcException` - `Grpc.Core.Interceptors.Interceptor` > 服务端 `order.proto` ``` syntax = "proto3";//协议版本 option csharp_namespace="GrpcServices";//命名空间 package GrpcServices; service OrderGrpc {//服务 rpc CreateOrder(CreateOrderCommand) returns (CreateOrderResult); } message CreateOrderCommand { //输入,字段的顺序决定了序列化顺序 string buyerId = 1; int32 productId = 2; double unitPrice = 3; double discount = 4; int32 units = 5; } message CreateOrderResult {//响应 int32 orderId = 1; } ``` 会自动生成`Order`和`OrderGrpc`两个类。 `OrderService.cs` ``` public class OrderService : OrderGrpc.OrderGrpcBase { public override Task CreateOrder(CreateOrderCommand request, ServerCallContext context) { return Task.FromResult(new CreateOrderResult {OrderId=24;}); } } ``` `Startup.cs` ``` sevices.AddGrpc(options => { options.EnaleDetailErrors = false; options.Intercoptions.Add(); }); app.UseEndpoints(endpoints => { endpoints.MapGrpcService(); }); ``` `appSettings.json` ``` "Kestrel":{ "Endpoints":{ "Http":{ "Url":"http://+:5000" }, "Https":{ "Url":"http://+:5001" }, "Http2":{ "Url":"http://+:5002", "Protocols":"Http2" } } } ``` > 客户端 项目文件中引用服务端的`order.proto`,这样可以自动生成客户端代码。 ``` ``` `Startup.cs` ``` AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport",true);//允许使用不加密的HTTP/2协议 services.AddGrpcClient(options => { //options.Address= new Uri("https://localhost:5000");//服务端地址 options.Address= new Uri("https://localhost:5002");//不配置证书使用Grpc }).ConfigurePrimaryHttpMessageHandler(provider => { var handler = new SocketHttpHandler(); handler.SslOptions.RemoteCeretificateValidationCallback = (a, b, c, d) => true; return handler; }); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { OrderGrpcClient service = context.RequestServices.GetService(); try { var r = service.CreateOrder(new CreateOrderCommand {BuyerId=""}); } catch(Exception ex) { } }); }); ``` # 6、`Polly`失败重试和熔断机制 组件包: - `Polly` - `Polly.Extensions.Http` - `Microsoft.Extensions.Http.Polly` `Polly`的能力 - 失败重试 - 服务熔断:服务不可用时快速响应一个熔断结果,避免持续请求不可用的服务导致跪掉 - 超时处理:当超时发生,比如说可以返回一个缓存结果 - 舱壁隔离:为服务定义最大的流量和队列,避免请求过服务被压崩 - 缓存策略 - 失败降级:当服务不可用时,响应一个更友好的结果而不是报错 - 组合策略 `Polly`的使用步骤 - 定义要处理的异常类型和返回值 - 定义要处理的动作(重试、熔断、降级响应) - 使用定义的策略来执行代码 适合失败重试的场景 - 服务失败时短暂的,可自愈的 - 服务时幂等的,重复调用不会有副作用 - 网络闪断 - 部分服务节点异常 最佳实践 - 设置失败重试次数 - 设置带有步长策略的失败等待间隔:否则会持续不断地重试,类似`DDOS`攻击 - 设置降级响应:当失败重试次数达到上限,为服务提供一个降级响应,更好的响应结果 - 设置短路器:当重试一定次数,可能服务还是不可用 # 7、网关与`BFF` `BFF`是指`Backend For Frontend`,负责认证授权,负责服务聚合,目标是为前端提供服务。网关和`BFF`的职责可以是重叠的。 ![](/ddd5.png) ![](/ddd6.png) ![ddd7](/ddd7.png) 打造网关 - 添加`Ocelot` - 添加配置文件`ocelot.json` - 添加配置读取代码 - 注册`Ocelot`服务 - 注册`Ocelot`中间件 网关和负载均衡的区别 | 方面 | 网关 | 负载均衡 | | ---------------- | ------ | ------------------ | | OSI模型 | 第7层 | 第4层 | | 基于url的路由 | 可以 | 不可以 | | 基于cookie的路由 | 可以 | 不可以 | | web防火墙 | 可以 | 不可以 | | 地域性 | 任意ip | 云服务商下的某个ip | | | | | 项目介绍 - `Ordering.API` ``` "applicationUrl":"https://localhost.5001;http://localhost:5000" ``` ``` public class TestController : ControllerBase { [HttpGet] public IActionResult Abc() { return Content("Ordering.API"); } } ``` - `Mobile.ApiAggregator` ``` "applicationUrl":"https://localhost.5005;http://localhost:5004" ``` ``` public class TestController : ControllerBase { [HttpGet] public IActionResult Abc() { return Content("Mobile.ApiAggregator"); } } ``` - `Mobile.Gateway`网关项目 `appSettings.json` ``` "Apollo": { "AppId": "geektime-mobile-gateway", "Env": "DEV", "MetaServer": "http://192.168.67.76:8080", "ConfigServer": [ "http://192.168.67.76:8080" ] }, "AllowedHosts": "*", "ReRoutes": [ { "DownstreamPathTemplate": "/api/{everything}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5004 } ], "UpstreamPathTemplate": "/mobileAgg/api/{everything}", "UpstreamHttpMethod": [] }, { "DownstreamPathTemplate": "/api/{everything}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5000 } ], "UpstreamPathTemplate": "/mobile/api/{everything}", "UpstreamHttpMethod": [] } ], "GlobalConfiguration": { "RequestIdKey": "OcRequestId", "AdministrationPath": "/administration" }, "SecurityKey": "aabbccddffskldjfklajskdlfjlas234234234" ``` `Startup.cs` ``` services.AddOcelot(Configuration); app.UseOcelot().Wait(); ``` `TestController.cs` ``` public IActionResult Abc() { return Content("GeekTime.Mobile.Gateway"); } ``` # 8、防跨站请求伪造 ![ddd8](/ddd8.png) 如何防御 - 不适用`Cookie`存储或传输身份信息 - 适用`AntiforgeryToken`机制 - 避免适用`GET`作为业务操作的请求方法 两种选择 - `ValidateAntiForgeryToken` - `AutoValidateAntiforgeryToken` 举例 ``` [ValidateAntiForgeryToken] public IActionResult CreateOrder(string itemId, int count) { _logger.LogInformation("创建了订单itemId:{itemId},count:{count}", itemId, count); return Content("Order Created"); } ``` # 9、防开放重定向攻击 ![ddd9](/ddd9.png) 防范错误 - 使用`LoalRedirect`处理重定向:适合重定向仅限于本站的情况 - 验证重定向的目标域名是否合法 举例 ``` public async Task Login([FromServices]IAntiforgery antiforgery, string name, string password, string returnUrl) { HttpContext.Response.Cookies.Append("CSRF-TOKEN", antiforgery.GetTokens(HttpContext).RequestToken, new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false }); var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);//一定要声明AuthenticationScheme identity.AddClaim(new Claim("Name", "小王")); await this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity)); if (string.IsNullOrEmpty(returnUrl)) { return Content("登录成功"); } try { var uri = new Uri(returnUrl); ///uri.Host 根据配置表数据库表数据来验证 return Redirect(returnUrl); } catch { return Redirect("/"); } //return Redirect(returnUrl); } ``` # 10、防跨站脚本 ![ddd10](/ddd10.png) 防范措施 - 对用户内容进行验证,拒绝恶意脚本 - 对用户提交的内容进行编码`UrlEncoder`,`JavaScriptEncoder` - 慎用`HtmlString`和`HtmlHelper.Raw` - 身份信息`Cookie`设置为`HttpOnly` - 避免使用`Path`传递带有不受信的字符,使用`Query`进行传递 举例 ``` services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => { options.LoginPath = "/home/login"; options.Cookie.HttpOnly = true;//这里的设置生效 }); ``` # 11、跨域请求 同源 - 方案相同(`HTTP/HTTPS`) - 主机或域名相同 - 端口相同 `CORS`是什么 - 浏览器允许跨域发起请求 - 是浏览器的行为协议 - 并不会让服务器拒绝其它路径发起的`HTTP`请求 - 开启时需要考虑是否存在被恶意网站攻击的情形 `CORS`请求头 - `Origin`请求源 - `Access-Control-Reqyest-Method` - `Access-Control-Request-Headers` `CORS`响应头 - `Access-Control-Allow-Origin` - `Access-Control-Allow-Credentials` - `Access-Control-Expose-Headers` - `Access-Control-Max-Age` - `Access-Control-Allow-Methods` - `Access-Control-Allow-Headers` 默认支持的`Expose Headers` - `Cache-Control` - `Content-Language` - `Content-Type` - `Expires` - `Last-Modified` - `Pragma` 举例 ``` services.AddCors(options => { options.AddPolicy("api", builder => { builder.WithOrigins("https://localhost:5001").AllowAnyHeader().AllowCredentials().WithExposedHeaders("abc"); builder.SetIsOriginAllowed(orgin => true).AllowCredentials().AllowAnyHeader(); }); }); app.UseCors(); ``` # 12、为不同场景设计合适的缓存策略 缓存的场景 - 计算结果缓存:反射对象缓存 - 请求结果缓存:`DNS`缓存 - 临时共享数据缓存:会话缓存 - 热点内容缓存:商品详情页 - 热点变更逻辑数据:秒杀库存数 缓存策略 - 越接近最终的输出结果效果越好 - 命中率越高越好 缓存位置 - 浏览器中 - 反向代理服务器(负载均衡) - 应用进程内存中 - 分布式存储系统中 要点 - `Key`的生成策略,表示数据范围业务含义 - 缓存失效策略,过期时间机制,主动刷新机制 - 缓存更新策略,表示更新缓存数据的时机 几个问题 - 缓存失效,导致数据不一致 - 缓存穿透,查询无数据时,导致缓存不生效,查询都落在数据库 - 缓存击穿,缓存失效瞬间,大量请求访问到数据库 - 缓存雪崩,大量缓存同一时间失效,导致数据库压力 内存缓存和分布式缓存的区别 - 内存缓存可以存储任意对象 - 分布式缓存的对象需要支持序列化 - 分布式缓存远程请求可能失败,内存缓存不会