java連接opcua的常見(jiàn)問(wèn)題及解決方法
一、前言
OPC UA(Open Platform Communications Unified Architecture)是針對(duì)工業(yè)自動(dòng)化領(lǐng)域的跨平臺(tái)通信協(xié)議標(biāo)準(zhǔn)。它在 OPC 經(jīng)典版本的基礎(chǔ)上進(jìn)行優(yōu)化,可以在不同操作系統(tǒng)、設(shè)備和編程語(yǔ)言之間進(jìn)行安全且可靠的數(shù)據(jù)交換。對(duì)于很多工業(yè)控制、設(shè)備監(jiān)控以及物聯(lián)網(wǎng)相關(guān)項(xiàng)目,OPC UA 是常用的數(shù)據(jù)通信方式。
在 Java 中,我們常用的 OPC UA 客戶端開(kāi)發(fā)庫(kù)包括:
- Eclipse Milo
- Prosys OPC UA SDK for Java
- 其他商業(yè)或開(kāi)源的 Java SDK
本篇將使用 Eclipse Milo 作為示例庫(kù),演示如何在 Java 中使用匿名、用戶名密碼以及證書(shū)加密三種方式連接到 OPC UA 服務(wù)器。若需要使用其他 SDK,原理大同小異,API 的調(diào)用方式會(huì)有所不同。
二、準(zhǔn)備工作
JDK
建議至少使用 JDK 8 或更高版本。
Maven 或 Gradle
便于引入 Eclipse Milo 等依賴。如果使用 Maven,請(qǐng)?jiān)?pom.xml
中添加以下依賴:
<dependency> <groupId>org.eclipse.milo</groupId> <artifactId>sdk-client</artifactId> <version>0.6.15</version> <!-- 版本號(hào)可根據(jù)需要更新 --> </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 服務(wù)器
本地或遠(yuǎn)程的 OPC UA 服務(wù)器環(huán)境,用于測(cè)試連接。可以在虛擬機(jī)或本地主機(jī)上安裝開(kāi)源的 OPC UA 服務(wù)器,也可以使用商業(yè)軟件自帶的模擬服務(wù)器。
證書(shū)文件(僅在證書(shū)加密方式時(shí)需要)
若您在服務(wù)器上開(kāi)啟了證書(shū)加密,需要準(zhǔn)備好客戶端證書(shū)(public key)和客戶端私鑰(private key),也可能需要服務(wù)器的信任證書(shū)。Eclipse Milo 提供了簡(jiǎn)單的證書(shū)管理機(jī)制,或者您也可以使用標(biāo)準(zhǔn) Java KeyStore 的方式來(lái)存儲(chǔ)并讀取證書(shū)和私鑰。
三、匿名方式連接
3.1 匿名方式簡(jiǎn)介
匿名連接是最簡(jiǎn)單的方式,不需要用戶名、密碼或任何證書(shū)。只要服務(wù)器允許匿名訪問(wèn),就可以通過(guò)匿名方式連接。適合在測(cè)試環(huán)境或?qū)Π踩蟛桓叩膱?chǎng)景下使用。
3.2 示例代碼
以下演示最基本的匿名連接流程,包括:
- 創(chuàng)建 OPC UA Client 配置
- 初始化并連接到服務(wù)器
- 讀取或?qū)懭霐?shù)據(jù)(僅作示例)
請(qǐng)確保替換示例中的 endpointUrl
與 nodeId
等信息為你自己的實(shí)際配置。
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 服務(wù)器地址,例如 "opc.tcp://localhost:49320" String url= "opc.tcp://127.0.0.1:49320"; // 創(chuàng)建 client OpcUaClient client = OpcUaClient.create(url, endpoints -> endpoints.stream() .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri())) .findFirst(), configBuilder -> configBuilder //訪問(wèn)方式 .setIdentityProvider(new AnonymousProvider()) .setRequestTimeout(UInteger.valueOf(5000)) .build()); } // 連接到服務(wù)器 CompletableFuture<OpcUaClient> future = client.connect(); future.get(); // 等待連接完成 System.out.println("匿名連接成功!"); // 在此處可以進(jìn)行讀寫(xiě)操作,例如讀取節(jié)點(diǎn)的值 // client.readValue(0, TimestampsToReturn.Both, new ReadValueId(NodeId, ...)); // ... // 最后斷開(kāi)連接 client.disconnect().get(); System.out.println("客戶端斷開(kāi)連接。"); } catch (Exception e) { e.printStackTrace(); } } // 簡(jiǎn)單選擇一個(gè)安全策略為 None 的端點(diǎn)(匿名方式一般使用安全策略None,具體看服務(wù)器配置) 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; } }
在上述示例中,最關(guān)鍵的步驟是將身份認(rèn)證方式設(shè)為 new AnonymousProvider()
并選擇一個(gè) SecurityPolicy 為 None
的 endpoint。這樣即可使用匿名方式成功連接。
四、用戶名密碼方式連接
4.1 用戶名密碼方式簡(jiǎn)介
在實(shí)際生產(chǎn)環(huán)境中,常常需要使用賬號(hào)密碼進(jìn)行身份驗(yàn)證,以限制訪問(wèn)權(quán)限、保護(hù)關(guān)鍵信息。與匿名方式相比,多了用戶名密碼的配置,但整體流程類似。
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); // 假設(shè)用戶名為 "user", 密碼為 "password" configBuilder.setIdentityProvider(new UsernameProvider("user", "password")); OpcUaClient client = OpcUaClient.create(configBuilder.build()); CompletableFuture<OpcUaClient> future = client.connect(); future.get(); System.out.println("用戶名密碼方式連接成功!"); // 進(jìn)行后續(xù)讀寫(xiě)操作 // ... client.disconnect().get(); System.out.println("客戶端斷開(kāi)連接。"); } catch (Exception e) { e.printStackTrace(); } } private static EndpointDescription chooseUserNameEndpoint(List<EndpointDescription> endpoints) { // 通常 OPC UA 服務(wù)器也支持 SecurityPolicy.None + UserName 方式 // 也可能是 Basic128Rsa15, Basic256, etc. 具體看服務(wù)端配置 for (EndpointDescription e : endpoints) { if (e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri())) { // 確保端點(diǎn)支持 UserName 類型的認(rèn)證 for (UserTokenPolicy tokenPolicy : e.getUserIdentityTokens()) { if (tokenPolicy.getTokenType() == UserTokenType.UserName) { return e; } } } } return null; } }
要點(diǎn)說(shuō)明:
- 將 IdentityProvider 切換為 new UsernameProvider("username", "password")。
- 根據(jù)服務(wù)端提供的用戶名、密碼進(jìn)行配置。
- 需要注意端點(diǎn)是否支持 UserName 類型認(rèn)證。如果端點(diǎn)僅支持 Anonymous 或 Certificate,則無(wú)法使用用戶名密碼方式。
五、證書(shū)加密方式連接
5.1 證書(shū)加密方式簡(jiǎn)介
在實(shí)際工業(yè)環(huán)境中,安全性要求更高時(shí)通常會(huì)啟用證書(shū)加密(基于 Public Key Infrastructure)。
- 每個(gè)客戶端都會(huì)持有一份證書(shū)(公鑰)和對(duì)應(yīng)的私鑰,服務(wù)器端也有自己的證書(shū)。
- 當(dāng)客戶端與服務(wù)器通信時(shí),會(huì)先驗(yàn)證雙方的證書(shū)簽名并進(jìn)行加密傳輸,從而保證安全性與完整性。
在這種方式下,服務(wù)端可能要求:
- 客戶端必須提供已經(jīng)被服務(wù)器信任(或在服務(wù)器端手動(dòng)信任)的證書(shū)。
- 采用特定的安全策略(例如
Basic256Sha256
)并通過(guò)相應(yīng)端點(diǎn)連接。
5.2 證書(shū)和私鑰獲取
- 可以通過(guò)第三方工具(例如 openssl、keytool 或 Eclipse Milo 提供的證書(shū)工具腳本)生成自簽名證書(shū)。
- 生成后的證書(shū)和私鑰,可以存儲(chǔ)在 Java KeyStore 中,或者存儲(chǔ)為
.der
、.pem
等格式并讓?xiě)?yīng)用程序讀取。
下方示例假設(shè)已經(jīng)擁有 ClientCert.der
(客戶端公鑰)和 ClientKey.der
(客戶端私鑰),并且服務(wù)器端配置了對(duì)應(yīng)的信任或信任鏈。
OPC UA訪問(wèn)證書(shū)類
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])$"); // 證書(shū)別名 private static final String CLIENT_ALIAS = "client-ai"; // 獲取私鑰的密碼 private static final char[] PASSWORD = "password".toCharArray(); // 證書(shū)對(duì)象 private X509Certificate clientCertificate; // 密鑰對(duì)對(duì)象 private KeyPair clientKeyPair; KeyStoreLoader load(Path baseDir) throws Exception { // 創(chuàng)建一個(gè)使用`PKCS12`加密標(biāo)準(zhǔn)的KeyStore。KeyStore在后面將作為讀取和生成證書(shū)的對(duì)象。 KeyStore keyStore = KeyStore.getInstance("PKCS12"); // PKCS12的加密標(biāo)準(zhǔn)的文件后綴是.pfx,其中包含了公鑰和私鑰。 // 而其他如.der等的格式只包含公鑰,私鑰在另外的文件中。 Path serverKeyStore = baseDir.resolve("example-client.pfx"); logger.info("Loading KeyStore at {}", serverKeyStore); // 如果文件不存在則創(chuàng)建.pfx證書(shū)文件。 if (!Files.exists(serverKeyStore)) { keyStore.load(null, PASSWORD); // 用2048位的RAS算法。`SelfSignedCertificateGenerator`為Milo庫(kù)的對(duì)象。 KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048); // `SelfSignedCertificateBuilder`也是Milo庫(kù)的對(duì)象,用來(lái)生成證書(shū)。 // 中間所設(shè)置的證書(shū)屬性可以自行修改。 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); } } // 創(chuàng)建證書(shū) X509Certificate certificate = builder.build(); // 設(shè)置對(duì)應(yīng)私鑰的別名,密碼,證書(shū)鏈 keyStore.setKeyEntry(CLIENT_ALIAS, keyPair.getPrivate(), PASSWORD, new X509Certificate[]{certificate}); try (OutputStream out = Files.newOutputStream(serverKeyStore)) { // 保存證書(shū)到輸出流 keyStore.store(out, PASSWORD); } } else { try (InputStream in = Files.newInputStream(serverKeyStore)) { // 如果文件存在則讀取 keyStore.load(in, PASSWORD); } } // 用密碼獲取對(duì)應(yīng)別名的私鑰。 Key serverPrivateKey = keyStore.getKey(CLIENT_ALIAS, PASSWORD); if (serverPrivateKey instanceof PrivateKey) { // 獲取對(duì)應(yīng)別名的證書(shū)對(duì)象。 clientCertificate = (X509Certificate) keyStore.getCertificate(CLIENT_ALIAS); // 獲取公鑰 PublicKey serverPublicKey = clientCertificate.getPublicKey(); // 創(chuàng)建Keypair對(duì)象。 clientKeyPair = new KeyPair(serverPublicKey, (PrivateKey) serverPrivateKey); } return this; } // 返回證書(shū) X509Certificate getClientCertificate() { return clientCertificate; } // 返回密鑰對(duì) 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 //訪問(wèn)方式 .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 -> { // 構(gòu)建一個(gè)新的 EndpointDescription(可選修改某些字段) return new EndpointDescription( url, endpoint.getServer(), endpoint.getServerCertificate(), endpoint.getSecurityMode(), // 或者強(qiáng)制改為某種模式 endpoint.getSecurityPolicyUri(), endpoint.getUserIdentityTokens(), endpoint.getTransportProfileUri(), endpoint.getSecurityLevel() ); }) .filter(e -> e.getSecurityPolicyUri().equals(securityPolicy.getUri())) .findFirst(), configBuilder -> configBuilder //訪問(wèn)方式 .setApplicationName(LocalizedText.english("datacollector-driver")) .setApplicationUri(String.format("urn:%s:opcua-client", hostName)) // 必須與證書(shū)中的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); } }
證書(shū)路徑
我們需要把服務(wù)器證書(shū)放在pki\trusted\certs
目錄下:
要點(diǎn)說(shuō)明:
- 選擇合適的安全策略(如
Basic256Sha256
)。 - 使用客戶端證書(shū)和私鑰(可以自簽名,也可以通過(guò)權(quán)威 CA 簽發(fā))。
- 服務(wù)端需信任此客戶端證書(shū)(在服務(wù)器配置中添加到信任列表)。
- 配置
CertificateManager
、CertificateValidator
以及X509IdentityProvider
。
六、常見(jiàn)問(wèn)題與注意事項(xiàng)
端點(diǎn)選擇
- 不同 OPC UA 服務(wù)器可能同時(shí)暴露多個(gè)端點(diǎn),包含不同的安全模式(Security Mode)和安全策略(Security Policy)。
- 在匿名或用戶名密碼方式時(shí),如果選擇了需要證書(shū)的端點(diǎn),就會(huì)出現(xiàn)認(rèn)證失敗或連接被拒的情況。
- 在證書(shū)加密方式時(shí),如果選擇了安全策略為 None 的端點(diǎn),則證書(shū)不會(huì)被使用,同樣也會(huì)連接異?;蛘邔?dǎo)致安全策略不匹配。
服務(wù)器信任客戶端證書(shū)
- 大多數(shù) OPC UA 服務(wù)器在默認(rèn)情況下不信任任何客戶端的證書(shū),需要在服務(wù)端管理界面或配置文件中手動(dòng)將客戶端證書(shū)加入白名單。
- 記得查看服務(wù)器日志,若提示「Untrusted Certificate」,就需要在服務(wù)器端操作信任列表。
安全策略與性能
- 加密等級(jí)越高(如 Basic256Sha256),對(duì) CPU 資源消耗越大,通信速度會(huì)相對(duì)降低,但數(shù)據(jù)安全性更強(qiáng)。
- 在測(cè)試環(huán)境或低安全需求的場(chǎng)景下可以先使用 SecurityPolicy.None ;正式項(xiàng)目上線時(shí)再切換到更高的安全策略。
兼容性
- 不同版本的 OPC UA SDK、服務(wù)器或 Java 版本之間可能存在兼容性問(wèn)題;如果連接失敗,可以嘗試升級(jí)或降低 Milo 版本、換用不同的 JDK 版本等。
- OPC UA 服務(wù)器上若啟用特定的加密算法(例如 AES-256),客戶端也需要對(duì)應(yīng)的加密套件。
斷線重連
- 工業(yè)現(xiàn)場(chǎng)環(huán)境中網(wǎng)絡(luò)抖動(dòng)常見(jiàn),客戶端需要實(shí)現(xiàn)斷線重連或重試機(jī)制,以確保數(shù)據(jù)采集的連續(xù)性與穩(wěn)定性。
到此這篇關(guān)于java連接opcua的文章就介紹到這了,更多相關(guān)java連接opcua內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
實(shí)現(xiàn)一個(gè)基于Servlet的hello world程序詳解步驟
Java Servlet 是運(yùn)行在 Web 服務(wù)器或應(yīng)用服務(wù)器上的程序,它是作為來(lái)自 Web 瀏覽器或其他 HTTP 客戶端的請(qǐng)求和 HTTP 服務(wù)器上的數(shù)據(jù)庫(kù)或應(yīng)用程序之間的中間層2022-02-02SpringBoot整合mybatis使用Druid做連接池的方式
這篇文章主要介紹了SpringBoot整合mybatis使用Druid做連接池的方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08SpringBoot調(diào)用公共模塊的自定義注解失效的解決
這篇文章主要介紹了SpringBoot調(diào)用公共模塊的自定義注解失效的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02Java 位運(yùn)算符>>與>>>區(qū)別案例詳解
這篇文章主要介紹了Java 位運(yùn)算符>>與>>>區(qū)別案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08SpringBoot項(xiàng)目部署到服務(wù)器的兩種方式
目前,前后端分離的架構(gòu)已成主流,而使用SpringBoot構(gòu)建Web應(yīng)用是非??焖俚?項(xiàng)目發(fā)布到服務(wù)器上的時(shí)候,只需要打成一個(gè)jar包,然后通過(guò)命令 : java -jar jar包名稱即可啟動(dòng)服務(wù)了,本文介紹了SpringBoot項(xiàng)目部署到服務(wù)器的兩種方式,需要的朋友可以參考下2024-10-10使用原生JDBC動(dòng)態(tài)解析并獲取表格列名和數(shù)據(jù)的方法
這篇文章主要介紹了使用原生JDBC動(dòng)態(tài)解析并獲取表格列名和數(shù)據(jù),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-08-08