java连接opcua

一、前言

OPC UA(Open Platform Communications Unified Architecture)是针对工业自动化领域的跨平台通信协议标准。它在 OPC 经典版本的基础上进行优化,可以在不同操作系统、设备和编程语言之间进行安全且可靠的数据交换。对于很多工业控制、设备监控以及物联网相关项目,OPC UA 是常用的数据通信方式。

在 Java 中,我们常用的 OPC UA 客户端开发库包括:

本篇将使用 Eclipse Milo 作为示例库,演示如何在 Java 中使用匿名、用户名密码以及证书加密三种方式连接到 OPC UA 服务器。若需要使用其他 SDK,原理大同小异,API 的调用方式会有所不同。


二、准备工作

  1. JDK
    建议至少使用 JDK 8 或更高版本。

  2. Maven 或 Gradle
    便于引入 Eclipse Milo 等依赖。如果使用 Maven,请在 pom.xml 中添加以下依赖:

    <dependency>
        <groupId>org.eclipse.milo</groupId>
        <artifactId>sdk-client</artifactId>
        <version>0.6.15</version> 
        <!-- 版本号可根据需要更新 -->
    </dependency>
    <dependency>
             <groupId>org.eclipse.milo</groupId>
             <artifactId>server-examples</artifactId>
             <version>0.6.15</version> 
         </dependency>
    

    如果使用 Gradle,则在 build.gradle 中添加:

    implementation 'org.eclipse.milo:sdk-client:0.6.15'
    
  3. OPC UA 服务器
    本地或远程的 OPC UA 服务器环境,用于测试连接。可以在虚拟机或本地主机上安装开源的 OPC UA 服务器,也可以使用商业软件自带的模拟服务器。

  4. 证书文件(仅在证书加密方式时需要)

    • 若您在服务器上开启了证书加密,需要准备好客户端证书(public key)和客户端私钥(private key),也可能需要服务器的信任证书。
    • Eclipse Milo 提供了简单的证书管理机制,或者您也可以使用标准 Java KeyStore 的方式来存储并读取证书和私钥。

三、匿名方式连接

3.1 匿名方式简介

匿名连接是最简单的方式,不需要用户名、密码或任何证书。只要服务器允许匿名访问,就可以通过匿名方式连接。适合在测试环境或对安全要求不高的场景下使用。

3.2 示例代码

以下演示最基本的匿名连接流程,包括:

  1. 创建 OPC UA Client 配置
  2. 初始化并连接到服务器
  3. 读取或写入数据(仅作示例)

请确保替换示例中的 endpointUrlnodeId 等信息为你自己的实际配置。

import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder;
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.UserTokenPolicy;
import org.eclipse.milo.opcua.stack.core.types.enumerated.UserTokenType;
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider;

import java.util.List;
import java.util.concurrent.CompletableFuture;

public class OpcUaAnonymousExample {

    public static void main(String[] args) {
        try {
            // OPC UA 服务器地址,例如 "opc.tcp://localhost:49320"
            String url= "opc.tcp://127.0.0.1:49320";

            // 创建 client
                OpcUaClient client = OpcUaClient.create(url,
                        endpoints ->
                                endpoints.stream()
                                        .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri()))
                                        .findFirst(),
                        configBuilder ->
                                configBuilder
                                        //访问方式
                                        .setIdentityProvider(new AnonymousProvider())
                                        .setRequestTimeout(UInteger.valueOf(5000))
                                        .build());
            }

            // 连接到服务器
            CompletableFuture<OpcUaClient> future = client.connect();
            future.get(); // 等待连接完成

            System.out.println("匿名连接成功!");

            // 在此处可以进行读写操作,例如读取节点的值
            // client.readValue(0, TimestampsToReturn.Both, new ReadValueId(NodeId, ...));
            // ...

            // 最后断开连接
            client.disconnect().get();
            System.out.println("客户端断开连接。");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 简单选择一个安全策略为 None 的端点(匿名方式一般使用安全策略None,具体看服务器配置)
    private static EndpointDescription chooseSecureEndpoint(List<EndpointDescription> endpoints) {
        EndpointDescription result = null;
        for (EndpointDescription e : endpoints) {
            if (e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri())) {
                result = e;
                break;
            }
        }
        return result;
    }
}

在上述示例中,最关键的步骤是将身份认证方式设为 new AnonymousProvider() 并选择一个 SecurityPolicy 为 None 的 endpoint。这样即可使用匿名方式成功连接。


四、用户名密码方式连接

4.1 用户名密码方式简介

在实际生产环境中,常常需要使用账号密码进行身份验证,以限制访问权限、保护关键信息。与匿名方式相比,多了用户名密码的配置,但整体流程类似。

4.2 示例代码

import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder;
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.UserTokenPolicy;
import org.eclipse.milo.opcua.stack.core.types.enumerated.UserTokenType;
import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider;

import java.util.List;
import java.util.concurrent.CompletableFuture;

public class OpcUaUsernamePasswordExample {

    public static void main(String[] args) {
        try {
            String endpointUrl = "opc.tcp://127.0.0.1:4840";

            List<EndpointDescription> endpoints = OpcUaClient
                    .getEndpoints(endpointUrl).get();

            EndpointDescription endpoint = chooseUserNameEndpoint(endpoints);

            OpcUaClientConfigBuilder configBuilder = new OpcUaClientConfigBuilder();
            configBuilder.setEndpoint(endpoint);

            // 假设用户名为 "user", 密码为 "password"
            configBuilder.setIdentityProvider(new UsernameProvider("user", "password"));

            OpcUaClient client = OpcUaClient.create(configBuilder.build());

            CompletableFuture<OpcUaClient> future = client.connect();
            future.get();

            System.out.println("用户名密码方式连接成功!");

            // 进行后续读写操作
            // ...

            client.disconnect().get();
            System.out.println("客户端断开连接。");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static EndpointDescription chooseUserNameEndpoint(List<EndpointDescription> endpoints) {
        // 通常 OPC UA 服务器也支持 SecurityPolicy.None + UserName 方式
        // 也可能是 Basic128Rsa15, Basic256, etc. 具体看服务端配置
        for (EndpointDescription e : endpoints) {
            if (e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri())) {
                // 确保端点支持 UserName 类型的认证
                for (UserTokenPolicy tokenPolicy : e.getUserIdentityTokens()) {
                    if (tokenPolicy.getTokenType() == UserTokenType.UserName) {
                        return e;
                    }
                }
            }
        }
        return null;
    }
}

要点说明:

  1. IdentityProvider 切换为 new UsernameProvider("username", "password")
  2. 根据服务端提供的用户名、密码进行配置。
  3. 需要注意端点是否支持 UserName 类型认证。如果端点仅支持 Anonymous 或 Certificate,则无法使用用户名密码方式。

五、证书加密方式连接

5.1 证书加密方式简介

在实际工业环境中,安全性要求更高时通常会启用证书加密(基于 Public Key Infrastructure)。

  • 每个客户端都会持有一份证书(公钥)和对应的私钥,服务器端也有自己的证书。
  • 当客户端与服务器通信时,会先验证双方的证书签名并进行加密传输,从而保证安全性与完整性。

在这种方式下,服务端可能要求:

  1. 客户端必须提供已经被服务器信任(或在服务器端手动信任)的证书。
  2. 采用特定的安全策略(例如 Basic256Sha256)并通过相应端点连接。

5.2 证书和私钥获取

  • 可以通过第三方工具(例如 openssl、keytool 或 Eclipse Milo 提供的证书工具脚本)生成自签名证书。
  • 生成后的证书和私钥,可以存储在 Java KeyStore 中,或者存储为 .der.pem 等格式并让应用程序读取。

下方示例假设已经拥有 ClientCert.der(客户端公钥)和 ClientKey.der(客户端私钥),并且服务器端配置了对应的信任或信任链。

OPC UA访问证书类

import org.eclipse.milo.opcua.sdk.server.util.HostnameUtil;
import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateBuilder;
import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.regex.Pattern;
class KeyStoreLoader {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private static final Pattern IP_ADDR_PATTERN = Pattern.compile(
        "^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
    // 证书别名
    private static final String CLIENT_ALIAS = "client-ai";
    // 获取私钥的密码
    private static final char[] PASSWORD = "password".toCharArray();
    // 证书对象
    private X509Certificate clientCertificate;
    // 密钥对对象
    private KeyPair clientKeyPair;
    KeyStoreLoader load(Path baseDir) throws Exception {
        // 创建一个使用`PKCS12`加密标准的KeyStore。KeyStore在后面将作为读取和生成证书的对象。
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        // PKCS12的加密标准的文件后缀是.pfx,其中包含了公钥和私钥。
        // 而其他如.der等的格式只包含公钥,私钥在另外的文件中。
        Path serverKeyStore = baseDir.resolve("example-client.pfx");
        logger.info("Loading KeyStore at {}", serverKeyStore);
        // 如果文件不存在则创建.pfx证书文件。
        if (!Files.exists(serverKeyStore)) {
            keyStore.load(null, PASSWORD);
            // 用2048位的RAS算法。`SelfSignedCertificateGenerator`为Milo库的对象。
            KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048);
            // `SelfSignedCertificateBuilder`也是Milo库的对象,用来生成证书。
            // 中间所设置的证书属性可以自行修改。
            SelfSignedCertificateBuilder builder = new SelfSignedCertificateBuilder(keyPair)
                .setCommonName("Eclipse Milo Example Client")
                .setOrganization("digitalpetri")
                .setOrganizationalUnit("dev")
                .setLocalityName("Folsom")
                .setStateName("CA")
                .setCountryCode("US")
                .setApplicationUri("urn:eclipse:milo:examples:client")
                .addDnsName("localhost")
                .addIpAddress("127.0.0.1");
            // Get as many hostnames and IP addresses as we can listed in the certificate.
            for (String hostname : HostnameUtil.getHostnames("0.0.0.0")) {
                if (IP_ADDR_PATTERN.matcher(hostname).matches()) {
                    builder.addIpAddress(hostname);
                } else {
                    builder.addDnsName(hostname);
                }
            }
            // 创建证书
            X509Certificate certificate = builder.build();
            // 设置对应私钥的别名,密码,证书链
            keyStore.setKeyEntry(CLIENT_ALIAS, keyPair.getPrivate(), PASSWORD, new X509Certificate[]{certificate});
            try (OutputStream out = Files.newOutputStream(serverKeyStore)) {
                // 保存证书到输出流
                keyStore.store(out, PASSWORD);
            }
        } else {
            try (InputStream in = Files.newInputStream(serverKeyStore)) {
                // 如果文件存在则读取
                keyStore.load(in, PASSWORD);
            }
        }
        // 用密码获取对应别名的私钥。
        Key serverPrivateKey = keyStore.getKey(CLIENT_ALIAS, PASSWORD);
        if (serverPrivateKey instanceof PrivateKey) {
            // 获取对应别名的证书对象。
            clientCertificate = (X509Certificate) keyStore.getCertificate(CLIENT_ALIAS);
            // 获取公钥
            PublicKey serverPublicKey = clientCertificate.getPublicKey();
            // 创建Keypair对象。
            clientKeyPair = new KeyPair(serverPublicKey, (PrivateKey) serverPrivateKey);
        }
        return this;
    }
    // 返回证书
    X509Certificate getClientCertificate() {
        return clientCertificate;
    }
    // 返回密钥对
    KeyPair getClientKeyPair() {
        return clientKeyPair;
    }
}

5.3 示例代码

public static OpcUaClient initClient(String url,SecurityPolicy securityPolicy) {
        try {
            if (securityPolicy.equals(SecurityPolicy.None)){
                return OpcUaClient.create(url,
                        endpoints ->
                                endpoints.stream()
                                        .filter(e -> e.getSecurityPolicyUri().equals(securityPolicy.getUri()))
                                        .findFirst(),
                        configBuilder ->
                                configBuilder
                                        //访问方式
                                        .setIdentityProvider(new AnonymousProvider())
                                        .setRequestTimeout(UInteger.valueOf(5000))
                                        .build());
            }
            Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "security");
            Files.createDirectories(securityTempDir);
            if (!Files.exists(securityTempDir)) {
                throw new Exception("unable to create security dir: " + securityTempDir);
            }
            KeyStoreLoader loader = new KeyStoreLoader().load(securityTempDir);
            File pkiDir = securityTempDir.resolve("pki").toFile();

            DefaultTrustListManager trustListManager = new DefaultTrustListManager(pkiDir);

            DefaultClientCertificateValidator certificateValidator =
                    new DefaultClientCertificateValidator(trustListManager);
            String hostName = InetAddress.getLocalHost().getHostName();
            return OpcUaClient.create(url,
                    endpoints ->
                            endpoints.stream()
                                    .map(endpoint -> {
                                        // 构建一个新的 EndpointDescription(可选修改某些字段)
                                        return new EndpointDescription(
                                                url,
                                                endpoint.getServer(),
                                                endpoint.getServerCertificate(),
                                                endpoint.getSecurityMode(), // 或者强制改为某种模式
                                                endpoint.getSecurityPolicyUri(),
                                                endpoint.getUserIdentityTokens(),
                                                endpoint.getTransportProfileUri(),
                                                endpoint.getSecurityLevel()
                                        );
                                    })
                                    .filter(e -> e.getSecurityPolicyUri().equals(securityPolicy.getUri()))
                                    .findFirst(),
                    configBuilder ->
                            configBuilder
                                    //访问方式
                                    .setApplicationName(LocalizedText.english("datacollector-driver"))
                                    .setApplicationUri(String.format("urn:%s:opcua-client", hostName))  // 必须与证书中的URI一致
                                    .setKeyPair(loader.getClientKeyPair())
                                    .setCertificate(loader.getClientCertificate())
                                    .setCertificateChain(loader.getClientCertificateChain())
                                    .setCertificateValidator(certificateValidator)
                                    .setIdentityProvider(new UsernameProvider("admin", "123456"))
                                    .setRequestTimeout(UInteger.valueOf(5000))
                                    .build());

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

证书路径
在这里插入图片描述
我们需要把服务器证书放在pki\trusted\certs目录下:
在这里插入图片描述

要点说明:

  1. 选择合适的安全策略(如 Basic256Sha256)。
  2. 使用客户端证书和私钥(可以自签名,也可以通过权威 CA 签发)。
  3. 服务端需信任此客户端证书(在服务器配置中添加到信任列表)。
  4. 配置 CertificateManagerCertificateValidator 以及 X509IdentityProvider

六、常见问题与注意事项

  1. 端点选择

    • 不同 OPC UA 服务器可能同时暴露多个端点,包含不同的安全模式(Security Mode)和安全策略(Security Policy)。
    • 在匿名或用户名密码方式时,如果选择了需要证书的端点,就会出现认证失败或连接被拒的情况。
    • 在证书加密方式时,如果选择了安全策略为 None 的端点,则证书不会被使用,同样也会连接异常或者导致安全策略不匹配。
  2. 服务器信任客户端证书

    • 大多数 OPC UA 服务器在默认情况下不信任任何客户端的证书,需要在服务端管理界面或配置文件中手动将客户端证书加入白名单。
    • 记得查看服务器日志,若提示「Untrusted Certificate」,就需要在服务器端操作信任列表。
  3. 安全策略与性能

    • 加密等级越高(如 Basic256Sha256),对 CPU 资源消耗越大,通信速度会相对降低,但数据安全性更强。
    • 在测试环境或低安全需求的场景下可以先使用 SecurityPolicy.None ;正式项目上线时再切换到更高的安全策略。
  4. 兼容性

    • 不同版本的 OPC UA SDK、服务器或 Java 版本之间可能存在兼容性问题;如果连接失败,可以尝试升级或降低 Milo 版本、换用不同的 JDK 版本等。
    • OPC UA 服务器上若启用特定的加密算法(例如 AES-256),客户端也需要对应的加密套件。
  5. 断线重连

    • 工业现场环境中网络抖动常见,客户端需要实现断线重连或重试机制,以确保数据采集的连续性与稳定性。

下面是一个简单的 Java 代码示例,用于连接 OPC UA 服务器和读取其中的数据。这个代码示例使用 Eclipse Milo 库来实现 OPC UA 功能。 ```java import org.eclipse.milo.opcua.sdk.client.OpcUaClient; import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider; import org.eclipse.milo.opcua.sdk.client.model.nodes.objects.ServerNode; import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class OpcUaClientExample { public static void main(String[] args) throws InterruptedException, ExecutionException { // 创建 OPC UA 客户端 OpcUaClient client = OpcUaClient.create("opc.tcp://localhost:12686/UA/MyLittleServer"); // 设置用户凭证(如果需要) client.setIdentityProvider(new UsernameProvider("user", "password".toCharArray())); // 连接 OPC UA 服务器 CompletableFuture<OpcUaClient> future = client.connect(); future.get(); // 读取 OPC UA 服务器上的节点数据,例如服务器版本号 NodeId nodeId = new NodeId(2, "i=2261"); CompletableFuture<DataValue> readFuture = client.readValue(0, null, nodeId); DataValue dataValue = readFuture.get(); Variant variant = dataValue.getValue(); Object value = variant.getValue(); System.out.println("Server version: " + value); // 关闭 OPC UA 客户端 client.disconnect().get(); } } ``` 在上面的代码示例中,我们首先创建了一个 OPC UA 客户端对象,然后设置了 OPC UA 服务器的 URL 地址和用户凭证(如果需要)。接着,我们使用 `connect()` 方法连接 OPC UA 服务器,这个方法返回一个 `CompletableFuture<OpcUaClient>` 对象,我们需要使用 `get()` 方法等待连接成功。连接成功后,我们就可以使用客户端对象的各种方法来读取 OPC UA 服务器上的节点数据。在上面的代码示例中,我们读取了服务器版本号,并将其打印到控制台上。最后,我们使用 `disconnect()` 方法关闭了 OPC UA 客户端。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值