一、前言
OPC UA(Open Platform Communications Unified Architecture)是针对工业自动化领域的跨平台通信协议标准。它在 OPC 经典版本的基础上进行优化,可以在不同操作系统、设备和编程语言之间进行安全且可靠的数据交换。对于很多工业控制、设备监控以及物联网相关项目,OPC UA 是常用的数据通信方式。
在 Java 中,我们常用的 OPC UA 客户端开发库包括:
- Eclipse Milo
- Prosys OPC UA SDK for Java
- 其他商业或开源的 Java SDK
本篇将使用 Eclipse Milo 作为示例库,演示如何在 Java 中使用匿名、用户名密码以及证书加密三种方式连接到 OPC UA 服务器。若需要使用其他 SDK,原理大同小异,API 的调用方式会有所不同。
二、准备工作
-
JDK
建议至少使用 JDK 8 或更高版本。 -
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'
-
OPC UA 服务器
本地或远程的 OPC UA 服务器环境,用于测试连接。可以在虚拟机或本地主机上安装开源的 OPC UA 服务器,也可以使用商业软件自带的模拟服务器。 -
证书文件(仅在证书加密方式时需要)
- 若您在服务器上开启了证书加密,需要准备好客户端证书(public key)和客户端私钥(private key),也可能需要服务器的信任证书。
- Eclipse Milo 提供了简单的证书管理机制,或者您也可以使用标准 Java KeyStore 的方式来存储并读取证书和私钥。
三、匿名方式连接
3.1 匿名方式简介
匿名连接是最简单的方式,不需要用户名、密码或任何证书。只要服务器允许匿名访问,就可以通过匿名方式连接。适合在测试环境或对安全要求不高的场景下使用。
3.2 示例代码
以下演示最基本的匿名连接流程,包括:
- 创建 OPC UA Client 配置
- 初始化并连接到服务器
- 读取或写入数据(仅作示例)
请确保替换示例中的 endpointUrl
与 nodeId
等信息为你自己的实际配置。
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;
}
}
要点说明:
- 将
IdentityProvider
切换为new UsernameProvider("username", "password")
。 - 根据服务端提供的用户名、密码进行配置。
- 需要注意端点是否支持 UserName 类型认证。如果端点仅支持 Anonymous 或 Certificate,则无法使用用户名密码方式。
五、证书加密方式连接
5.1 证书加密方式简介
在实际工业环境中,安全性要求更高时通常会启用证书加密(基于 Public Key Infrastructure)。
- 每个客户端都会持有一份证书(公钥)和对应的私钥,服务器端也有自己的证书。
- 当客户端与服务器通信时,会先验证双方的证书签名并进行加密传输,从而保证安全性与完整性。
在这种方式下,服务端可能要求:
- 客户端必须提供已经被服务器信任(或在服务器端手动信任)的证书。
- 采用特定的安全策略(例如
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
目录下:
要点说明:
- 选择合适的安全策略(如
Basic256Sha256
)。 - 使用客户端证书和私钥(可以自签名,也可以通过权威 CA 签发)。
- 服务端需信任此客户端证书(在服务器配置中添加到信任列表)。
- 配置
CertificateManager
、CertificateValidator
以及X509IdentityProvider
。
六、常见问题与注意事项
-
端点选择
- 不同 OPC UA 服务器可能同时暴露多个端点,包含不同的安全模式(Security Mode)和安全策略(Security Policy)。
- 在匿名或用户名密码方式时,如果选择了需要证书的端点,就会出现认证失败或连接被拒的情况。
- 在证书加密方式时,如果选择了安全策略为 None 的端点,则证书不会被使用,同样也会连接异常或者导致安全策略不匹配。
-
服务器信任客户端证书
- 大多数 OPC UA 服务器在默认情况下不信任任何客户端的证书,需要在服务端管理界面或配置文件中手动将客户端证书加入白名单。
- 记得查看服务器日志,若提示「Untrusted Certificate」,就需要在服务器端操作信任列表。
-
安全策略与性能
- 加密等级越高(如 Basic256Sha256),对 CPU 资源消耗越大,通信速度会相对降低,但数据安全性更强。
- 在测试环境或低安全需求的场景下可以先使用 SecurityPolicy.None ;正式项目上线时再切换到更高的安全策略。
-
兼容性
- 不同版本的 OPC UA SDK、服务器或 Java 版本之间可能存在兼容性问题;如果连接失败,可以尝试升级或降低 Milo 版本、换用不同的 JDK 版本等。
- OPC UA 服务器上若启用特定的加密算法(例如 AES-256),客户端也需要对应的加密套件。
-
断线重连
- 工业现场环境中网络抖动常见,客户端需要实现断线重连或重试机制,以确保数据采集的连续性与稳定性。