基于github.com/awcullen/opcua/server [Go]实现OPC UA服务器用户角色与细粒度访问控制的技术问询
基于github.com/awcullen/opcua/server [Go]实现OPC UA服务器用户角色与细粒度访问控制的技术问询
嘿,看起来你已经在awcullen的OPC UA服务器库上走得挺顺了,基础的节点增删改查都搞定,现在要搞角色权限控制对吧?我来帮你梳理下可行的方案和具体实现思路,完全贴合你的需求:
先直接回答你的三个问题
- 这种基于角色的访问控制完全可行:这个库遵循OPC UA规范,支持通过节点的
RolePermissions配置+会话请求拦截的方式实现细粒度权限控制。 - 角色可以绑定到用户会话/认证方式:你已经在认证函数里存储了用户会话上下文,刚好可以用来关联每个会话的用户角色,不同认证方式(比如用户名密码、证书)也可以对应不同角色逻辑。
- 可以针对特定节点限制读写权限:结合节点的权限配置和请求拦截器,能精准控制不同角色对指定命名空间、指定节点的读写订阅权限。
具体实现步骤(结合你的现有代码)
1. 完善用户角色与会话上下文定义
先把角色和用户会话的关联逻辑补全,替换你现有的UserContext:
import "sync" type UserRole string const ( RoleAdmin UserRole = "admin" RoleUser1 UserRole = "user1" RoleUser2 UserRole = "user2" ) type UserContext struct { Username string Role UserRole } // 全局变量做好并发安全 var ( userMutex sync.Mutex currentUser map[string]string userSessions map[string]*UserContext ) // 初始化全局变量 func init() { currentUser = make(map[string]string) userSessions = make(map[string]*UserContext) }
然后在你的认证函数里,给每个登录用户分配对应角色:
// 替换原来的UserContext初始化代码 sessionID := fmt.Sprintf("%s_%s", userIdentity.UserName, applicationURI) userSessions[sessionID] = &UserContext{ Username: userIdentity.UserName, Role: func() UserRole { switch userIdentity.UserName { case "admin": return RoleAdmin case "user1": return RoleUser1 case "user2": return RoleUser2 default: return "" // 未知角色默认拒绝访问 } }(), }
2. 给节点配置角色权限
实现你代码里调用的getUserRolePermissions函数,根据节点所在命名空间和可写性,给不同角色分配权限:
func getUserRolePermissions(nodeNamespace string, isWritable bool) []ua.RolePermissionType { var permissions []ua.RolePermissionType // 管理员角色:拥有所有权限(用OPC UA内置的管理员角色节点ID) adminPerm := ua.RolePermissionType{ RoleID: ua.NodeIDNumeric{NamespaceIndex: 0, ID: ua.ObjectIDAdministratorRole}, Permissions: ua.PermissionTypeBrowse | ua.PermissionTypeRead | ua.PermissionTypeWrite | ua.PermissionTypeSubscribe, } permissions = append(permissions, adminPerm) // 根据节点命名空间配置其他角色权限 switch nodeNamespace { case "namespace1": // user1:只读权限 user1Perm := ua.RolePermissionType{ RoleID: ua.NodeIDString{NamespaceIndex: 0, ID: "user1"}, Permissions: ua.PermissionTypeRead, } permissions = append(permissions, user1Perm) // user2:只读权限 user2Perm := ua.RolePermissionType{ RoleID: ua.NodeIDString{NamespaceIndex: 0, ID: "user2"}, Permissions: ua.PermissionTypeRead, } permissions = append(permissions, user2Perm) case "namespace2": if isWritable { // user1:可读可写 user1Perm := ua.RolePermissionType{ RoleID: ua.NodeIDString{NamespaceIndex: 0, ID: "user1"}, Permissions: ua.PermissionTypeRead | ua.PermissionTypeWrite, } permissions = append(permissions, user1Perm) } // user2:只读(符合需求:不能写任何节点) user2Perm := ua.RolePermissionType{ RoleID: ua.NodeIDString{NamespaceIndex: 0, ID: "user2"}, Permissions: ua.PermissionTypeRead, } permissions = append(permissions, user2Perm) } return permissions }
然后在创建变量节点时,把这个权限传入:
variableNode := server.NewVariableNode( srv, nodeID, ua.QualifiedName{NamespaceIndex: nsi, Name: nodeConfig.BrowseName}, ua.LocalizedText{Text: nodeConfig.BrowseName}, ua.LocalizedText{Text: fmt.Sprintf("Variable %s of type %s", nodeConfig.BrowseName, nodeConfig.DataType)}, getUserRolePermissions(nodeConfig.Namespace, nodeConfig.IsWritable), // 传入命名空间和可写性 []ua.Reference{ { ReferenceTypeID: ua.ReferenceTypeIDOrganizes, IsInverse: true, TargetID: ua.ExpandedNodeID{NodeID: ua.ObjectIDObjectsFolder}, }, // 关联到对应的命名空间文件夹 { ReferenceTypeID: ua.ReferenceTypeIDOrganizes, IsInverse: true, TargetID: ua.ExpandedNodeID{NodeID: namespaceFolders[nodeConfig.Namespace].NodeID()}, }, }, 0, )
3. 注册自定义角色节点到地址空间
OPC UA要求RolePermissions里的RoleID必须是地址空间中存在的节点,所以要把自定义的user1、user2角色节点添加到服务器:
func addCustomRoles(srv *server.Server) error { nm := srv.NamespaceManager() nsi := uint16(0) // 用OPC UA默认命名空间 // 创建user1角色节点 user1RoleID := ua.NodeIDString{NamespaceIndex: nsi, ID: "user1"} user1Role := server.NewObjectNode( srv, user1RoleID, ua.QualifiedName{NamespaceIndex: nsi, Name: "user1Role"}, ua.LocalizedText{Text: "User1 Role"}, ua.LocalizedText{Text: "Role with read access to namespace1 and write access to namespace2"}, []ua.RolePermissionType{}, // 角色节点本身只允许管理员访问 []ua.Reference{ { ReferenceTypeID: ua.ReferenceTypeIDOrganizes, IsInverse: true, TargetID: ua.ExpandedNodeID{NodeID: ua.ObjectIDRolesFolder}, }, { ReferenceTypeID: ua.ReferenceTypeIDHasSubtype, IsInverse: false, TargetID: ua.ExpandedNodeID{NodeID: ua.ObjectIDAbstractRole}, }, }, 0, ) if err := srv.AddNode(user1Role); err != nil { return err } // 创建user2角色节点 user2RoleID := ua.NodeIDString{NamespaceIndex: nsi, ID: "user2"} user2Role := server.NewObjectNode( srv, user2RoleID, ua.QualifiedName{NamespaceIndex: nsi, Name: "user2Role"}, ua.LocalizedText{Text: "User2 Role"}, ua.LocalizedText{Text: "Role with read-only access to namespace1 and namespace2"}, []ua.RolePermissionType{}, []ua.Reference{ { ReferenceTypeID: ua.ReferenceTypeIDOrganizes, IsInverse: true, TargetID: ua.ExpandedNodeID{NodeID: ua.ObjectIDRolesFolder}, }, { ReferenceTypeID: ua.ReferenceTypeIDHasSubtype, IsInverse: false, TargetID: ua.ExpandedNodeID{NodeID: ua.ObjectIDAbstractRole}, }, }, 0, ) return srv.AddNode(user2Role) }
在创建服务器之后调用这个函数:
srv, err := server.New(description, "CertFile", "KeyFile", endpointURL, opts...) if err != nil { return nil, err } // 添加自定义角色节点 if err := addCustomRoles(srv); err != nil { return nil, fmt.Errorf("failed to add custom roles: %v", err) }
4. 设置请求拦截器做细粒度权限校验
光靠节点的权限配置还不够,需要在请求层面拦截校验,确保每个会话的操作符合角色权限:
拦截Read请求
opts = append(opts, server.WithReadRequestFunc(func(req *ua.ReadRequest, ctx *server.ServiceContext) (*ua.ReadResponse, error) { // 获取当前会话的用户上下文 userIdentity, ok := ctx.Session.UserIdentity.(ua.UserNameIdentity) if !ok { return nil, ua.BadUserAccessDenied } sessionID := fmt.Sprintf("%s_%s", userIdentity.UserName, ctx.Session.ApplicationURI) userCtx, exists := userSessions[sessionID] if !exists { return nil, ua.BadUserAccessDenied } // 检查每个要读取的节点权限 for _, nodeToRead := range req.NodesToRead { node, err := srv.Node(nodeToRead.NodeID) if err != nil { continue } nodePerms := node.RolePermissions() hasPermission := false for _, perm := range nodePerms { // 匹配用户角色和权限里的RoleID switch roleID := perm.RoleID.(type) { case ua.NodeIDString: if string(userCtx.Role) == roleID.ID && (perm.Permissions&ua.PermissionTypeRead) != 0 { hasPermission = true } case ua.NodeIDNumeric: if roleID.ID == ua.ObjectIDAdministratorRole && userCtx.Role == RoleAdmin { hasPermission = true } } if hasPermission { break } } if !hasPermission { return nil, ua.BadUserAccessDenied } } // 权限校验通过,执行默认读取逻辑 return server.DefaultReadRequestHandler(req, ctx) }))
拦截Write请求
opts = append(opts, server.WithWriteRequestFunc(func(req *ua.WriteRequest, ctx *server.ServiceContext) (*ua.WriteResponse, error) { userIdentity, ok := ctx.Session.UserIdentity.(ua.UserNameIdentity) if !ok { return nil, ua.BadUserAccessDenied } sessionID := fmt.Sprintf("%s_%s", userIdentity.UserName, ctx.Session.ApplicationURI) userCtx, exists := userSessions[sessionID] if !exists { return nil, ua.BadUserAccessDenied } // user2不能写任何节点,直接拒绝 if userCtx.Role == RoleUser2 { return nil, ua.BadUserAccessDenied } // 检查每个要写入的节点权限 for _, nodeToWrite := range req.NodesToWrite { node, err := srv.Node(nodeToWrite.NodeID) if err != nil { continue } nodePerms := node.RolePermissions() hasPermission := false for _, perm := range nodePerms { switch roleID := perm.RoleID.(type) { case ua.NodeIDString: if string(userCtx.Role) == roleID.ID && (perm.Permissions&ua.PermissionTypeWrite) != 0 { hasPermission = true } case ua.NodeIDNumeric: if roleID.ID == ua.ObjectIDAdministratorRole && userCtx.Role == RoleAdmin { hasPermission = true } } if hasPermission { break } } if !hasPermission { return nil, ua.BadUserAccessDenied } } return server.DefaultWriteRequestHandler(req, ctx) }))
拦截Subscribe请求
opts = append(opts, server.WithCreateMonitoredItemsRequestFunc(func(req *ua.CreateMonitoredItemsRequest, ctx *server.ServiceContext) (*ua.CreateMonitoredItemsResponse, error) { userIdentity, ok := ctx.Session.UserIdentity.(ua.UserNameIdentity) if !ok { return nil, ua.BadUserAccessDenied } sessionID := fmt.Sprintf("%s_%s", userIdentity.UserName, ctx.Session.ApplicationURI) userCtx, exists := userSessions[sessionID] if !exists { return nil, ua.BadUserAccessDenied } // 管理员可以订阅所有节点,直接放行 if userCtx.Role == RoleAdmin { return server.DefaultCreateMonitoredItemsRequestHandler(req, ctx) } // 其他用户检查订阅节点权限 for _, item := range req.ItemsToCreate { node, err := srv.Node(item.ItemToMonitor.NodeID) if err != nil { continue } nodePerms := node.RolePermissions() hasPermission := false for _, perm := range nodePerms { switch roleID := perm.RoleID.(type) { case ua.NodeIDString: if string(userCtx.Role) == roleID.ID && (perm.Permissions&ua.PermissionTypeSubscribe) != 0 { hasPermission = true } } if hasPermission { break } } if !hasPermission { return nil, ua.BadUserAccessDenied } } return server.DefaultCreateMonitoredItemsRequestHandler(req, ctx) }))
最后补充几个注意点
- 确保你的
nodeConfig结构体里包含IsWritable字段,用来标识节点是否可写 - 并发安全:所有访问
userSessions的地方都要加userMutex锁,避免竞态问题 - 错误处理:可以根据不同场景返回更具体的OPC UA错误码,比如
BadNotWritable、BadPermissionDenied等
内容来源于stack exchange
相关产品推荐
相关产品推荐

