學習open62541 --- [21] 使用用戶名和密碼登錄

在之前的系列文章中,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)

其參數分析:

  1. config:server的配置指針
  2. allowAnonymous:是否允許匿名登錄,true表示允許,false表示不允許
  3. userTokenPolicyUri:用戶登錄時使用的安全策略Uri
  4. usernamePasswordLoginSize:允許登錄的用戶數量
  5. 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賬戶等是類似的。關閉匿名登錄,可以加大保密性,使用安全證書登錄可以增加安全性。

如果有寫的不對的地方,希望能留言指正,謝謝閱讀。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章