云
2 天以前 95ae0c83d2aa58d86b4039e3f7f16ee93afb8f00
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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<String> operations, String permission) {
        return issue(taskId, moduleId, attachmentId == null ? Set.of() : Set.of(attachmentId),
                operations, permission);
    }
 
    public AgentDelegationToken issue(String taskId, String moduleId, Set<Long> attachmentIds,
                                      Set<String> 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);
        }
    }
}