在这篇文章里,讲述了如何使用用户名和密码连接OPC UA Server,例子中添加了2个用户,权限都是一样的。本文讲述如何给不同用户添加指定权限。
一 Client代码
client端代码如下,和之前的文章中一样,
#include <stdlib.h>
#include "open62541.h"
int main(void)
{
UA_Client *client = UA_Client_new();
UA_ClientConfig_setDefault(UA_Client_getConfig(client));
UA_StatusCode retval = UA_Client_connect_username(client, "opc.tcp://localhost:4840", "paula", "paula123");
if (retval != UA_STATUSCODE_GOOD)
{
UA_Client_delete(client);
return EXIT_FAILURE;
}
UA_NodeId newVariableIdRequest = UA_NODEID_NUMERIC(1, 1001);
UA_NodeId newVariableId = UA_NODEID_NULL;
UA_VariableAttributes newVariableAttributes = UA_VariableAttributes_default;
newVariableAttributes.accessLevel = UA_ACCESSLEVELMASK_READ;
newVariableAttributes.description = UA_LOCALIZEDTEXT_ALLOC("en-US", "NewVariable desc");
newVariableAttributes.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", "NewVariable");
newVariableAttributes.dataType = UA_TYPES[UA_TYPES_UINT32].typeId;
UA_UInt32 value = 50;
UA_Variant_setScalarCopy(&newVariableAttributes.value, &value, &UA_TYPES[UA_TYPES_UINT32]);
UA_StatusCode retCode;
retCode = UA_Client_addVariableNode(client, newVariableIdRequest,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(1, "newVariable"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
newVariableAttributes, &newVariableId);
printf("addVariable returned: %s\n", UA_StatusCode_name(retCode));
UA_ExpandedNodeId extNodeId = UA_EXPANDEDNODEID_NUMERIC(0, 0);
extNodeId.nodeId = newVariableId;
retCode = UA_Client_addReference(client, UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), UA_TRUE,
UA_STRING_NULL, extNodeId, UA_NODECLASS_VARIABLE);
printf("addReference returned: %s\n", UA_StatusCode_name(retCode));
retCode = UA_Client_deleteReference(client, UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), UA_TRUE, extNodeId,
UA_TRUE);
printf("deleteReference returned: %s\n", UA_StatusCode_name(retCode));
retCode = UA_Client_deleteNode(client, newVariableId, UA_TRUE);
printf("deleteNode returned: %s\n", UA_StatusCode_name(retCode));
/* Clean up */
UA_VariableAttributes_clear(&newVariableAttributes);
UA_Client_delete(client); /* Disconnects the client internally */
return EXIT_SUCCESS;
}
代码功能是使用用户名和密码连接Server,然后测试是否可以添加Node,添加Reference,删除Reference以及删除Node。用户名:paula,密码:paula123
二 Server指定权限原理
Server代码是基于之前文章中的代码,进行修改。
首先定义一个结构体,表示用户权限,
typedef struct {
UA_String userName;
UA_Boolean fAllowAddNode; // 允许添加节点
UA_Boolean fAllowAddReference; // 允许添加Reference
UA_Boolean fAllowDeleteNode; // 允许删除节点
UA_Boolean fAllowDeleteReference; // 允许删除Reference
} UserRight_t;
然后使用这个结构体创建一个数组来表示用户peter和paula的权限,
static UserRight_t userRight[2] = {
{
UA_STRING_STATIC("peter"), UA_TRUE, UA_TRUE, UA_TRUE, UA_TRUE },
{
UA_STRING_STATIC("paula"), UA_TRUE, UA_TRUE, UA_FALSE, UA_FALSE }
};
在初始化Server的access control时,我们调用了UA_AccessControl_default(),这个会设置默认函数,具体可以自行查阅源码。
需要使用我们自定义的函数来替代其中一些默认函数,第一个是activateSession,其默认函数是activateSession_default(),
自定义activateSession函数如下,
static UA_StatusCode
activateSession(UA_Server *server, UA_AccessControl *ac,
const UA_EndpointDescription *endpointDescription,
const UA_ByteString *secureChannelRemoteCertificate,
const UA_NodeId *sessionId,
const UA_ExtensionObject *userIdentityToken,
void **sessionContext)
{
AccessControlContext * context = (AccessControlContext*)ac->context;
/* The empty token is interpreted as anonymous */
if (userIdentityToken->encoding == UA_EXTENSIONOBJECT_ENCODED_NOBODY) {
if (!context->allowAnonymous)
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
/* No userdata atm */
*sessionContext = NULL;
return UA_STATUSCODE_GOOD;
}
/* Could the token be decoded? */
if (userIdentityToken->encoding < UA_EXTENSIONOBJECT_DECODED)
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
/* Anonymous login */
if (userIdentityToken->content.decoded.type == &UA_TYPES[UA_TYPES_ANONYMOUSIDENTITYTOKEN]) {
if (!context->allowAnonymous)
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
const UA_AnonymousIdentityToken *token = (UA_AnonymousIdentityToken*)
userIdentityToken->content.decoded.data;
/* Compatibility notice: Siemens OPC Scout v10 provides an empty
* policyId. This is not compliant. For compatibility, assume that empty
* policyId == ANONYMOUS_POLICY */
if (token->policyId.data && !UA_String_equal(&token->policyId, &anonymous_policy))
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
/* No userdata atm */
*sessionContext = NULL;
return UA_STATUSCODE_GOOD;
}
/* Username and password */
if (userIdentityToken->content.decoded.type == &UA_TYPES[UA_TYPES_USERNAMEIDENTITYTOKEN]) {
const UA_UserNameIdentityToken *userToken =
(UA_UserNameIdentityToken*)userIdentityToken->content.decoded.data;
if (!UA_String_equal(&userToken->policyId, &username_policy))
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
/* The userToken has been decrypted by the server before forwarding
* it to the plugin. This information can be used here. */
/* if(userToken->encryptionAlgorithm.length > 0) {} */
/* Empty username and password */
if (userToken->userName.length == 0 && userToken->password.length == 0)
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
/* Try to match username/pw */
UA_Boolean match = false;
for (size_t i = 0; i < context->usernamePasswordLoginSize; i++) {
if (UA_String_equal(&userToken->userName, &context->usernamePasswordLogin[i].username) &&
UA_String_equal(&userToken->password, &context->usernamePasswordLogin[i].password)) {
match = true;
break;
}
}
if (!match)
return UA_STATUSCODE_BADUSERACCESSDENIED;
/* 关键代码 */
UA_UsernamePasswordLogin* currentUsernamePasswordLogin =
(UA_UsernamePasswordLogin*)UA_malloc(sizeof(UA_UsernamePasswordLogin));
UA_String_copy(&(userToken->userName), &(currentUsernamePasswordLogin->username));
UA_String_copy(&(userToken->password), &(currentUsernamePasswordLogin->password));
*sessionContext = currentUsernamePasswordLogin;
return UA_STATUSCODE_GOOD;
}
/* Unsupported token type */
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
}
代码内容和activateSession_default()基本是一样的,只是在最后把访问的用户名和密码存到动态分配的内存里,然后把该内存的地址传给sessionContext ,这样在后续的节点管理函数里就可以知道是哪个用户在调用,这个非常重要!
然后就是自定义添加Node,添加Reference,删除Reference以及删除Node的函数,如下,
const UA_UInt32 UserCount = sizeof(userRight) / sizeof(UserRight_t);
static UA_Boolean
allowAddNode(UA_Server *server, UA_AccessControl *ac,
const UA_NodeId *sessionId, void *sessionContext,
const UA_AddNodesItem *item)
{
printf("Called allowAddNode\n");
UA_UsernamePasswordLogin* currentUsernamePasswordLogin =
(UA_UsernamePasswordLogin*)sessionContext;
for (UA_UInt32 i = 0; i < UserCount; ++i)
{
if (UA_String_equal(&(currentUsernamePasswordLogin->username), &(userRight[i].userName)))
{
return userRight[i].fAllowAddNode;
}
}
return UA_TRUE;
}
static UA_Boolean
allowAddReference(UA_Server *server, UA_AccessControl *ac,
const UA_NodeId *sessionId, void *sessionContext,
const UA_AddReferencesItem *item)
{
printf("Called allowAddReference\n");
UA_UsernamePasswordLogin* currentUsernamePasswordLogin =
(UA_UsernamePasswordLogin*)sessionContext;
for (UA_UInt32 i = 0; i < UserCount; ++i)
{
if (UA_String_equal(&(currentUsernamePasswordLogin->username), &(userRight[i].userName)))
{
return userRight[i].fAllowAddReference;
}
}
return UA_TRUE;
}
static UA_Boolean
allowDeleteNode(UA_Server *server, UA_AccessControl *ac,
const UA_NodeId *sessionId, void *sessionContext,
const UA_DeleteNodesItem *item)
{
printf("Called allowDeleteNode\n");
UA_UsernamePasswordLogin* currentUsernamePasswordLogin =
(UA_UsernamePasswordLogin*)sessionContext;
for (UA_UInt32 i = 0; i < UserCount; ++i)
{
if (UA_String_equal(&(currentUsernamePasswordLogin->username), &(userRight[i].userName)))
{
return userRight[i].fAllowDeleteNode;
}
}
return UA_TRUE;
}
static UA_Boolean
allowDeleteReference(UA_Server *server, UA_AccessControl *ac,
const UA_NodeId *sessionId, void *sessionContext,
const UA_DeleteReferencesItem *item)
{
printf("Called allowDeleteReference\n");
UA_UsernamePasswordLogin* currentUsernamePasswordLogin =
(UA_UsernamePasswordLogin*)sessionContext;
for (UA_UInt32 i = 0; i < UserCount; ++i)
{
if (UA_String_equal(&(currentUsernamePasswordLogin->username), &(userRight[i].userName)))
{
return userRight[i].fAllowDeleteReference;
}
}
return UA_TRUE;
}
这4个函数用于判断用户是否拥有对应的节点管理权限,其内容也很简单,就是从sessionContext里提取用户名,然后查找用户名,最后返回该用户名的权限。
最后要做的就是把这些自定义函数覆盖掉原先的默认函数,
// Set accessControl functions for activae session
config->accessControl.activateSession = activateSession;
/* Set accessControl functions for nodeManagement */
config->accessControl.allowAddNode = allowAddNode;
config->accessControl.allowAddReference = allowAddReference;
config->accessControl.allowDeleteNode = allowDeleteNode;
config->accessControl.allowDeleteReference = allowDeleteReference;
整体源码在第四节
三 测试
测试很简单,先编译Server和Client,然后先运行Server再运行Client,由于Client使用的用户是paula,而paula在Server这边没有删除Node和删除Reference的权限,所以会提示没权限,如下,
和设置的权限表现一致。
四 Server整体源码
整体源码如下,
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include "open62541.h"
typedef struct {
UA_Boolean allowAnonymous;
size_t usernamePasswordLoginSize;
UA_UsernamePasswordLogin *usernamePasswordLogin;
} AccessControlContext;
typedef struct {
UA_String userName;
UA_Boolean fAllowAddNode;
UA_Boolean fAllowAddReference;
UA_Boolean fAllowDeleteNode;
UA_Boolean fAllowDeleteReference;
} UserRight_t;
#define ANONYMOUS_POLICY "open62541-anonymous-policy"
#define USERNAME_POLICY "open62541-username-policy"
const UA_String anonymous_policy = UA_STRING_STATIC(ANONYMOUS_POLICY);
const UA_String username_policy = UA_STRING_STATIC(USERNAME_POLICY);
static UA_UsernamePasswordLogin logins[2] = {
{
UA_STRING_STATIC("peter"), UA_STRING_STATIC("peter123") },
{
UA_STRING_STATIC("paula"), UA_STRING_STATIC("paula123") }
};
static UserRight_t userRight[2] = {
{
UA_STRING_STATIC("peter"), UA_TRUE, UA_TRUE, UA_TRUE, UA_TRUE },
{
UA_STRING_STATIC("paula"), UA_TRUE, UA_TRUE, UA_FALSE, UA_FALSE }
};
const UA_UInt32 UserCount = sizeof(userRight) / sizeof(UserRight_t);
static UA_Boolean
allowAddNode(UA_Server *server, UA_AccessControl *ac,
const UA_NodeId *sessionId, void *sessionContext,
const UA_AddNodesItem *item)
{
printf("Called allowAddNode\n");
UA_UsernamePasswordLogin* currentUsernamePasswordLogin =
(UA_UsernamePasswordLogin*)sessionContext;
for (UA_UInt32 i = 0; i < UserCount; ++i)
{
if (UA_String_equal(&(currentUsernamePasswordLogin->username), &(userRight[i].userName)))
{
return userRight[i].fAllowAddNode;
}
}
return UA_TRUE;
}
static UA_Boolean
allowAddReference(UA_Server *server, UA_AccessControl *ac,
const UA_NodeId *sessionId, void *sessionContext,
const UA_AddReferencesItem *item)
{
printf("Called allowAddReference\n");
UA_UsernamePasswordLogin* currentUsernamePasswordLogin =
(UA_UsernamePasswordLogin*)sessionContext;
for (UA_UInt32 i = 0; i < UserCount; ++i)
{
if (UA_String_equal(&(currentUsernamePasswordLogin->username), &(userRight[i].userName)))
{
return userRight[i].fAllowAddReference;
}
}
return UA_TRUE;
}
static UA_Boolean
allowDeleteNode(UA_Server *server, UA_AccessControl *ac,
const UA_NodeId *sessionId, void *sessionContext,
const UA_DeleteNodesItem *item)
{
printf("Called allowDeleteNode\n");
UA_UsernamePasswordLogin* currentUsernamePasswordLogin =
(UA_UsernamePasswordLogin*)sessionContext;
for (UA_UInt32 i = 0; i < UserCount; ++i)
{
if (UA_String_equal(&(currentUsernamePasswordLogin->username), &(userRight[i].userName)))
{
return userRight[i].fAllowDeleteNode;
}
}
return UA_TRUE;
}
static UA_Boolean
allowDeleteReference(UA_Server *server, UA_AccessControl *ac,
const UA_NodeId *sessionId, void *sessionContext,
const UA_DeleteReferencesItem *item)
{
printf("Called allowDeleteReference\n");
UA_UsernamePasswordLogin* currentUsernamePasswordLogin =
(UA_UsernamePasswordLogin*)sessionContext;
for (UA_UInt32 i = 0; i < UserCount; ++i)
{
if (UA_String_equal(&(currentUsernamePasswordLogin->username), &(userRight[i].userName)))
{
return userRight[i].fAllowDeleteReference;
}
}
return UA_TRUE;
}
static UA_StatusCode
activateSession(UA_Server *server, UA_AccessControl *ac,
const UA_EndpointDescription *endpointDescription,
const UA_ByteString *secureChannelRemoteCertificate,
const UA_NodeId *sessionId,
const UA_ExtensionObject *userIdentityToken,
void **sessionContext)
{
AccessControlContext * context = (AccessControlContext*)ac->context;
/* The empty token is interpreted as anonymous */
if (userIdentityToken->encoding == UA_EXTENSIONOBJECT_ENCODED_NOBODY) {
if (!context->allowAnonymous)
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
/* No userdata atm */
*sessionContext = NULL;
return UA_STATUSCODE_GOOD;
}
/* Could the token be decoded? */
if (userIdentityToken->encoding < UA_EXTENSIONOBJECT_DECODED)
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
/* Anonymous login */
if (userIdentityToken->content.decoded.type == &UA_TYPES[UA_TYPES_ANONYMOUSIDENTITYTOKEN]) {
if (!context->allowAnonymous)
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
const UA_AnonymousIdentityToken *token = (UA_AnonymousIdentityToken*)
userIdentityToken->content.decoded.data;
/* Compatibility notice: Siemens OPC Scout v10 provides an empty
* policyId. This is not compliant. For compatibility, assume that empty
* policyId == ANONYMOUS_POLICY */
if (token->policyId.data && !UA_String_equal(&token->policyId, &anonymous_policy))
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
/* No userdata atm */
*sessionContext = NULL;
return UA_STATUSCODE_GOOD;
}
/* Username and password */
if (userIdentityToken->content.decoded.type == &UA_TYPES[UA_TYPES_USERNAMEIDENTITYTOKEN]) {
const UA_UserNameIdentityToken *userToken =
(UA_UserNameIdentityToken*)userIdentityToken->content.decoded.data;
if (!UA_String_equal(&userToken->policyId, &username_policy))
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
/* The userToken has been decrypted by the server before forwarding
* it to the plugin. This information can be used here. */
/* if(userToken->encryptionAlgorithm.length > 0) {} */
/* Empty username and password */
if (userToken->userName.length == 0 && userToken->password.length == 0)
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
/* Try to match username/pw */
UA_Boolean match = false;
for (size_t i = 0; i < context->usernamePasswordLoginSize; i++) {
if (UA_String_equal(&userToken->userName, &context->usernamePasswordLogin[i].username) &&
UA_String_equal(&userToken->password, &context->usernamePasswordLogin[i].password)) {
match = true;
break;
}
}
if (!match)
return UA_STATUSCODE_BADUSERACCESSDENIED;
/* No userdata atm */
UA_UsernamePasswordLogin* currentUsernamePasswordLogin =
(UA_UsernamePasswordLogin*)UA_malloc(sizeof(UA_UsernamePasswordLogin));
UA_String_copy(&(userToken->userName), &(currentUsernamePasswordLogin->username));
UA_String_copy(&(userToken->password), &(currentUsernamePasswordLogin->password));
*sessionContext = currentUsernamePasswordLogin;
return UA_STATUSCODE_GOOD;
}
/* Unsupported token type */
return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
}
UA_Boolean running = true;
static void stopHandler(int sign)
{
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
running = false;
}
int main(void)
{
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_Server *server = UA_Server_new();
UA_ServerConfig *config = UA_Server_getConfig(server);
UA_ServerConfig_setDefault(config);
/* Disable anonymous logins, enable two user/password logins */
config->accessControl.deleteMembers(&config->accessControl);
UA_StatusCode retval = UA_AccessControl_default(config, false,
&config->securityPolicies[config->securityPoliciesSize - 1].policyUri, 2, logins);
if (retval != UA_STATUSCODE_GOOD)
goto cleanup;
// Set accessControl functions for activae session
config->accessControl.activateSession = activateSession;
/* Set accessControl functions for nodeManagement */
config->accessControl.allowAddNode = allowAddNode;
config->accessControl.allowAddReference = allowAddReference;
config->accessControl.allowDeleteNode = allowDeleteNode;
config->accessControl.allowDeleteReference = allowDeleteReference;
retval = UA_Server_run(server, &running);
cleanup:
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
需要注意的是如果用的是1.0之后的版本,第235行需要替换成下面的语句,api发生了变化
config->accessControl.clear(&config->accessControl);
五 总结
本文主要讲述了如何给不同用户指定不同的权限,主要是修改默认的access control函数,知道原理就比较简单了,最核心的是要获取运行时的用户名。
如果有写的不对的地方,希望能留言指正,谢谢阅读。