Неправильный поток.CurrentPrincipal в асинхронном конечном методе WCF

StackOverflow https://stackoverflow.com//questions/21008509

Вопрос

У меня есть служба WCF, которая имеет свой Thread.CurrentPrincipal установленный в ServiceConfiguration.ClaimsAuthorizationManager.

Когда я реализую службу асинхронно, как это:

    public IAsyncResult BeginMethod1(AsyncCallback callback, object state)
    {
        // Audit log call (uses Thread.CurrentPrincipal)

        var task = Task<int>.Factory.StartNew(this.WorkerFunction, state);

        return task.ContinueWith(res => callback(task));
    }

    public string EndMethod1(IAsyncResult ar)
    {
        // Audit log result (uses Thread.CurrentPrincipal)

        return ar.AsyncState as string;
    }

    private int WorkerFunction(object state)
    {
        // perform work
    }

Я нахожу, что поток.CurrentPrincipal установлен на правильный ClaimsPrincipal в Begin-методе, а также в WorkerFunction, но в End-методе он установлен на GenericPrincipal.

Я знаю, что могу включить ASP.NET совместимость для сервиса и использования HttpContext.Current.User который имеет правильный принцип действия во всех методах, но я бы предпочел этого не делать.

Есть ли способ принудительно изменить поток.CurrentPrincipal на правильный ClaimsPrincipal без включения ASP.NET совместимости?

Это было полезно?

Решение

Начиная с краткое описание точек расширения WCF, вы увидите тот, который специально разработан для решения вашей проблемы.Это называется Инициализатор CallContextInitializer.Взгляните на это статья, в которой приводится пример кода CallContextInitializer.

Если вы создадите расширение ICallContextInitializer, вам будет предоставлен контроль над обоими контекстами потока BeginXXX и контекст потока EndXXX.Вы говорите, что ClaimsAuthorizationManager правильно установил пользователя-участника в вашем методе BeginXXX(...).В этом случае вы затем создаете для себя пользовательский ICallContextInitializer, который либо назначает, либо записывает CurrentPrincipal, в зависимости от того, обрабатывает ли он ваш BeginXXX() или ваш EndXXX().Что-то вроде:

public object BeforeInvoke(System.ServiceModel.InstanceContext instanceContext, System.ServiceModel.IClientChannel channel, System.ServiceModel.Channels.Message request){
    object principal = null;
    if (request.Properties.TryGetValue("userPrincipal", out principal))
    {
        //If we got here, it means we're about to call the EndXXX(...) method.
        Thread.CurrentPrincipal = (IPrincipal)principal;
    }
    else
    {
        //If we got here, it means we're about to call the BeginXXX(...) method.
        request.Properties["userPrincipal"] = Thread.CurrentPrincipal;            
    }
    ...
 }

Чтобы внести дальнейшую ясность, рассмотрим два случая.Предположим, вы реализовали как ICallContextInitializer, так и IParameterInspector.Предположим, что ожидается, что эти перехватчики будут выполняться с синхронной службой WCF и с асинхронной службой WCF (что является вашим особым случаем).

Ниже приведена последовательность событий и объяснение того, что происходит:

Синхронный случай

ICallContextInitializer.BeforeInvoke();
IParemeterInspector.BeforeCall();
//...service executes...
IParameterInspector.AfterCall();
ICallContextInitializer.AfterInvoke();

Ничего удивительного в приведенном выше коде нет.Но теперь посмотрите ниже на то, что происходит с асинхронными сервисными операциями...

Асинхронный случай

ICallContextInitializer.BeforeInvoke();  //TryGetValue() fails, so this records the UserPrincipal.
IParameterInspector.BeforeCall();
//...Your BeginXXX() routine now executes...
ICallContextInitializer.AfterInvoke();

//...Now your Task async code executes (or finishes executing)...

ICallContextInitializercut.BeforeInvoke();  //TryGetValue succeeds, so this assigns the UserPrincipal.
//...Your EndXXX() routine now executes...
IParameterInspector.AfterCall();
ICallContextInitializer.AfterInvoke();

Как вы можете видеть, CallContextInitializer гарантирует, что у вас есть возможность инициализировать значения, такие как ваш CurrentPrincipal, непосредственно перед запуском процедуры EndXXX().Поэтому не имеет значения, что процедура EndXXX(), несомненно, выполняется в другом потоке, чем процедура BeginXXX().И да, тот System.ServiceModel.Channels.Message объект, который хранит ваш пользовательский принципал между методами Begin/End, сохраняется и должным образом передается WCF, даже если поток изменился.

В целом, этот подход позволяет вашему EndXXX(IAsyncResult) выполняться с правильным IPrincipal без необходимости явно переустанавливать CurrentPrincipal в подпрограмме EndXXX().И, как и в случае с любым поведением WCF, вы можете решить, применяется ли это к отдельным операциям, ко всем операциям в контракте или ко всем операциям в конечной точке.

Другие советы

На самом деле это не ответ на мой вопрос, а альтернативный подход к реализации службы WCF (в .NET 4.5), который не проявляет тех же проблем с Thread.CurrentPrincipal.

    public async Task<string> Method1()
    {
        // Audit log call (uses Thread.CurrentPrincipal)

        try
        {
            return await Task.Factory.StartNew(() => this.WorkerFunction());
        }
        finally 
        {
            // Audit log result (uses Thread.CurrentPrincipal)
        }
    }

    private string WorkerFunction()
    {
        // perform work
        return string.Empty;
    }

Правильным подходом к этому является создание расширения:

public class SLOperationContext : IExtension<OperationContext>
{
    private readonly IDictionary<string, object> items;

    private static ReaderWriterLockSlim _instanceLock = new ReaderWriterLockSlim();

    private SLOperationContext()
    {
        items = new Dictionary<string, object>();
    }

    public IDictionary<string, object> Items
    {
        get { return items; }
    }

    public static SLOperationContext Current
    {
        get
        {
            SLOperationContext context = OperationContext.Current.Extensions.Find<SLOperationContext>();
            if (context == null)
            {
                _instanceLock.EnterWriteLock();
                context = new SLOperationContext();
                OperationContext.Current.Extensions.Add(context);
                _instanceLock.ExitWriteLock();
            }
            return context;
        }
    }

    public void Attach(OperationContext owner) { }
    public void Detach(OperationContext owner) { }
}

Теперь это расширение используется в качестве контейнера для объектов, которые вы хотите сохранить между переключениями потоков, поскольку OperationContext.Current останется прежним.

Теперь вы можете использовать это в BeginMethod1 для сохранения текущего пользователя:

SLOperationContext.Current.Items["Principal"] = OperationContext.Current.ClaimsPrincipal;

И затем в EndMethod1 вы можете получить пользователя, набрав:

ClaimsPrincipal principal = SLOperationContext.Current.Items["Principal"];

РЕДАКТИРОВАТЬ (другой подход):

public IAsyncResult BeginMethod1(AsyncCallback callback, object state)
{
    var task = Task.Factory.StartNew(this.WorkerFunction, state);

    var ec = ExecutionContext.Capture();

    return task.ContinueWith(res =>
        ExecutionContext.Run(ec, (_) => callback(task), null));
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top