在之前的系列文章中,OPC UA Client和OPC UA Server之間的連接都是使用的匿名,並沒有使用用戶名和密碼,本文主要講述如何關閉匿名登錄,以及如何使用用戶名和密碼去連接Server。
一 默認行爲
首先看下OPC UA Server的默認行爲,下面是段典型的Server代碼,
#include "open62541.h"
#include <signal.h>
#include <stdlib.h>
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_setDefault(UA_Server_getConfig(server));
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
從源碼去查看一下其default行爲:UA_ServerConfig_setDefault()->UA_ServerConfig_setMinimal()->UA_ServerConfig_setMinimalCustomBuffer()
在UA_ServerConfig_setMinimalCustomBuffer()裏,發現如下代碼,
/* Initialize the Access Control plugin */
retval = UA_AccessControl_default(config, true,
&config->securityPolicies[config->securityPoliciesSize-1].policyUri,
usernamePasswordsSize, usernamePasswords);
UA_AccessControl_default()的函數原型如下,
UA_StatusCode
UA_AccessControl_default(UA_ServerConfig *config, UA_Boolean allowAnonymous,
const UA_ByteString *userTokenPolicyUri,
size_t usernamePasswordLoginSize,
const UA_UsernamePasswordLogin *usernamePasswordLogin)
其參數分析:
- config:server的配置指針
- allowAnonymous:是否允許匿名登錄,true表示允許,false表示不允許
- userTokenPolicyUri:用戶登錄時使用的安全策略Uri
- usernamePasswordLoginSize:允許登錄的用戶數量
- usernamePasswordLogin:允許登錄的用戶名和密碼數組
在調用UA_AccessControl_default()時第2個實參是true,表示允許匿名登錄,第3個參數由於Server未使用安全策略,所以爲None,第4個實參是usernamePasswordsSize,第5個實參是usernamePasswords,它們定義如下,
static const size_t usernamePasswordsSize = 2;
static UA_UsernamePasswordLogin usernamePasswords[2] = {
{UA_STRING_STATIC("user1"), UA_STRING_STATIC("password")},
{UA_STRING_STATIC("user2"), UA_STRING_STATIC("password1")}};
表示允許2個用戶登錄,第一個名叫user1,密碼是password,第二個名叫user2,密碼是password1
這裏要記住一個概念:某個用戶想登錄一個Server,那麼在Server這邊要有對這個用戶的記錄,表示允許該用戶登錄,不然隨便哪個用戶都能登錄,就沒有安全性可言了。這個和電腦開機後要輸入正確的用戶名密碼登錄是一樣的。
下面就使用用戶user1來登錄,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", "user1", "password");
if (retval != UA_STATUSCODE_GOOD) {
UA_Client_delete(client);
return EXIT_FAILURE;
}
else {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "user1 connect ok!\n");
}
UA_Client_delete(client); /* Disconnects the client internally */
return EXIT_SUCCESS;
}
代碼中使用了函數UA_Client_connect_username()來連接Server,client端打印如下,
表示user1登錄Server成功!
二 自定義用戶名和密碼
Server的默認行爲往往不是我們需要的,下面就來看下如何自定義用戶名和密碼。首先,Server端要更改配置,把自定義的用戶名和密碼添加進去,
新的用戶名和密碼如下,
static UA_UsernamePasswordLogin logins[2] = {
{UA_STRING_STATIC("peter"), UA_STRING_STATIC("peter123")},
{UA_STRING_STATIC("paula"), UA_STRING_STATIC("paula123")}
};
Server把默認添加的user1和user2刪掉,
UA_Server *server = UA_Server_new();
UA_ServerConfig *config = UA_Server_getConfig(server);
UA_ServerConfig_setDefault(config);
config->accessControl.deleteMembers(&config->accessControl);
然後把新的用戶名和密碼添加進去,並關閉匿名登錄,
UA_StatusCode retval = UA_AccessControl_default(config, false,
&config->securityPolicies[config->securityPoliciesSize-1].policyUri, 2, logins);
UA_AccessControl_default()裏同時也設置了用戶的各種默認權限,如是否運行添加Node,是否允許刪除Node等,如下,
UA_AccessControl *ac = &config->accessControl;
ac->deleteMembers = deleteMembers_default;
ac->activateSession = activateSession_default;
ac->closeSession = closeSession_default;
ac->getUserRightsMask = getUserRightsMask_default;
ac->getUserAccessLevel = getUserAccessLevel_default;
ac->getUserExecutable = getUserExecutable_default;
ac->getUserExecutableOnObject = getUserExecutableOnObject_default;
ac->allowAddNode = allowAddNode_default;
ac->allowAddReference = allowAddReference_default;
#ifdef UA_ENABLE_HISTORIZING
ac->allowHistoryUpdateUpdateData = allowHistoryUpdateUpdateData_default;
ac->allowHistoryUpdateDeleteRawModified = allowHistoryUpdateDeleteRawModified_default;
#endif
ac->allowDeleteNode = allowDeleteNode_default;
ac->allowDeleteReference = allowDeleteReference_default;
我們可以根據自己的需要進行修改,即替換相關函數,舉例如下,
/* Set accessControl functions for nodeManagement */
config->accessControl.allowAddNode = allowAddNode;
config->accessControl.allowAddReference = allowAddReference;
config->accessControl.allowDeleteNode = allowDeleteNode;
config->accessControl.allowDeleteReference = allowDeleteReference;
這4個函數定義下,
static UA_Boolean
allowAddNode(UA_Server *server, UA_AccessControl *ac,
const UA_NodeId *sessionId, void *sessionContext,
const UA_AddNodesItem *item) {
printf("Called allowAddNode\n");
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");
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");
return UA_FALSE; // Do not allow deletion from client
}
static UA_Boolean
allowDeleteReference(UA_Server *server, UA_AccessControl *ac,
const UA_NodeId *sessionId, void *sessionContext,
const UA_DeleteReferencesItem *item) {
printf("Called allowDeleteReference\n");
return UA_TRUE;
}
可以看出,允許用戶添加Node和Reference,允許刪除Reference,但是不允許刪除Node。
Server整體代碼如下,
#include <signal.h>
#include <stdlib.h>
#include "open62541.h"
static UA_Boolean
allowAddNode(UA_Server *server, UA_AccessControl *ac,
const UA_NodeId *sessionId, void *sessionContext,
const UA_AddNodesItem *item) {
printf("Called allowAddNode\n");
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");
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");
return UA_FALSE; // Do not allow deletion from client
}
static UA_Boolean
allowDeleteReference(UA_Server *server, UA_AccessControl *ac,
const UA_NodeId *sessionId, void *sessionContext,
const UA_DeleteReferencesItem *item) {
printf("Called allowDeleteReference\n");
return UA_TRUE;
}
UA_Boolean running = true;
static void stopHandler(int sign) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
running = false;
}
static UA_UsernamePasswordLogin logins[2] = {
{UA_STRING_STATIC("peter"), UA_STRING_STATIC("peter123")},
{UA_STRING_STATIC("paula"), UA_STRING_STATIC("paula123")}
};
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 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;
}
Client端代碼如下,代碼裏讓用戶paula添加一個Node和Reference,並去刪除Reference,最後嘗試去刪除Node,由之前的設置可知,刪除Node會失敗,
#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;
}
運行後可以看到Client端打印如下,
用戶paula登錄成功,並且添加Node和Rference,以及刪除Reference都OK,但是刪除Node失敗,因爲paula沒有這個權限,和預期一致。
此時,如果我們使用UaExpert去連接Server,會提示失敗,如下這種,
因爲這是匿名登錄,已經被關閉,可以改爲使用用戶名登錄,如下,就可以登錄成功!
PS:登錄時會提示沒有安全模式,點擊Ignore就行了,因爲現在沒有使用安全證書。
三 安全登錄
這一節講述如何使用安全策略來實現安全登錄,需要參考這篇文章,除了Server和Client端代碼略微修改了一下,其它都是一樣。
Server端代碼如下,
#include <signal.h>
#include <stdlib.h>
#include "common.h"
UA_Boolean running = true;
static void stopHandler(int sig) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
running = false;
}
static UA_UsernamePasswordLogin logins[2] = {
{UA_STRING_STATIC("peter"), UA_STRING_STATIC("peter123")},
{UA_STRING_STATIC("paula"), UA_STRING_STATIC("paula123")}
};
int main(int argc, char* argv[]) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
if(argc < 3) {
UA_LOG_FATAL(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Missing arguments. Arguments are "
"<server-certificate.der> <private-key.der> "
"[<trustlist1.crl>, ...]");
return EXIT_FAILURE;
}
/* Load certificate and private key */
UA_ByteString certificate = loadFile(argv[1]);
UA_ByteString privateKey = loadFile(argv[2]);
/* Load the trustlist */
size_t trustListSize = 0;
if(argc > 3)
trustListSize = (size_t)argc-3;
UA_STACKARRAY(UA_ByteString, trustList, trustListSize);
for(size_t i = 0; i < trustListSize; i++)
trustList[i] = loadFile(argv[i+3]);
/* Loading of a issuer list, not used in this application */
size_t issuerListSize = 0;
UA_ByteString *issuerList = NULL;
/* Loading of a revocation list currently unsupported */
UA_ByteString *revocationList = NULL;
size_t revocationListSize = 0;
UA_Server *server = UA_Server_new();
UA_ServerConfig *config = UA_Server_getConfig(server);
UA_StatusCode retval =
UA_ServerConfig_setDefaultWithSecurityPolicies(config, 4840,
&certificate, &privateKey,
trustList, trustListSize,
issuerList, issuerListSize,
revocationList, revocationListSize);
config->applicationDescription.applicationUri = UA_STRING_ALLOC("urn:open62541.server.application");
UA_ByteString_clear(&certificate);
UA_ByteString_clear(&privateKey);
for(size_t i = 0; i < trustListSize; i++)
UA_ByteString_clear(&trustList[i]);
if(retval != UA_STATUSCODE_GOOD)
goto cleanup;
/* Disable anonymous logins, enable two user/password logins */
config->accessControl.deleteMembers(&config->accessControl);
retval = UA_AccessControl_default(config, false,
&config->securityPolicies[config->securityPoliciesSize-1].policyUri, 2, logins);
if(retval != UA_STATUSCODE_GOOD)
goto cleanup;
retval = UA_Server_run(server, &running);
cleanup:
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
Client端代碼如下,把安全策略改爲Basic256Sha256,另外2種也可以,
#include <stdlib.h>
#include "common.h"
#define MIN_ARGS 4
int main(int argc, char* argv[]) {
if(argc < MIN_ARGS) {
UA_LOG_FATAL(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Arguments are missing. The required arguments are "
"<opc.tcp://host:port> "
"<client-certificate.der> <client-private-key.der> "
"[<trustlist1.crl>, ...]");
return EXIT_FAILURE;
}
const char *endpointUrl = argv[1];
/* Load certificate and private key */
UA_ByteString certificate = loadFile(argv[2]);
UA_ByteString privateKey = loadFile(argv[3]);
/* Load the trustList. Load revocationList is not supported now */
size_t trustListSize = 0;
if(argc > MIN_ARGS)
trustListSize = (size_t)argc-MIN_ARGS;
UA_STACKARRAY(UA_ByteString, trustList, trustListSize);
for(size_t trustListCount = 0; trustListCount < trustListSize; trustListCount++)
trustList[trustListCount] = loadFile(argv[trustListCount+4]);
UA_ByteString *revocationList = NULL;
size_t revocationListSize = 0;
UA_Client *client = UA_Client_new();
UA_ClientConfig *cc = UA_Client_getConfig(client);
cc->securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT;
cc->securityPolicyUri = UA_STRING_ALLOC("http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256");
UA_ClientConfig_setDefaultEncryption(cc, certificate, privateKey,
trustList, trustListSize,
revocationList, revocationListSize);
// 填坑的地方,非常重要
cc->clientDescription.applicationUri = UA_STRING_ALLOC("urn:open62541.server.application");
UA_ByteString_clear(&certificate);
UA_ByteString_clear(&privateKey);
for(size_t deleteCount = 0; deleteCount < trustListSize; deleteCount++) {
UA_ByteString_clear(&trustList[deleteCount]);
}
/* Secure client connect */
UA_StatusCode retval = UA_Client_connect_username(client, endpointUrl, "paula", "paula123");
if(retval != UA_STATUSCODE_GOOD) {
UA_Client_delete(client);
return EXIT_FAILURE;
}
UA_Variant value;
UA_Variant_init(&value);
/* NodeId of the variable holding the current time */
const UA_NodeId nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME);
retval = UA_Client_readValueAttribute(client, nodeId, &value);
if(retval == UA_STATUSCODE_GOOD &&
UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_DATETIME])) {
UA_DateTime raw_date = *(UA_DateTime *) value.data;
UA_DateTimeStruct dts = UA_DateTime_toStruct(raw_date);
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "date is: %u-%u-%u %u:%u:%u.%03u\n",
dts.day, dts.month, dts.year, dts.hour, dts.min, dts.sec, dts.milliSec);
}
/* Clean up */
UA_Variant_clear(&value);
UA_Client_delete(client);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
Client端代碼目的是獲取Server的系統時間並打印。
運行後,關於安全策略和用戶的打印如下,表示已經使用了
時間打印如下,
登錄成功!
四 總結
本文主要講述如何使用用戶名和密碼登錄OPC UA Server,這個和我們平時登錄電腦或APP賬戶等是類似的。關閉匿名登錄,可以加大保密性,使用安全證書登錄可以增加安全性。
如果有寫的不對的地方,希望能留言指正,謝謝閱讀。