package cn.iocoder.yudao.module.system.framework.agent; import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.time.LocalDateTime; import java.util.Base64; import java.util.Set; import java.util.UUID; /** 签发、校验和撤销智能体短期委托。Redis 只保存密钥摘要。 */ @Service @RequiredArgsConstructor public class SystemAgentDelegationService { private static final SecureRandom SECURE_RANDOM = new SecureRandom(); @Value("${yudao.agent.bridge.delegation-ttl-seconds:1800}") private long delegationTtlSeconds; private final AgentDelegationStore delegationStore; private final PermissionCommonApi permissionApi; public AgentDelegationToken issue(String taskId, String moduleId, Long attachmentId, Set operations, String permission) { return issue(taskId, moduleId, attachmentId == null ? Set.of() : Set.of(attachmentId), operations, permission); } public AgentDelegationToken issue(String taskId, String moduleId, Set attachmentIds, Set operations, String permission) { Long userId = SecurityFrameworkUtils.getLoginUserId(); if (userId == null) { throw new AgentBridgeException(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "未登录或登录已过期"); } if (!permissionApi.hasAnyPermissions(userId, permission)) { throw new AgentBridgeException(HttpStatus.FORBIDDEN, "FORBIDDEN", "当前用户缺少委托所需权限"); } if (taskId == null || taskId.isBlank() || taskId.length() > 128 || operations == null || operations.isEmpty() || attachmentIds == null || attachmentIds.size() > 20 || attachmentIds.stream().anyMatch(id -> id == null || id <= 0)) { throw new AgentBridgeException(HttpStatus.BAD_REQUEST, "DELEGATION_SCOPE_INVALID", "委托任务或操作范围无效"); } String id = UUID.randomUUID().toString(); byte[] secretBytes = new byte[32]; SECURE_RANDOM.nextBytes(secretBytes); String secret = Base64.getUrlEncoder().withoutPadding().encodeToString(secretBytes); String token = id + "." + secret; long ttlSeconds = Math.max(60, Math.min(delegationTtlSeconds, 3600)); LocalDateTime now = LocalDateTime.now(); AgentDelegation delegation = new AgentDelegation(); delegation.setId(id); delegation.setSecretHash(sha256(secret)); delegation.setTaskId(taskId); delegation.setUserId(userId); delegation.setModuleId(moduleId); delegation.setAttachmentId(attachmentIds.size() == 1 ? attachmentIds.iterator().next() : null); delegation.setAttachmentIds(Set.copyOf(attachmentIds)); delegation.setOperations(Set.copyOf(operations)); delegation.setPermission(permission); delegation.setIssuedAt(now); delegation.setExpiresAt(now.plusSeconds(ttlSeconds)); delegationStore.save(delegation, ttlSeconds); return new AgentDelegationToken(id, token, delegation.getExpiresAt()); } public AgentPrincipal require(String token, String taskId, String operation, String permission, String moduleId, Long attachmentId) { String[] tokenParts = token == null ? new String[0] : token.split("\\.", 2); if (tokenParts.length != 2) { throw invalidToken(); } AgentDelegation delegation = delegationStore.get(tokenParts[0]); if (delegation == null || delegation.getExpiresAt() == null || !delegation.getExpiresAt().isAfter(LocalDateTime.now()) || !constantTimeEquals(delegation.getSecretHash(), sha256(tokenParts[1]))) { throw invalidToken(); } if (!delegation.getTaskId().equals(taskId) || !delegation.getOperations().contains(operation) || !delegation.getPermission().equals(permission) || (delegation.getModuleId() != null && !delegation.getModuleId().equals(moduleId)) || !allowsAttachment(delegation, attachmentId)) { throw new AgentBridgeException(HttpStatus.FORBIDDEN, "DELEGATION_SCOPE_MISMATCH", "委托票据不允许当前操作"); } if (!permissionApi.hasAnyPermissions(delegation.getUserId(), permission)) { throw new AgentBridgeException(HttpStatus.FORBIDDEN, "DELEGATION_PERMISSION_REVOKED", "委托用户权限已变更"); } LoginUser loginUser = new LoginUser().setId(delegation.getUserId()) .setUserType(UserTypeEnum.ADMIN.getValue()) .setExpiresTime(delegation.getExpiresAt()); SecurityFrameworkUtils.setLoginUser(loginUser, ServletUtils.getRequest()); return new AgentPrincipal(delegation.getUserId(), "delegated:" + delegation.getId()); } private static boolean allowsAttachment(AgentDelegation delegation, Long attachmentId) { if (delegation.getAttachmentIds() != null && !delegation.getAttachmentIds().isEmpty()) { return attachmentId != null && delegation.getAttachmentIds().contains(attachmentId); } return delegation.getAttachmentId() == null || delegation.getAttachmentId().equals(attachmentId); } public void revoke(String delegationId, Long currentUserId) { AgentDelegation delegation = delegationStore.get(delegationId); if (delegation == null) { return; } if (!delegation.getUserId().equals(currentUserId)) { throw new AgentBridgeException(HttpStatus.FORBIDDEN, "DELEGATION_FORBIDDEN", "无权撤销该委托票据"); } delegationStore.delete(delegationId); } private static AgentBridgeException invalidToken() { return new AgentBridgeException(HttpStatus.UNAUTHORIZED, "DELEGATION_INVALID", "委托票据无效、已撤销或已过期"); } private static boolean constantTimeEquals(String left, String right) { return left != null && right != null && MessageDigest.isEqual(left.getBytes(StandardCharsets.US_ASCII), right.getBytes(StandardCharsets.US_ASCII)); } private static String sha256(String value) { try { return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") .digest(value.getBytes(StandardCharsets.UTF_8))); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException("SHA-256 unavailable", ex); } } }