在对Service Fabric传输中的可靠服务的调用中传递用户和审核信息

时间:2017-01-13 07:51:42

标签: azure azure-service-fabric

如何以简单的方式在客户端和服务之间传递审计信息,而无需将该信息作为所有服务方法的参数添加?我可以使用邮件标头为呼叫设置此数据吗?

是否有办法允许服务也沿着下游传递,即,如果ServiceA调用ServiceC调用ServiceC,可以将相同的审核信息发送到第一个A,然后在A的呼叫中发送给B,然后在B的呼叫中C 1

1 个答案:

答案 0 :(得分:14)

如果您使用fabric transport进行远程处理,实际上有一个在客户端和服务之间传递的标头概念。如果您正在使用Http传输,那么您就可以像使用任何http请求那样拥有标头。

请注意,下面的提案不是最简单的解决方案,但它解决了问题,一旦它到位并且很容易使用,但如果你在整个代码库中寻找容易,这可能不是通往走。如果是这种情况,那么我建议您只需在所有服务方法中添加一些常见的审计信息参数。当然,当一些开发人员忘记添加它时,或者在调用流服务时未正确设置它时,一个重要的警告。这完全取决于代码中的权衡:)。

打下兔子洞

在结构传输中,通信涉及两个类:客户端的IServiceRemotingClient实例和服务端的IServiceRemotingListener实例。在来自客户端的每个请求中,发送了messgae body ServiceRemotingMessageHeaders。开箱即用的这些标题包括哪个接口(即哪个服务)和哪个方法被调用的信息(以及底层接收器如何知道如何解包作为主体的字节数组)。对于通过ActorService的Actors调用,其他Actor信息也包含在这些头文件中。

棘手的部分是挂钩进入该交换并实际设置然后阅读其他标题。请在这里忍受我,我们需要了解的幕后工作中涉及的一些课程。

服务方

当您为服务设置IServiceRemotingListener时(无状态服务的示例),您通常使用便利扩展方法,如下所示:

 protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
 {
     yield return new ServiceInstanceListener(context => 
         this.CreateServiceRemotingListener(this.Context));
 }

(另一种方法是实现自己的倾听者,但这不是我们在这里做的事情,我们不想添加内容现有基础设施的顶部。请参阅下面的方法。)

这是我们可以提供自己的监听器的地方,类似于窗帘背后的扩展方法。让我们首先看一下这种扩展方法的作用。它会在您的服务项目上查找程序集级别的特定属性:ServiceRemotingProviderAttribute。那个是abstract,但是你可以使用的那个,如果没有提供,你将获得默认实例FabricTransportServiceRemotingProviderAttribute。将其设置为AssemblyInfo.cs(或任何其他文件,它是一个程序集属性):

[assembly: FabricTransportServiceRemotingProvider()]

此属性有两个有趣的可覆盖方法:

public override IServiceRemotingListener CreateServiceRemotingListener(
    ServiceContext serviceContext, IService serviceImplementation)
public override IServiceRemotingClientFactory CreateServiceRemotingClientFactory(
    IServiceRemotingCallbackClient callbackClient)

这两种方法负责创建监听器和客户端工厂。这意味着它也由交易的客户端进行检查。这就是为什么它是服务程序集的程序集级别的属性,客户端也可以与我们想要与之通信的客户端的IService派生接口一起选择它。

CreateServiceRemotingListener最终会创建一个实例FabricTransportServiceRemotingListener,但在此实现中,我们无法设置我们自己的特定IServiceRemotingMessageHandler。如果您创建自己的FabricTransportServiceRemotingProviderAttribute子类并覆盖它,那么您实际上可以创建一个FabricTransportServiceRemotingListener的实例,它接受构造函数中的调度程序:

public class AuditableFabricTransportServiceRemotingProviderAttribute : 
    FabricTransportServiceRemotingProviderAttribute
{
    public override IServiceRemotingListener CreateServiceRemotingListener(
        ServiceContext serviceContext, IService serviceImplementation)
    {
            var messageHandler = new AuditableServiceRemotingDispatcher(
                serviceContext, serviceImplementation);

            return (IServiceRemotingListener)new FabricTransportServiceRemotingListener(
                serviceContext: serviceContext,
                messageHandler: messageHandler);
    }
}

AuditableServiceRemotingDispatcher是神奇发生的地方。它是我们自己的ServiceRemotingDispatcher子类。覆盖RequestResponseAsync(忽略HandleOneWay,服务远程处理不支持它,如果被调用则抛出NotImplementedException,如下所示:

public class AuditableServiceRemotingDispatcher : ServiceRemotingDispatcher
{
    public AuditableServiceRemotingDispatcher(ServiceContext serviceContext, IService service) : 
        base(serviceContext, service) { }

    public override async Task<byte[]> RequestResponseAsync(
        IServiceRemotingRequestContext requestContext, 
        ServiceRemotingMessageHeaders messageHeaders, 
        byte[] requestBodyBytes)
    {
        byte[] userHeader = null;
        if (messageHeaders.TryGetHeaderValue("user-header", out auditHeader))
        {
            // Deserialize from byte[] and handle the header
        }
        else
        {
            // Throw exception?
        }

        byte[] result = null;        
        result = await base.RequestResponseAsync(requestContext, messageHeaders, requestBodyBytes);
        return result;
    }
}

另一种更简单但不太灵活的方法是直接在服务中直接创建一个FabricTransportServiceRemotingListener实例,其中包含我们自定义调度程序的实例:

 protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
 {
     yield return new ServiceInstanceListener(context => 
         new FabricTransportServiceRemotingListener(this.Context, new AuditableServiceRemotingDispatcher(context, this)));
 }

为什么这不灵活?好吧,因为使用该属性也支持客户端,如下所示

客户端

好的,现在我们可以在接收邮件时阅读自定义标题,如何设置这些标题?让我们看一下该属性的另一种方法:

public override IServiceRemotingClientFactory CreateServiceRemotingClientFactory(IServiceRemotingCallbackClient callbackClient)
{
    return (IServiceRemotingClientFactory)new FabricTransportServiceRemotingClientFactory(
        callbackClient: callbackClient,
        servicePartitionResolver: (IServicePartitionResolver)null,
        traceId: (string)null);
}

这里我们不能只注入一个特定的处理程序或类似的服务,我们必须提供我们自己的自定义工厂。为了不必重新实现FabricTransportServiceRemotingClientFactory的详细信息,我只是将其封装在我自己的IServiceRemotingClientFactory实现中:

public class AuditedFabricTransportServiceRemotingClientFactory : IServiceRemotingClientFactory, ICommunicationClientFactory<IServiceRemotingClient>
{
    private readonly ICommunicationClientFactory<IServiceRemotingClient> _innerClientFactory;

    public AuditedFabricTransportServiceRemotingClientFactory(ICommunicationClientFactory<IServiceRemotingClient> innerClientFactory)
    {
        _innerClientFactory = innerClientFactory;
        _innerClientFactory.ClientConnected += OnClientConnected;
        _innerClientFactory.ClientDisconnected += OnClientDisconnected;
    }

    private void OnClientConnected(object sender, CommunicationClientEventArgs<IServiceRemotingClient> e)
    {
        EventHandler<CommunicationClientEventArgs<IServiceRemotingClient>> clientConnected = this.ClientConnected;
        if (clientConnected == null) return;
        clientConnected((object)this, new CommunicationClientEventArgs<IServiceRemotingClient>()
        {
            Client = e.Client
        });
    }

    private void OnClientDisconnected(object sender, CommunicationClientEventArgs<IServiceRemotingClient> e)
    {
        EventHandler<CommunicationClientEventArgs<IServiceRemotingClient>> clientDisconnected = this.ClientDisconnected;
        if (clientDisconnected == null) return;
        clientDisconnected((object)this, new CommunicationClientEventArgs<IServiceRemotingClient>()
        {
            Client = e.Client
        });
    }

    public async Task<IServiceRemotingClient> GetClientAsync(
        Uri serviceUri,
        ServicePartitionKey partitionKey, 
        TargetReplicaSelector targetReplicaSelector, 
        string listenerName,
        OperationRetrySettings retrySettings, 
        CancellationToken cancellationToken)
    {
        var client = await _innerClientFactory.GetClientAsync(
            serviceUri, 
            partitionKey, 
            targetReplicaSelector, 
            listenerName, 
            retrySettings, 
            cancellationToken);
        return new AuditedFabricTransportServiceRemotingClient(client);
    }

    public async Task<IServiceRemotingClient> GetClientAsync(
        ResolvedServicePartition previousRsp, 
        TargetReplicaSelector targetReplicaSelector, 
        string listenerName, 
        OperationRetrySettings retrySettings,
        CancellationToken cancellationToken)
    {
        var client = await _innerClientFactory.GetClientAsync(
            previousRsp, 
            targetReplicaSelector, 
            listenerName, 
            retrySettings, 
            cancellationToken);
        return new AuditedFabricTransportServiceRemotingClient(client);
    }

    public Task<OperationRetryControl> ReportOperationExceptionAsync(
        IServiceRemotingClient client, 
        ExceptionInformation exceptionInformation, 
        OperationRetrySettings retrySettings,
        CancellationToken cancellationToken)
    {
        return _innerClientFactory.ReportOperationExceptionAsync(
            client, 
            exceptionInformation, 
            retrySettings, 
            cancellationToken);
    }

    public event EventHandler<CommunicationClientEventArgs<IServiceRemotingClient>> ClientConnected;
    public event EventHandler<CommunicationClientEventArgs<IServiceRemotingClient>> ClientDisconnected;
}

这个实现简单地将任何繁重的工作转移到底层工厂,同时返回它自己的可审计客户端,类似地封装了IServiceRemotingClient

 public class AuditedFabricTransportServiceRemotingClient : IServiceRemotingClient, ICommunicationClient
{
    private readonly IServiceRemotingClient _innerClient;

    public AuditedFabricTransportServiceRemotingClient(IServiceRemotingClient innerClient)
    {
        _innerClient = innerClient;
    }

    ~AuditedFabricTransportServiceRemotingClient()
    {
        if (this._innerClient == null) return;
        var disposable = this._innerClient as IDisposable;
        disposable?.Dispose();
    }

    Task<byte[]> IServiceRemotingClient.RequestResponseAsync(ServiceRemotingMessageHeaders messageHeaders, byte[] requestBody)
    {            
        messageHeaders.SetUser(ServiceRequestContext.Current.User);
        messageHeaders.SetCorrelationId(ServiceRequestContext.Current.CorrelationId);
        return this._innerClient.RequestResponseAsync(messageHeaders, requestBody);
    }

    void IServiceRemotingClient.SendOneWay(ServiceRemotingMessageHeaders messageHeaders, byte[] requestBody)
    {
        messageHeaders.SetUser(ServiceRequestContext.Current.User);
        messageHeaders.SetCorrelationId(ServiceRequestContext.Current.CorrelationId);
        this._innerClient.SendOneWay(messageHeaders, requestBody);
    }

    public ResolvedServicePartition ResolvedServicePartition
    {
        get { return this._innerClient.ResolvedServicePartition; }
        set { this._innerClient.ResolvedServicePartition = value; }
    }

    public string ListenerName
    {
        get { return this._innerClient.ListenerName; }
        set { this._innerClient.ListenerName = value; }
    }
    public ResolvedServiceEndpoint Endpoint
    {
        get { return this._innerClient.Endpoint; }
        set { this._innerClient.Endpoint = value; }
    }
}

现在,在这里我们实际(并最终)设置我们想要传递给服务的审核名称。

调用链和服务请求上下文

最后一个难题,ServiceRequestContext,它是一个自定义类,允许我们处理服务请求调用的环境上下文。这是相关的,因为它为我们提供了一种简单的方法来传播上下文信息,例如用户或相关ID(或我们想要在客户端和服务之间传递的任何其他头信息),在一系列调用中。实施ServiceRequestContext看起来像:

public sealed class ServiceRequestContext
{
    private static readonly string ContextKey = Guid.NewGuid().ToString();

    public ServiceRequestContext(Guid correlationId, string user)
    {
        this.CorrelationId = correlationId;
        this.User = user;
    }

    public Guid CorrelationId { get; private set; }

    public string User { get; private set; }

    public static ServiceRequestContext Current
    {
        get { return (ServiceRequestContext)CallContext.LogicalGetData(ContextKey); }
        internal set
        {
            if (value == null)
            {
                CallContext.FreeNamedDataSlot(ContextKey);
            }
            else
            {
                CallContext.LogicalSetData(ContextKey, value);
            }
        }
    }

    public static Task RunInRequestContext(Func<Task> action, Guid correlationId, string user)
    {
        Task<Task> task = null;
        task = new Task<Task>(async () =>
        {
            Debug.Assert(ServiceRequestContext.Current == null);
            ServiceRequestContext.Current = new ServiceRequestContext(correlationId, user);
            try
            {
                await action();
            }
            finally
            {
                ServiceRequestContext.Current = null;
            }
        });
        task.Start();
        return task.Unwrap();
    }

    public static Task<TResult> RunInRequestContext<TResult>(Func<Task<TResult>> action, Guid correlationId, string user)
    {
        Task<Task<TResult>> task = null;
        task = new Task<Task<TResult>>(async () =>
        {
            Debug.Assert(ServiceRequestContext.Current == null);
            ServiceRequestContext.Current = new ServiceRequestContext(correlationId, user);
            try
            {
                return await action();
            }
            finally
            {
                ServiceRequestContext.Current = null;
            }
        });
        task.Start();
        return task.Unwrap<TResult>();
    }
}

最后一部分受到SO answer by Stephen Cleary的影响很大。它为我们提供了一种简单的方法来处理环境信息,这些信息可以通过呼叫,天气与任务同步或异步来处理。现在,通过这种方式,我们可以在服务端的Dispatcher中设置该信息:

    public override Task<byte[]> RequestResponseAsync(
        IServiceRemotingRequestContext requestContext, 
        ServiceRemotingMessageHeaders messageHeaders, 
        byte[] requestBody)
    {
        var user = messageHeaders.GetUser();
        var correlationId = messageHeaders.GetCorrelationId();

        return ServiceRequestContext.RunInRequestContext(async () => 
            await base.RequestResponseAsync(
                requestContext, 
                messageHeaders, 
                requestBody), 
            correlationId, user);
    }

GetUser()GetCorrelationId()只是获取和解包客户端设置的标头的辅助方法。

实现此功能意味着服务为任何aditional调用创建的任何新客户端也将设置sam标头,因此在场景ServiceA中 - &gt; ServiceB - &gt; ServiceC我们仍然会在从ServiceB到ServiceC的呼叫中设置相同的用户。

是什么?那容易吗?是的;)

从服务内部,例如Stateless OWIN web api,您首先捕获用户信息,您创建ServiceProxyFactory的实例并将该调用包装在ServiceRequestContext中:

var task = ServiceRequestContext.RunInRequestContext(async () =>
{
    var serviceA = ServiceProxyFactory.CreateServiceProxy<IServiceA>(new Uri($"{FabricRuntime.GetActivationContext().ApplicationName}/ServiceA"));
    await serviceA.DoStuffAsync(CancellationToken.None);
}, Guid.NewGuid(), user);

好的,总而言之 - 您可以挂钩服务远程处理以设置自己的标头。正如我们在上面看到的那样,需要做一些工作来获得适当的机制,主要是创建自己的底层基础结构的子类。好处是,一旦你有了这个,那么你有一个非常简单的方法来审核你的服务电话。