当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《在对ServiceFabric传输中的可靠服务的调用中传递用户和审核信息》经验,为你挑选了1个好方法。

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

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



1> yoape..:

There is actually a concept of headers that are passed between client and service if you are using fabric transport for remoting. If you are using Http transport then you have headers there just as you would with any http request.

Note, below proposal is not the easiest solution, but it solves the issue once it is in place and it is easy to use then, but if you are looking for easy in the overall code base this might not be the way to go. If that is the case then I suggest you simply add some common audit info parameter to all your service methods. The big caveat there is of course when some developer forgets to add it or it is not set properly when calling down stream services. It's all about trade-offs, as alway in code :).

Down the rabbit hole

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

棘手的部分是挂钩进入该交换并实际设置然后阅读其他标题.请在这里忍受我,这是我们需要了解的窗帘背后的一些课程.

服务方面

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

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

(另一种方法是实现自己的监听器,但这不是我们在这里不想做的事情,我们只是不想在现有基础设施之上添加东西.请参阅下面的方法.)

This is where we can provide our own listener instead, similar to what that extention method does behind the curtains. Let's first look at what that extention method does. It goes looking for a specific attribute on assembly level on your service project: ServiceRemotingProviderAttribute. That one is abstract, but the one that you can use, and which you will get a default instance of, if none is provided, is FabricTransportServiceRemotingProviderAttribute. Set it in AssemblyInfo.cs (or any other file, it's an assembly attribute):

[assembly: FabricTransportServiceRemotingProvider()]

This attribute has two interesting overridable methods:

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,服务远程处理不支持,它会抛出一个NotImplementedExceptionif调用),如下所示:

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

    public override async Task 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 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
{
    private readonly ICommunicationClientFactory _innerClientFactory;

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

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

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

    public async Task 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 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 ReportOperationExceptionAsync(
        IServiceRemotingClient client, 
        ExceptionInformation exceptionInformation, 
        OperationRetrySettings retrySettings,
        CancellationToken cancellationToken)
    {
        return _innerClientFactory.ReportOperationExceptionAsync(
            client, 
            exceptionInformation, 
            retrySettings, 
            cancellationToken);
    }

    public event EventHandler> ClientConnected;
    public event EventHandler> 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 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; }
    }
}

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

调用链和服务请求上下文

One final piece of the puzzle, the ServiceRequestContext, which is a custom class that allows us to handle an ambient context for a service request call. This is relevant because it gives us an easy way to propagate that context information, like the user or a correlation id (or any other header information we want to pass between client and service), in a chain of calls. The implementation ServiceRequestContext looks like:

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 action, Guid correlationId, string user)
    {
        Task task = null;
        task = new 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 RunInRequestContext(Func> action, Guid correlationId, string user)
    {
        Task> task = null;
        task = new Task>(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();
    }
}

This last part was much influenced by the SO answer by Stephen Cleary. It gives us an easy way to handle the ambient information down a hierarcy of calls, weather they are synchronous or asyncronous over Tasks. Now, with this we have a way of setting that information also in the Dispatcher on the service side:

    public override Task 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() and GetCorrelationId() are just helper methods that gets and unpacks the headers set by the client)

Having this in place means that any new client created by the service for any aditional call will also have the sam headers set, so in the scenario ServiceA -> ServiceB -> ServiceC we will still have the same user set in the call from ServiceB to ServiceC.

what? that easy? yes ;)

From inside a service, for instance a Stateless OWIN web api, where you first capture the user information, you create an instance of ServiceProxyFactoryand wrap that call in a ServiceRequestContext:

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

Ok, so to sum it up - you can hook into the service remoting to set your own headers. As we see above there is some work that needs to be done to get a mechanism for that in place, mainly creating your own subclasses of the underlying infrastructure. The upside is that once you have this in place, then you have a very easy way for auditing your service calls.

推荐阅读
我我檬檬我我186
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有