Не так давно, участвуя в одной теме, где у одного из пользователей появился вопрос по организации ролевого доступа при использовании 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> |
|
Вот, в принципе, и все. В прикрепленном архиве решение с обоими проектами - и сервер, и клиент, а также файл с небольшими пояснениями. От себя добавлю, что использование данного способа можно расширить на много случаев, как например - глобальный серверный перехватчик исключений с логгированием.
Если будут вопросы, комментарии по коду, предложения по улучшению - буду рад выслушать. |