搞Java的人第一次去连OPC Server,十有八九会撞上这个异常:
org.jinterop.dcom.common.JIException: Access is denied它出现的时机通常都在创建DCOM会话、调用CoCreateInstanceEx 那一步,也就是程序刚尝试连接远程OPC Server,或者还在网络协商阶段就抛出来了。明明代码是照着教程抄的,用户名、密码也都填了,可它就是要拒绝你。尤其当你在Windows防火墙已经关闭、网络能ping通的情况下还看到这个错误,那种挫败感我太熟悉了。
这篇文章我从问题根因说起,把DCOM这套老旧的访问控制机制拆开讲清楚,再给出dcomcnfg的详细配置步骤、Java侧连接代码的注意点、以及一套能直接落地的排查顺序。适合正在用Utgard、j-Interop这类库对接OPC DA的程序员,也适合第一次接触OPC集成、被Windows权限折腾到怀疑人生的新手。如果你是做OPC UA的,可以关掉了,这个异常基本只在OPC DA + DCOM这条老链路上出现。
1. Access is denied 到底是谁在拒绝你
1.1 整条技术链:Java 是怎么够到 OPC 的
Java本身跑在JVM里,而OPC DA(Data Access)是构建在Windows COM/DCOM之上的老协议。两端语言都不一样,中间必须有一座桥。实际项目中最常见的链路是:
Java应用 → OpenSCADA Utgard → j-Interop → DCOM网络协议 → 远程Windows机器上的OPC ServerUtgard是Java生态里比较老牌的开源OPC库,它封装了OPC DA 2.0/3.0的客户端逻辑;底层依赖j-Interop(也就是你报错信息里那个org.jinterop包),用纯Java重新实现了DCOM协议,所以客户端机器上不需要安装任何Windows COM组件或DLL。j-Interop的任务,就是在Java和远程Windows之间模拟出一个DCOM客户端来。
整个流程大致是这样:你的Java程序先通过j-Interop建立一个JISession会话,然后向远程机器的RPC Endpoint Mapper(也就是135端口)发起请求,申请创建一个OPC Server组件的实例。创建成功后,再通过DCOM调用OPC Server暴露出来的接口去读写点位数据。
注意“创建实例”这步很关键。一个OPC Server在Windows里是一个COM组件,你想远程使用它,不光要网络通、账户对,还要Windows的DCOM授权机制点头。任何一个环节不放行,结果就是一个错误码:
0x80070005 E_ACCESSDENIEDj-Interop收到这个错误,就包装成你看到的JIException: Access is denied。
1.2 它不是登录失败,而是“被门禁拦住了”
很多人把Access is denied理解为用户名或密码错误,这个理解方向不完全对。它更像是一个门禁系统:你到了楼下,有门禁卡(用户名密码),但系统没有给这张卡开放“进入这栋楼”的权限,于是门就是不打开。
DCOM的权限体系里至少有三道门:
- 访问权限(Access Permissions):决定谁能远程调用这个COM组件的方法。
- 启动和激活权限(Launch and Activation Permissions):决定谁能远程创建、激活这个COM组件的实例。
- 身份验证级别和模拟级别:决定身份是以什么安全级别在会话中传递,以及能否模拟客户端身份去访问其他资源。
你代码里填的用户名密码只是让DCOM知道“你是谁”,但实际问题往往是“你是谁”这个答案本身没有出现在允许列表里。所以光靠修Java代码解决不了,要去Windows那一侧给这个用户授权。这也是为什么网上搜这个错误,几乎所有人最后都指向同一个操作:打开dcomcnfg 改权限。
2. DCOM 权限模型里,最容易踩的坑在哪
2.1 “编辑默认值”和“编辑限制”是两回事
打开组件服务之后,在“我的电脑”属性里点进COM安全,你会看到两组设置:访问权限、启动和激活权限,每组下面又有“编辑默认值”和“编辑限制”两个按钮。
这两者的区别很多人搞不清。默认值是对没有单独配置安全属性的组件生效的统一授权;限制则是一个强制底线,优先级高于任何组件自己的配置。换句话说,哪怕你在某个具体组件的属性里给了用户“允许”,如果“编辑限制”里没有给,最终结果还是拒绝。
很多人改完默认值发现没用,就是因为没动限制:
- 访问权限的两处:默认值和限制,都要添加用户,并且勾选“远程访问”。
- 启动和激活权限的两处:默认值和限制,都要添加用户,并且勾选“远程启动”和“远程激活”。
我第一次配置时也踩过这个坑,只改了默认值,重启服务后连接还是Access is denied,后来把限制也改了,问题消失。所以配置时要记住:两侧同步,缺一不可。
2.2 用户选谁、加哪些账户,是有讲究的
在“编辑默认值”和“编辑限制”里需要添加哪些账户,取决于你的实际部署环境。常规做法是添加三类:
- Everyone:这一项能保证授权覆盖范围最大,适合测试环境快速验证。
- ANONYMOUS LOGON:用于匿名远程激活,如果局域网里没有域环境、工作组模式下非常常见。
- 实际运行j-Interop的用户账户:比如你代码里填的
ci.user,或者目标机器上的本地管理员账户。
要注意的是,某些Windows安全策略默认禁止匿名枚举、禁止匿名访问某些对象。如果你选择了添加ANONYMOUS LOGON,还需要确认本地安全策略里“网络安全:允许对SAM账户和共享的匿名枚举”这类选项没有被强行锁死。否则即使给了匿名登录权限,系统也可能不认。
在一个没有域控的工作组环境里,我建议直接用目标机上已有的管理员账户,把“Everyone”和“ANONYMOUS LOGON”都先加上,把环境跑通后再按最小权限原则收紧。生产环境千万别一上来就追求“最小权限”,否则你会分不清到底是权限不够还是配置没生效,排查周期会拉得很长。
2.3 网络访问模型和“拒绝从网络访问”的坑
Windows本地安全策略里有一个位置非常隐蔽,但它能让你所有权限配置都白费。在“本地策略→用户权限分配”下面,有两项:
- 从网络访问此计算机
- 拒绝从网络访问此计算机
默认情况下,普通管理员账号是允许从网络访问的。但在某些精简版系统、或者被安全加固过的服务器上,“拒绝从网络访问这台计算机”里可能会带着Guest或者其它账户组。如果你Java连接代码用的用户正好被放在了这个拒绝列表里,那DCOM请求过来时,系统会直接拒绝,日志里也像Access is denied。
另一个坑是“网络访问:本地账户的共享和安全模型”。在Windows 10/Server 2016之后,这个策略有两个选项:
- 仅来宾:所有网络用户都映射为Guest账户,这种模式下即使你填了管理员密码,远程创建DCOM实例也极大概率失败。
- 经典:本地用户以自身身份验证,这才是DCOM远程访问通常需要的模式。
如果你的机器是刚装好的Windows系统,先确认这两处策略没有拖后腿,再继续往下配置。
3. dcomcnfg 完整配置步骤,照着做就行
3.1 先确认 OPC Server 已经以 DCOM 方式注册
权限配置的前提是这个OPC Server组件已经被正确安装并注册到Windows的COM注册表里。如果你用的是KepServerEX、Matrikon OPC Simulation,或者西门子、罗克韦尔等厂家的OPC Server,安装完成后通常会自动注册。
可以用两种方式确认:
- 打开dcomcnfg → 组件服务 → 计算机 → 我的电脑 → DCOM配置,在列表里找有没有对应的OPC Server项。名字一般类似
KEPware.KEPServerEX.V6、Matrikon.OPC.Simulation.1这类,或者带有OPC关键字。 - 在注册表里搜ProgId字符串:开始菜单搜索
regedit,在HKEY_CLASSES_ROOT下搜索你OPC Server的ProgId名称,能找到对应的CLSID。
如果DCOM配置列表里看不到OPC Server,常见原因是OPC Core Components没装,或者某些绿色精简版OPC Server没有注册过COM。装一次OPC Core Components Redistributable 3.00.107,问题往往就解决了。
3.2 逐项配置 COM 安全,这里是最核心的一步
我以Windows Server 2019 / Windows 10为例,完整步骤是这样:
- 按
Win + R,输入dcomcnfg,回车,打开组件服务。 - 左侧导航:组件服务 → 计算机 → 我的电脑。
- 右键“我的电脑”,选择“属性”。
- 在“默认属性”页签里,确认:
- “在此计算机上启用分布式COM”是勾选状态。
- 默认身份验证级别选择“连接”。
- 默认模拟级别选择“标识”。
- 在“默认协议”页签里,确认有“面向连接的TCP/IP”,如果没有就添加。
- 切到“COM安全”页签,开始正式配置。
访问权限的配置:
- 点击“访问权限”区域里的“编辑默认值”,添加以下账户,并勾选“远程访问”:
- Everyone
- ANONYMOUS LOGON
- 你实际连接用的Windows账户
- 再点击“编辑限制”,做同样的添加和“远程访问”勾选。
- 确定保存。
启动和激活权限的配置:
- 点击“启动和激活权限”区域里的“编辑默认值”,添加同样几个账户,并勾选“远程启动”和“远程激活”。
- 再点击“编辑限制”,重复同样的操作。
- 确定保存。
配置完“我的电脑”这层,再去DCOM配置列表里找到你的OPC Server项:
- 右键组件,选择“属性”。
- 在“身份”页签里,选择“交互式用户”。有些服务型OPC Server会建议“指定用户”并填入高权限账户,这个可以按OPC Server的产品文档来。
- 在“安全”页签里,如果你希望针对这个组件单独设置权限,可以选择“自定义”,然后继续添加账户授权。实际上只要“我的电脑”层的权限都放开了,这里通常保持默认即可。
全部改完后,最好重启一次系统,或者至少重启OPC Server服务。DCOM权限的生效依赖系统服务重新加载,完全不用重启的情况也不是没有,但重启后确认最省心。
3.3 防火墙也要管:不只是放行 135 端口
DCOM远程调用的第一步是访问TCP 135端口。但这个端口只负责“牵线”,真正传输调用数据的端口是从系统动态端口范围里随机分配的,通常是高速TCP端口。所以只放行135端口是不够的,否则会出现“一开始好像连上了,随后又超时或拒绝”的诡异现象。
我通常用两条防火墙规则解决:
# 放行 RPC Endpoint Mapper netsh advfirewall firewall add rule name="DCOM TCP 135" dir=in action=allow protocol=TCP localport=135 # 放行动态 RPC 端口范围(根据系统版本动态端口可能不同,一般是 49152-65535) netsh advfirewall firewall add rule name="DCOM TCP Dynamic" dir=in action=allow protocol=TCP localport=49152-65535在测试环境里,我会先直接关闭Windows防火墙来确认问题是否出在防火墙侧:
# 临时关闭防火墙(仅测试环境) netsh advfirewall set allprofiles state off如果关了防火墙就通了,那基本可以确定是端口放行不完整。
顺带一提,某些网络环境里UDP 135、UDP 137-138、TCP/UDP 445也会被DCOM相关的发现、浏览机制用到。严格来说,只要OPC Server地址和CLSID是明确的,走TCP 135 + 动态端口就够了,但如果你用主机名做host,NetBIOS和DNS解析就可能牵涉445端口。为了快速跑通,测试环境把防火墙全部关闭验证一次是最高效的办法。
4. Java 连接代码与 j-Interop 侧的注意事项
4.1 一个能跑的连接示例
依赖方面,如果你用Maven,核心依赖如下:
<dependency> <groupId>org.openscada.utgard</groupId> <artifactId>org.openscada.utgard</artifactId> <version>1.5.0</version> </dependency> <dependency> <groupId>org.jinterop</groupId> <artifactId>org.jinterop</artifactId> <version>2.4.7</version> </dependency>如果你的公司私服拉不到这些旧依赖,去Maven Central直接搜索utgard和org.jinterop也能找到对应坐标。正因为这些库维护得不活跃,我强烈建议锁定版本,不要盲目升到最新,否则API可能有变化。
连接代码的骨架大致是这样:
import org.openscada.opc.lib.da.ConnectionInformation; import org.openscada.opc.lib.da.OPCServer; public class OpcConnectionTest { public static void main(String[] args) { // 有些容器环境需要显式设置上下文ClassLoader,否则j-Interop反射时会出问题 Thread.currentThread().setContextClassLoader( OpcConnectionTest.class.getClassLoader() ); ConnectionInformation ci = new ConnectionInformation(); ci.setHost("192.168.1.100"); // OPC Server机器IP ci.setDomain("WORKGROUP"); // 域;工作组就填WORKGROUP或机器名 ci.setUser("administrator"); // 有DCOM远程权限的账户 ci.setPassword("your_password"); ci.setClsid("F8582CF2-88FB-11D0-B850-00A0C922A001"); // OPC Server组件的CLSID // 或者用ProgId,二选一: // ci.setProgId("KEPware.KEPServerEX.V6"); OPCServer server = new OPCServer(); try { server.connect(ci); System.out.println("连接成功,当前OPC Server: " + server.getServerState()); // 后续就是获取group、添加item、读取值 // ... } catch (Exception e) { e.printStackTrace(); } finally { try { server.disconnect(); } catch (Exception e) { // 忽略断开异常 } } } }我建议测试时先用clsid连接,而不是用progId,因为progId解析额外依赖OPCEnum服务,如果OPC Core Components没装好,解析会失败。CLSID需要从OPC Server的产品文档或注册表里查。KepServer的CLSID不同版本不一样,不要直接抄网上的。
4.2 两个经常被忽略的 Java 侧问题
第一个是ClassLoader。j-Interop内部某些类加载操作走的是线程上下文ClassLoader。如果你把连接代码写在Tomcat、Spring Boot或者其他类加载器隔离比较严格的容器里,偶尔会报奇怪的ClassNotFoundException,甚至表现为连接建立到一半就失败。提前加一行Thread.currentThread().setContextClassLoader(...),能省很多排查时间。
第二个是会话生命周期。每次连接都应该使用独立的JISession,用完必须销毁。如果不销毁,Windows端会积累大量来自同一客户机的DCOM会话,一段时间后新连接会被Windows拒绝,表现仍是Access is denied。很多人查了半天权限,结果发现是之前的测试会话没清理干净。
在Windows端看到会话堆积的办法:目标机器打开“计算机管理 → 系统工具 → 共享文件夹 → 会话”,如果看到大量来自你本机的网络连接,基本就是会话没释放。
另外,JDK版本要特别注意。j-Interop是很多年前的库,在JDK 8上运行最稳定。我遇到过在JDK 11及以上版本调用时出现模块化相关的IllegalAccessError,这不是业务代码问题,而是JDK强封装导致的反射受限。如果你必须用JDK 11+,可以尝试加上:
--add-opens java.base/java.lang=ALL-UNNAMED但坦白讲,这只能缓解一部分问题,最稳妥的还是JDK 8。
5. 一套实用的排障路线,五分钟定位问题
5.1 排查顺序:先剥开 Java,再处理 Windows
遇到Access is denied,我建议不要急着改代码,先按下面的顺序排查:
- 用现成的OPC客户端工具验证。在目标机器或同网络的另一台Windows机器上装一个Matrikon OPC Explorer,尝试远程连接同一个OPC Server。如果OPC Explorer也连不上,那问题百分百不在Java侧,而是DCOM、防火墙或OPC Server本身的问题。这个步骤能直接帮你把方向定下来。
- 在目标Windows机器上打开事件查看器,看“应用程序”日志里有没有DCOM相关报错。最重要的两个事件ID是10005和10010,它们会明确告诉你:哪个用户尝试激活哪个CLSID的组件、因为什么权限失败。看到CLSID后,再去注册表确认是不是你的OPC Server。
- 检查防火墙端口。先看135端口通不通,再确认动态端口范围。实在不行测试环境临时关防火墙验证。
- 最后再看Java代码:用户、域、CLSID是否和Windows那边一致,ClassLoader是否设置,会话是否正常销毁。
这个顺序能避免你在Java代码里反复折腾,最后发现是Windows权限没给。
5.2 常见错误与解决方法速查
| 错误信息 | 可能原因 | 处理方式 |
|---|---|---|
JIException: Access is denied | DCOM访问权限或启动激活权限不足 | dcomcnfg添加用户并勾选远程访问、远程启动、远程激活 |
JIException: Access is denied且固定发生在运行一段时间后 | 会话没释放,Windows端DCOM会话堆积 | 确保每次连接后disconnect,必要时重启OPC Server服务 |
0x800706ba: The RPC server is unavailable | 135端口不通、防火墙拦截、目标机器远程服务不可用 | 检查135端口和动态RPC端口放行 |
0x80040154: Class not registered | CLSID错误、OPC Core Components缺失、OPC Server未注册 | 核对CLSID,安装OPC Core Components,重新注册OPC Server |
0x80070002: The system cannot find the file specified | ProgId解析失败或注册信息损坏 | 改用CLSID连接,或修复OPC Server安装 |
ClassNotFoundException | 依赖缺失或上下文ClassLoader不对 | 补全依赖,设置Thread.currentThread().setContextClassLoader |
这张表我是在项目里整理出来的。以前接手过一台很老的工控机,对方说Java连不上OPC,我远程一看,和代码一点关系没有,纯粹是OPC Server那个Windows账户被人从“从网络访问此计算机”里移掉了,加上事件日志里10010一直刷。把用户加回允许列表,立刻就好。
6. 最后再分享一点实际操作中的体会
DCOM这套东西设计于上世纪九十年代,安全模型复杂,调试手段又少,和现代化的API风格完全不是一个时代的产品。我的体会是,遇到Access is denied,心态上要接受“大概率是Windows权限问题”,而不是一直在Java代码里钻牛角尖。
有几个小技巧对排查很有帮助:第一次配置时可以把Everyone和ANONYMOUS LOGON都加上,跑通后再按最小权限原则收缩到指定用户;配置完dcomcnfg后不要舍不得重启,重启一次能排除配置未生效的干扰;连接代码里把日志级别调到DEBUG,如果j-Interop能输出更详细的DCOM握手日志,很多报错原因会更早暴露出来。
另外,如果你的项目是从零开始、OPC可是可选的,我的建议很直接:上OPC UA。OPC UA摆脱了DCOM依赖,Java端有Eclipse Milo这种维护活跃、文档齐全的库,连接配置比OPC DA简单太多,也不用跟Windows权限纠缠。老一辈的OPC DA设备没法升级时,才需要和DCOM硬碰硬。这篇配置方案也是给这些“不得不兼容老设备”的场景兜底用的,希望它能让你少熬几个夜。