Skip to content

权限控制

Xiuxian 模块实现了基于角色的工具访问控制,确保 AI 只能执行用户权限范围内的操作。

身份体系

XiuxianRole 枚举

java
public enum XiuxianRole {

    /**
     * 宗主 - 最高权限
     */
    SECT_MASTER("宗主", "宗门之主,拥有最高权限,可管理全宗门事务", "P001"),

    /**
     * 峰主 - 峰管理权限
     */
    PEAK_MASTER("峰主", "一峰之主,可管理本峰弟子,查看本峰数据", "P004"),

    /**
     * 弟子 - 基础权限
     */
    DISCIPLE("弟子", "普通弟子,只能查看和操作自己的信息", "");

    private final String name;           // 显示名称
    private final String description;    // 身份说明
    private final String positionId;     // 职位 ID 映射
}

权限层级

权限控制方式

1. 注解方式

使用 @RequiresRole 注解标记工具所需权限:

java
@Tool(description = "查询修士详细信息(含敏感信息)")
@RequiresRole(value = XiuxianRole.PEAK_MASTER, message = "查看他人敏感信息需要峰主及以上权限")
public String queryUserDetail(String userName) {
    // ...
}

2. 注解定义

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRole {
    XiuxianRole value();          // 所需角色
    String message() default "权限不足";  // 权限不足提示
}

3. 权限提取

在工具注册时提取权限要求:

java
private XiuxianRole extractRequiredRole(ToolCallback callback, List<Object> domainTools) {
    String toolName = callback.getToolDefinition().name();

    for (Object toolBean : domainTools) {
        for (Method method : toolBean.getClass().getDeclaredMethods()) {
            Tool toolAnnotation = method.getAnnotation(Tool.class);
            if (toolAnnotation != null && toolAnnotation.name().equals(toolName)) {
                RequiresRole roleAnnotation = method.getAnnotation(RequiresRole.class);
                if (roleAnnotation != null) {
                    return roleAnnotation.value();
                }
                return XiuxianRole.DISCIPLE;  // 默认最低权限
            }
        }
    }
    return XiuxianRole.DISCIPLE;
}

权限检查流程

1. 工具列表过滤

根据用户身份过滤可用工具:

java
public String getToolDescriptions(XiuxianRole role) {
    StringBuilder sb = new StringBuilder();
    toolDefinitions.values().stream()
            .sorted(Comparator.comparing(ToolDefinition::name))
            .forEach(def -> {
                String toolName = def.name();
                XiuxianRole requiredRole = toolRoles.getOrDefault(toolName, XiuxianRole.DISCIPLE);

                // 权限检查
                if (!hasRolePermission(role, requiredRole)) {
                    return;  // 跳过无权限的工具
                }

                sb.append("- `").append(toolName).append("` - ")
                        .append(def.description());

                if (requiredRole != XiuxianRole.DISCIPLE) {
                    sb.append(" 【需要").append(requiredRole.getName()).append("权限】");
                }

                sb.append("\n");
            });
    return sb.toString();
}

2. 权限判断逻辑

java
private boolean hasRolePermission(XiuxianRole userRole, XiuxianRole requiredRole) {
    return switch (requiredRole) {
        case DISCIPLE -> true;  // 所有身份都能访问
        case PEAK_MASTER -> userRole == XiuxianRole.PEAK_MASTER
                         || userRole == XiuxianRole.SECT_MASTER;
        case SECT_MASTER -> userRole == XiuxianRole.SECT_MASTER;
    };
}

3. 执行时校验

executeTool 中再次校验权限:

java
public String executeTool(String toolName, Map<String, Object> parameters) {
    // 获取工具所需权限
    XiuxianRole requiredRole = toolRoles.get(toolName);

    // 获取当前用户权限
    AgentContext ctx = AgentContextHolder.getContext();
    XiuxianRole userRole = ctx.getRole();

    // 校验权限
    if (!hasRolePermission(userRole, requiredRole)) {
        return "权限不足:" + requiredRole.getName() + " 才能执行此操作";
    }

    // 执行工具
    ToolCallback callback = toolCallbacks.get(toolName);
    return callback.call(JSONUtils.toJsonString(parameters));
}

数据权限

峰主数据范围

峰主只能操作本峰弟子,通过 checkPeakPermission 方法检查:

java
protected String checkPeakPermission(XiuxianUserEntity user, String actionDescription) {
    AgentContext ctx = AgentContextHolder.getContext();
    if (ctx != null && ctx.getRole() == XiuxianRole.PEAK_MASTER) {
        String userPeakId = user.getPeakId();
        String ctxPeakId = ctx.getPeakId();
        if (userPeakId != null && !userPeakId.equals(ctxPeakId)) {
            return "权限不足:您只能" + actionDescription + "本峰弟子,【"
                   + user.getName() + "】属于其他峰";
        }
    }
    return null;  // null 表示通过
}

使用示例

java
@Tool(description = "调整修士灵石")
@RequiresRole(XiuxianRole.PEAK_MASTER)
public String adjustSpiritStones(String userName, Long amount) {
    XiuxianUserEntity user = resolveUserByName(userName);
    if (user == null) {
        return error("未找到修士");
    }

    // 检查数据权限
    String permissionError = checkPeakPermission(user, "调整灵石");
    if (permissionError != null) {
        return error(permissionError);
    }

    // 执行调整...
}

身份识别

从 PositionId 解析

java
public static XiuxianRole fromPositionId(String positionId) {
    if (SECT_MASTER.getPositionId().equals(positionId)) return SECT_MASTER;
    if (PEAK_MASTER.getPositionId().equals(positionId)) return PEAK_MASTER;
    return DISCIPLE;
}

构建上下文

java
public SseEmitter chatSse(String conversationId, String message, String mockUserId) {
    // 获取用户信息
    XiuxianUserEntity user = userService.getById(mockUserId);

    // 解析身份
    XiuxianRole role = XiuxianRole.fromPositionId(user.getPositionId());

    // 构建上下文
    AgentContext context = AgentContext.builder()
            .conversationId(conversationId)
            .userId(mockUserId)
            .role(role)
            .xiuxianName(user.getName())
            .peakId(user.getPeakId())
            .build();

    // ...
}

System Prompt 中的权限提示

java
private String buildUserInfo(AgentContext context, XiuxianUserEntity user) {
    XiuxianRole role = context.getRole();

    String managementScope = role == XiuxianRole.PEAK_MASTER && user.getPeakId() != null
            ? "管理范围:仅限本峰弟子\n"
            : "";

    return """
            当前用户身份:**%s**
            身份说明:%s
            修士姓名:%s
            %s
            **重要提示**:
            - 你只能执行当前身份权限范围内的操作
            - 涉及其他峰或更高权限的操作会被拒绝
            - 查询他人敏感信息(灵石余额等)需要相应权限
            """.formatted(
            role.getName(),
            role.getDescription(),
            user.getName(),
            managementScope
    );
}

权限不足的处理

工具列表中隐藏

无权限的工具不会出现在 System Prompt 的工具列表中,LLM 不知道这些工具的存在。

执行时拒绝

即使 LLM 尝试调用无权限的工具,执行时也会被拒绝:

java
if (!hasRolePermission(userRole, requiredRole)) {
    return "权限不足:需要 " + requiredRole.getName() + " 权限才能执行此操作";
}

友好提示

LLM 会用修仙世界的语气告知用户:

AI: 抱歉,查看他人敏感信息需要峰主及以上权限。您目前是弟子身份,
    只能查看自己的信息。如需查看他人信息,请联系您的峰主或宗主。

最佳实践

1. 最小权限原则

工具默认不需要权限,敏感操作才添加权限注解:

java
// 查询基本信息 - 无需权限
@Tool(description = "查询修士基本信息")
public String queryUserBasic(String userName) { ... }

// 查询详细信息 - 需要峰主权限
@Tool(description = "查询修士详细信息")
@RequiresRole(XiuxianRole.PEAK_MASTER)
public String queryUserDetail(String userName) { ... }

2. 数据权限检查

除了角色权限,还要检查数据权限:

java
@RequiresRole(XiuxianRole.PEAK_MASTER)
public String manageUser(ManageUserDto dto) {
    XiuxianUserEntity user = userService.getById(dto.getUserId());

    // 峰主只能管理本峰弟子
    String error = checkPeakPermission(user, "管理");
    if (error != null) {
        return error(error);
    }

    // 执行管理操作...
}

3. 明确的错误提示

权限不足时,告诉用户如何获取权限:

java
@RequiresRole(value = XiuxianRole.SECT_MASTER,
              message = "删除修士需要宗主权限,请联系宗主处理")
public String deleteUser(String userName) { ... }

4. 双重保障

  • System Prompt 层面:不告诉 LLM 无权限工具的存在
  • 执行层面:再次校验权限,防止绕过

下一步