Форум программистов, компьютерный форум, киберфорум
insite2012
Войти
Регистрация
Восстановить пароль
Рейтинг: 5.00. Голосов: 2.

Реализация ролевого доступа в WCF

Запись от insite2012 размещена 16.09.2016 в 20:34

Не так давно, участвуя в одной теме, где у одного из пользователей появился вопрос по организации ролевого доступа при использовании WCF, у меня возникла довольно удачный вариант реализации оной, которую я и намерен предсатвить.
Немного справочной информации. WCF, при использовании безопасности на уровне сообщений и сертификатов для защиты, позволяет нам использовать имя пользователя и пароль. Это очень удобно, поскольку избавляет нас от необходимости при вызове каждого метода из клиента-прокси передавать данные пользователя для проверки их на стороне сервера.
Однако тут возникает одна небольшая проблема - что, если нам надо не только обеспечить безопасность с использованием учетных данных, но еще и разграничить всем пользователям доступ по ролям? Именно для этого и предназначено мое решение.
Оно основано на том, что WCF имеет много различных точек для подключения, которые я и использую в своем проекте.
Теперь непосредственно к решению. В нем используется в первую очередь т.н. Поведение операции, которое в WCF представлено интерфейсом IOperationBehavior. Реализовав это поведение, мы можем впоследствии влиять на сам код той операции, к которой оно относится. А для упрощения добавления данного поведения к определенной операции этот класс также наследуется от класса Attribute, что позволяет не добавлять это поведение в список поведений выбранной операции, а сделать это декларативно, просто пометив метод этим атрибутом. Перед открытием хоста службы исполняющая среда сделает это за нас. Ниже код данного атрибута-поведения.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Description;
 
namespace RoleManagerServer {
    /// <summary>
    /// Атрибут-поведение для проверки пользователя и его роли
    /// </summary>
    [AttributeUsage(AttributeTargets.Method, AllowMultiple=false)]
    public class RoleManagerAttribute : Attribute, IOperationBehavior {
        /// <summary>
        /// Роль пользователя
        /// </summary>
        public string Role { get; set; }
 
        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="role">Роль пользователя</param>
        public RoleManagerAttribute(string role) {
            Role = role;
        }
 
        /// <summary>
        /// Пустая реализация
        /// </summary>
        public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) {
            return;
        }
 
        /// <summary>
        /// Пустая реализация
        /// </summary>
        public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) {
            return;
        }
 
        /// <summary>
        /// Подключение необходимых компонентов к диспетчеру операции
        /// </summary>
        /// <param name="operationDescription">Описание операции</param>
        /// <param name="dispatchOperation">Диспетчер операции</param>
        public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation) {
            dispatchOperation.Invoker = new RoleManagerInvoker(dispatchOperation.Invoker, Role);
        }
 
        /// <summary>
        /// Пустая реализация
        /// </summary>
        public void Validate(OperationDescription operationDescription) {
            return;
        }
    }
}
Далее, в WCF имеется интерфейс IOperationInvoker, который позволяет нам выполнить какой-либо код перед вызовом и после вызова операции. Именно это его свойство и используется. Создается класс-наследник от IOperationInvoker, и в методе ApplyDispatchBehavior нашего класса-поведения-атрибута этот класс добавляется в список поведений определенной операции. Код данного класса ниже.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Description;
 
namespace RoleManagerServer {
    /// <summary>
    /// Класс для проверки данных пользователя и его роли
    /// </summary>
    public class RoleManagerInvoker : IOperationInvoker {
        /// <summary>
        /// IOperationInvoker
        /// </summary>
        IOperationInvoker baseInvoker;
        /// <summary>
        /// Роль пользователя
        /// </summary>
        string role;
 
        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="invoker">IOperationInvoker</param>
        /// <param name="role">Роль пользователя</param>
        public RoleManagerInvoker(IOperationInvoker invoker, string role) {
            baseInvoker = invoker;
            this.role = role;
        }
 
        /// <summary>
        /// Пустая реализация
        /// </summary>
        public object[] AllocateInputs() {
            return new object[] { };
        }
 
        /// <summary>
        /// Основной метод реализации
        /// </summary>
        /// <param name="instance">Instance</param>
        /// <param name="inputs">Входные параметры</param>
        /// <param name="outputs">Выходные параметры</param>
        /// <returns>Результат вызова</returns>
        public object Invoke(object instance, object[] inputs, out object[] outputs) {
            object result = null;
            try {
                DatabaseHelper.CheckUserData(this.role);
                result = baseInvoker.Invoke(instance, inputs, out outputs);
            }
            catch (Exception ex) {
                outputs = new object[] { };
                throw new FaultException(ex.Message);
            }
            return result;
        }
 
        /// <summary>
        /// Пустая реализация
        /// </summary>
        public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state) {
            throw new Exception("The operation invoker is not asynchronous.");
        }
 
        /// <summary>
        /// Пустая реализация
        /// </summary>
        public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result) {
            throw new Exception("The operation invoker is not asynchronous.");
        }
 
        /// <summary>
        /// Резултат операции (синхронно/асинхронно)
        /// </summary>
        public bool IsSynchronous {
            get { return true; }
        }
    }
}
В данном классе, а именно в его методе Invoke мы обращаемся (через статический класс DatabaseHelper) к нашей базе данных, где хранятся логины, имена пользователей и их роли, и проверяем данные по определенному пользователю. Код этого класса ниже.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SQLite;
using System.Configuration;
 
namespace RoleManagerServer {
    /// <summary>
    /// 
    /// </summary>
    public static class DatabaseHelper {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="role"></param>
        public static void CheckUserData(string role) {
            string connectStr = ConfigurationManager.ConnectionStrings["CONNECT_STR"].ConnectionString;
            string selectCmd = "SELECT role FROM Users WHERE name=@name AND password=@password";
            using (SQLiteConnection cnn = new SQLiteConnection(connectStr)) {
                cnn.Open();
                using (SQLiteCommand cmd = new SQLiteCommand(selectCmd, cnn)) {
                    SQLiteParameter param = new SQLiteParameter("@name", Settings.Name);
                    cmd.Parameters.Add(param);
 
                    param = new SQLiteParameter("@password", Settings.Password);
                    cmd.Parameters.Add(param);
                    using (SQLiteDataReader dr = cmd.ExecuteReader()) {
                        if (!dr.HasRows) {
                            throw new Exception("Wrong user credentials!");
                        }
                        dr.Read();
                        string userRole = (string)dr["role"];
                        if (userRole != role) {
                            throw new Exception("Access denied!");
                        }
                    }
                }
            }
        }
    }
}
Также у нас имеется класс-валидатор имени пользователя и пароля (он работает автоматически, без нашего участия, что обеспечивается средой WCF), но все, что в нем нужно - это просто сохранить данные пользователя, запросившего вызов операции, для их последующего использования. Ниже код этого класса-валидатора.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IdentityModel.Selectors;
 
namespace RoleManagerServer {
    /// <summary>
    /// Класс-валидатор учетных данных пользователя
    /// </summary>
    public class RoleValidatorHelper : UserNamePasswordValidator {
        /// <summary>
        /// Переопределенный метод
        /// </summary>
        /// <param name="userName">Имя пользователя</param>
        /// <param name="password">Пароль пользователя</param>
        public override void Validate(string userName, string password) {
            Settings.Name = userName;
            Settings.Password = password;
        }
    }
}
При вызове клиентом любого метода данные пользователя передаются в метод Validate() данного класса, где они просто сохраняются в статическом классе настроек - Settings().
Вот его код.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace RoleManagerServer {
    /// <summary>
    /// Класс хранения настроек приложения
    /// </summary>
    public static class Settings {
        /// <summary>
        /// Имя пользователя
        /// </summary>
        public static string Name { get; set; }
        /// <summary>
        /// Пароль пользователя
        /// </summary>
        public static string Password { get; set; }
    }
}
Ну и наконец, у нас есть контракт простой тестовой службы с тремя операциями - IService()
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
 
namespace RoleManagerServer {
    /// <summary>
    /// Контракт службы
    /// </summary>
    [ServiceContract]
    public interface IService {
        /// <summary>
        /// Контракт операции guest
        /// </summary>
        [OperationContract]
        void GuestOperation();
 
        /// <summary>
        /// Контракт операции user
        /// </summary>
        [OperationContract]
        void UserOperation();
 
        /// <summary>
        /// Контракт операции admin
        /// </summary>
        [OperationContract]
        void AdminOperation();
    }
}
И реализация данного контракта, класс с именем Service().
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace RoleManagerServer {
    /// <summary>
    /// Реализация контракта службы
    /// </summary>
    public class Service : IService {
        /// <summary>
        /// Реализация операции guest
        /// </summary>
        [RoleManager("guest")]
        public void GuestOperation() {
            Console.WriteLine("Calling guest operation!");
        }
 
        /// <summary>
        /// Реализация операции user
        /// </summary>
        [RoleManager("user")]
        public void UserOperation() {
            Console.WriteLine("Calling user operation!");
        }
 
        /// <summary>
        /// Реализация операции admin
        /// </summary>
        [RoleManager("admin")]
        public void AdminOperation() {
            Console.WriteLine("Calling admin operation!");
        }
    }
}
Как можно видеть, теперь трудности с проверкой на принадлежность к роли сводятся к минимуму - нам достаточно пометить определенный метод службы нашим атрибутом, передав ему в конструкторе имя роли, и перед его выполнением пользователь, запросивший выполнение данного метода, будет проверен на принадлежность к данной роли. Если его роль не соответствует, будет выброшено исключение и выполнить метод ему на удастся.
В завершение приведу код создания и запуска хоста службы и файл конфигурации.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
 
namespace RoleManagerServer {
    /// <summary>
    /// Класс с точкой входа приложения
    /// </summary>
    class Program {
        static void Main(string[] args) {
            using (ServiceHost host = new ServiceHost(typeof(Service))) {
                host.Opening += new EventHandler(host_Opening);
                host.Opened += new EventHandler(host_Opened);
                host.Open();
                Console.WriteLine("Press <ENTER> to close...");
                Console.ReadLine();
            }
        }
 
        static void host_Opened(object sender, EventArgs e) {
            Console.WriteLine("Service is ready...");
        }
 
        static void host_Opening(object sender, EventArgs e) {
            Console.WriteLine("Opening service...");
        }
    }
}
XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add  name="CONNECT_STR" connectionString="Data Source=userbase.db"/>
  </connectionStrings>
  <system.serviceModel>
    <services>
      <service behaviorConfiguration="mexBehavior" name="RoleManagerServer.Service">
        <endpoint address="" binding="wsHttpBinding" bindingConfiguration="wsBindingConfiguration"
          contract="RoleManagerServer.IService" />
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8090/RoleManagerService" />
          </baseAddresses>
        </host>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="mexBehavior">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceCredentials>
            <clientCertificate>
              <authentication certificateValidationMode="None"/>
            </clientCertificate>
            <userNameAuthentication userNamePasswordValidationMode="Custom"
                                    customUserNamePasswordValidatorType="RoleManagerSample.RoleValidatorHelper, RoleManagerSample"/>
            <serviceCertificate storeLocation="CurrentUser"
                                x509FindType="FindBySubjectName"
                                findValue="TempCert"
                                storeName="My"/>
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <bindings>
      <wsHttpBinding>
        <binding name="wsBindingConfiguration">
          <security mode="Message">
            <message clientCredentialType="UserName"/>
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>
Теперь о клиенте. Используется самый простой способ - добавление ссылки на службу, с небольшой корректировкой файла конфигурации (это обусловлено применением сертификата).
Вот код и файл конфигурации клиента.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using RoleManagerClient.ServiceReference1;
 
namespace RoleManagerClient {
    class Program {
        static void Main(string[] args) {
            using (ServiceClient client = new ServiceClient()) {
                client.ClientCredentials.UserName.UserName = "Nick";
                client.ClientCredentials.UserName.Password = "333";
                try {
                    client.GuestOperation();
                    //client.UserOperation();
                    client.AdminOperation();
                }
                catch (Exception ex) {
                    Console.WriteLine(ex.Message);
                }
                Console.ReadLine();
            }
        }
    }
}
XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
            <wsHttpBinding>
                <binding name="WSHttpBinding_IService">
                    <security mode="Message">
                        <message clientCredentialType="UserName"/>
                    </security>
                </binding>
            </wsHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost:8090/RoleManagerService"
                      binding="wsHttpBinding" 
                      bindingConfiguration="WSHttpBinding_IService"
                      contract="ServiceReference1.IService" 
                      name="WSHttpBinding_IService" 
                      behaviorConfiguration="endpointConfiguration">
                <identity>
                    <certificate encodedValue="AwAAAAEAAAAUAAAAUNv9NiwA8f4Zo0aMmztzJ+/e7j8gAAAAAQAAALQBAAAwggGwMIIBXqADAgECAhBHPR8qklLig0PPZhPgoGykMAkGBSsOAwIdBQAwFjEUMBIGA1UEAxMLUm9vdCBBZ2VuY3kwHhcNMTYwOTE1MDM1NDQ1WhcNMzkxMjMxMjM1OTU5WjATMREwDwYDVQQDEwhUZW1wQ2VydDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxNmpLQtA5PQYXxHqzvtUv68a/RYpB3Cz7t+Ve+jzTwjYwdtzKFtujRpiIzuN3iXoz1Lezg/RgJHkkVlNAtpnBdgFdAX6NtuQ+2LaFKIqGAkyfvmHq/PFPrp0d92dnA+Dmb7ILjhEDNLXc3S2AEGl0Pe7MSo/B49ugLNsmVGU4H8CAwEAAaNLMEkwRwYDVR0BBEAwPoAQEuQJLQYdHU8AjWEh3BZkY6EYMBYxFDASBgNVBAMTC1Jvb3QgQWdlbmN5ghAGN2wAqgBkihHPuNSqXDX0MAkGBSsOAwIdBQADQQBcDIupdzAWMbz8HORXSMKZ8l5iA3EuhoRZReXl5G40O761fa7LdEjK2/AQqxCCaoek/kJkGUyknNmb37yMIFSk" />
                  <dns value="TempCert"/>
                </identity>
            </endpoint>
        </client>
      <behaviors>
        <endpointBehaviors>
          <behavior name="endpointConfiguration">
            <clientCredentials>
              <serviceCertificate>
                <authentication certificateValidationMode="None"/>
              </serviceCertificate>
              <clientCertificate storeLocation="CurrentUser" 
                                 x509FindType="FindBySubjectName" 
                                 findValue="TempCert" 
                                 storeName="My"/>
            </clientCredentials>
          </behavior>
        </endpointBehaviors>
      </behaviors>
    </system.serviceModel>
</configuration>
Вот, в принципе, и все. В прикрепленном архиве решение с обоими проектами - и сервер, и клиент, а также файл с небольшими пояснениями. От себя добавлю, что использование данного способа можно расширить на много случаев, как например - глобальный серверный перехватчик исключений с логгированием.
Если будут вопросы, комментарии по коду, предложения по улучшению - буду рад выслушать.
Вложения
Тип файла: rar RoleManagerSample.rar (624.6 Кб, 316 просмотров)
Размещено в Без категории
Показов 3735 Комментарии 12
Всего комментариев 12
Комментарии
  1. Старый комментарий

    С клиента на сервер приходя null значения

    Здравствуйте, такой вопрос, я взял за основу ваш код переделал его чтобы все передавалось по защищенному соединению, но когда я с клиента на сервер отсылаю логин и пароль для авторизации почему-то приходят null значения. как это можно исправить?
    Запись от djserg125 размещена 19.10.2017 в 02:16 djserg125 вне форума
  2. Старый комментарий
    Аватар для insite2012
    Не видя ваш код, я вряд ли смогу что-то подсказать. Пришлите мне свой проект, или создайте тему в соответствующем разделе, где и покажете ваш код. Думаю, проблема будет найдена.
    Запись от insite2012 размещена 19.10.2017 в 07:09 insite2012 вне форума
  3. Старый комментарий

    проблема решена

    проблему решил, была не правильная конфигурация сервиса.
    Запись от djserg125 размещена 23.10.2017 в 18:27 djserg125 вне форума
  4. Старый комментарий

    возвращения роли на клиент

    Извините, не могли бы вы подсказать как модифицировать вашу программу так, чтобы
    сначала клиент авторизовался на сервере, потом роль вернулась на клиент, и на клиенте уже можно было определить какую форму клиенту открывать (админа, гостя или юзера)?
    Запись от djserg125 размещена 24.10.2017 в 23:16 djserg125 вне форума
  5. Старый комментарий
    Аватар для insite2012

    возвращения роли на клиент

    Цитата:
    Сообщение от djserg125 Просмотреть комментарий
    Извините, не могли бы вы подсказать как модифицировать вашу программу так, чтобы
    сначала клиент авторизовался на сервере, потом роль вернулась на клиент, и на клиенте уже можно было определить какую форму клиенту открывать (админа, гостя или юзера)?
    Ну тут все очевидно. Определить перечисление с членами: Admin, User, Guest, и метод, который будет возвращать определенный вариант для имени пользователя и пароля.
    Запись от insite2012 размещена 25.10.2017 в 15:41 insite2012 вне форума
  6. Старый комментарий
    А можно пример?
    Запись от djserg125 размещена 25.10.2017 в 16:29 djserg125 вне форума
  7. Старый комментарий
    не могу понять где использовать этот метод (который будет возвращать определенный вариант для имени пользователя и пароля.)?
    Запись от djserg125 размещена 28.10.2017 в 14:18 djserg125 вне форума
  8. Старый комментарий

    Уничтожение предыдущей сессии

    Здравствуйте, не могли бы вы подсказать как уничтожишь сессию если при аунтетификации логин и пароль не подошёл, а что сейчас получается если я не правильно ввёл логин и пароль сервер говорит что аутентификациям не удачная, а когда потом вводишь правильные логин и пароль сервер использует почему-то старые логины и пароли(
    Запись от djserg125 размещена 10.11.2017 в 22:25 djserg125 вне форума
  9. Старый комментарий
    Аватар для insite2012
    Как один из вариантов-использовать демаркацию методов, и обрабатывать исключения. В обработчике исключений принудительно закрывать сессию.
    Но будет лучше, если вы покажете ваш код.
    Запись от insite2012 размещена 10.11.2017 в 22:54 insite2012 вне форума
  10. Старый комментарий
    Цитата:
    Сообщение от insite2012 Просмотреть комментарий
    Как один из вариантов-использовать демаркацию методов, и обрабатывать исключения. В обработчике исключений принудительно закрывать сессию.
    Но будет лучше, если вы покажете ваш код.
    А куда можно выложить код?
    Запись от djserg125 размещена 10.11.2017 в 23:00 djserg125 вне форума
  11. Старый комментарий
    Цитата:
    Сообщение от insite2012 Просмотреть комментарий
    Как один из вариантов-использовать демаркацию методов, и обрабатывать исключения. В обработчике исключений принудительно закрывать сессию.
    Но будет лучше, если вы покажете ваш код.
    Извините что с таким большим опозданием вот создал тему https://www.cyberforum.ru/web-... st11933204 спустя месяц так и не получилось решить проблему(
    Запись от djserg125 размещена 11.12.2017 в 03:57 djserg125 вне форума
  12. Старый комментарий
    Круто. А я блин вожусь с enum и записью в БД потом. А тут роли можно проще реализовать.
    Очень познавательно. Спасибо
    Запись от anomal6 размещена 25.02.2021 в 08:08 anomal6 вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2024, CyberForum.ru