《Spring AI + 大模型全栈实战》学习手册系列 · 专题八:《RAG 系统安全与权限管理:企业级数据保护方案》

专题八:《RAG 系统安全与权限管理:企业级数据保护方案》

📚 企业级 RAG 智能问答系统全栈实施指南

第八部分:安全与权限管理


第 1 章 企业级安全架构设计

1.1 RAG 系统安全威胁模型

复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                      RAG 系统安全威胁矩阵                                │
├─────────────────────────────────────────────────────────────────────────┤
│  威胁类型      │ 攻击场景                  │ 风险等级 │ 防护措施        │
├─────────────────────────────────────────────────────────────────────────┤
│  未授权访问    │ 越权查询敏感文档          │ 🔴 P0    │ RBAC + ABAC    │
│  数据泄露      │ 向量数据库被导出          │ 🔴 P0    │ 加密存储        │
│  提示词注入    │ 恶意 Prompt 绕过权限       │ 🔴 P0    │ 输入过滤        │
│  模型投毒      │ 污染训练/检索数据         │ 🟠 P1    │ 数据校验        │
│  会话劫持      │ Token 盗用                │ 🟠 P1    │ JWT + 刷新机制  │
│  审计缺失      │ 操作无法追溯              │ 🟡 P2    │ 全链路日志      │
│  DDoS 攻击     │ 大量请求耗尽资源          │ 🟡 P2    │ 限流 + 熔断     │
└─────────────────────────────────────────────────────────────────────────┘

1.2 安全架构分层设计

复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                        企业级 RAG 安全架构                               │
├─────────────────────────────────────────────────────────────────────────┤
│  L1: 接入层 → WAF + 限流 + IP 黑白名单 + SSL/TLS                         │
│     ↓                                                                   │
│  L2: 认证层 → SSO 单点登录 + JWT/OAuth2 + MFA 多因素认证                  │
│     ↓                                                                   │
│  L3: 授权层 → RBAC 角色权限 + ABAC 属性权限 + 文档级权限                  │
│     ↓                                                                   │
│  L4: 数据层 → 字段加密 + 数据脱敏 + 向量加密 + 审计日志                  │
│     ↓                                                                   │
│  L5: 模型层 → Prompt 过滤 + 输出审查 + 敏感词检测                        │
└─────────────────────────────────────────────────────────────────────────┘

1.3 安全合规要求对照表

合规标准 适用场景 核心要求 RAG 系统对应措施
等保 2.0 中国政府/国企 三级等保 身份认证、访问控制、审计日志
GDPR 欧盟用户数据 数据保护 数据脱敏、删除权、可携带权
ISO 27001 国际企业 信息安全 安全策略、风险评估、持续改进
SOC 2 SaaS 服务 信任服务 可用性、保密性、隐私保护
个人信息保护法 中国用户数据 隐私保护 最小化收集、知情同意、安全存储

第 2 章 RBAC 权限模型设计与实现

2.1 RBAC 权限模型架构

复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                         RBAC 权限模型                                    │
├─────────────────────────────────────────────────────────────────────────┤
│  用户 (User) → 角色 (Role) → 权限 (Permission) → 资源 (Resource)         │
│     ↓           ↓              ↓                ↓                        │
│   张三      部门经理      文档读取        财务部文档                      │
│   李四      普通员工      文档查询        公开文档                        │
│   王五      系统管理员    全部权限        全部资源                        │
└─────────────────────────────────────────────────────────────────────────┘

2.2 数据库表设计

sql 复制代码
-- 用户表
CREATE TABLE sys_user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    email VARCHAR(100),
    phone VARCHAR(20),
    department_id BIGINT,
    status TINYINT DEFAULT 1,  -- 1:正常 0:禁用
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_department (department_id),
    INDEX idx_status (status)
);

-- 角色表
CREATE TABLE sys_role (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    role_code VARCHAR(50) UNIQUE NOT NULL,
    role_name VARCHAR(100) NOT NULL,
    description VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_code (role_code)
);

-- 权限表
CREATE TABLE sys_permission (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    perm_code VARCHAR(100) UNIQUE NOT NULL,
    perm_name VARCHAR(100) NOT NULL,
    resource_type VARCHAR(20),  -- MENU/API/DOCUMENT
    resource_path VARCHAR(255),
    parent_id BIGINT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_code (perm_code)
);

-- 用户角色关联表
CREATE TABLE sys_user_role (
    user_id BIGINT NOT NULL,
    role_id BIGINT NOT NULL,
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES sys_user(id),
    FOREIGN KEY (role_id) REFERENCES sys_role(id)
);

-- 角色权限关联表
CREATE TABLE sys_role_permission (
    role_id BIGINT NOT NULL,
    permission_id BIGINT NOT NULL,
    PRIMARY KEY (role_id, permission_id),
    FOREIGN KEY (role_id) REFERENCES sys_role(id),
    FOREIGN KEY (permission_id) REFERENCES sys_permission(id)
);

-- 文档权限表(文档级权限控制)
CREATE TABLE doc_permission (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    doc_id VARCHAR(100) NOT NULL,
    department_id BIGINT,
    role_id BIGINT,
    user_id BIGINT,
    permission_level TINYINT NOT NULL,  -- 1:可读 2:可写 3:完全控制
    expire_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_doc_user (doc_id, user_id),
    INDEX idx_doc (doc_id),
    INDEX idx_department (department_id)
);

2.3 Spring Security + JWT 实现

java 复制代码
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    
    private final JwtAuthenticationFilter jwtAuthFilter;
    private final AuthenticationProvider authenticationProvider;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .authorizeHttpRequests(auth -> auth
                // 公开接口
                .requestMatchers("/api/auth/**", "/api/public/**").permitAll()
                // 需要认证的接口
                .requestMatchers("/api/rag/**").authenticated()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                // 其他接口需要认证
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)
            )
            .build();
    }
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("https://rag.yourdomain.com"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

// JwtAuthenticationFilter.java
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    private final JwtTokenProvider jwtTokenProvider;
    private final UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) {
        try {
            // 1. 从请求头获取 Token
            String token = extractToken(request);
            
            if (token != null && jwtTokenProvider.validateToken(token)) {
                // 2. 解析 Token 获取用户名
                String username = jwtTokenProvider.getUsername(token);
                
                // 3. 加载用户详情
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                
                // 4. 设置认证上下文
                UsernamePasswordAuthenticationToken authentication = 
                    new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                    );
                
                authentication.setDetails(
                    new WebAuthenticationDetailsSource().buildDetails(request)
                );
                
                SecurityContextHolder.getContext().setAuthentication(authentication);
                
                // 5. 记录审计日志
                AuditLogContext.setUserId(userDetails.getUsername());
                AuditLogContext.setRequestId(UUID.randomUUID().toString());
            }
        } catch (Exception e) {
            log.error("认证失败", e);
        }
        
        try {
            filterChain.doFilter(request, response);
        } finally {
            // 清理 ThreadLocal
            AuditLogContext.clear();
        }
    }
    
    private String extractToken(HttpServletRequest request) {
        String bearer = request.getHeader("Authorization");
        if (bearer != null && bearer.startsWith("Bearer ")) {
            return bearer.substring(7);
        }
        return null;
    }
}

// JwtTokenProvider.java
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
    
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    @Value("${jwt.expiration:3600000}")
    private long jwtExpiration;
    
    @Value("${jwt.refresh-expiration:604800000}")
    private long refreshExpiration;
    
    private Key getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
        return Keys.hmacShaKeyFor(keyBytes);
    }
    
    public String generateToken(UserDetails userDetails) {
        AppUser appUser = (AppUser) userDetails;
        
        return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .claim("userId", appUser.getId())
            .claim("roles", appUser.getRoles().stream()
                .map(Role::getRoleCode)
                .collect(Collectors.toList()))
            .claim("departmentId", appUser.getDepartmentId())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
            .signWith(getSigningKey(), SignatureAlgorithm.HS256)
            .compact();
    }
    
    public String generateRefreshToken(UserDetails userDetails) {
        return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + refreshExpiration))
            .signWith(getSigningKey(), SignatureAlgorithm.HS256)
            .compact();
    }
    
    public String getUsername(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(getSigningKey())
            .build()
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
    
    public boolean isTokenExpired(String token) {
        Date expiration = Jwts.parserBuilder()
            .setSigningKey(getSigningKey())
            .build()
            .parseClaimsJws(token)
            .getBody()
            .getExpiration();
        return expiration.before(new Date());
    }
}

2.4 权限注解与 AOP 实现

java 复制代码
// @RequirePermission 自定义注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequirePermission {
    String value();  // 权限码
    Logical logical() default Logical.AND;  // AND/OR
}

// PermissionAspect.java
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class PermissionAspect {
    
    private final PermissionService permissionService;
    
    @Around("@annotation(requirePermission)")
    public Object checkPermission(ProceedingJoinPoint pjp, 
                                   RequirePermission requirePermission) throws Throwable {
        // 1. 获取当前用户
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            throw new AccessDeniedException("未认证");
        }
        
        String username = authentication.getName();
        String permissionCode = requirePermission.value();
        
        // 2. 检查权限
        boolean hasPermission = permissionService.hasPermission(username, permissionCode);
        
        if (!hasPermission) {
            log.warn("用户 {} 无权访问权限 {}", username, permissionCode);
            
            // 3. 记录审计日志
            AuditLog auditLog = AuditLog.builder()
                .userId(username)
                .action("PERMISSION_DENIED")
                .resource(permissionCode)
                .status("FAILED")
                .reason("权限不足")
                .timestamp(LocalDateTime.now())
                .build();
            auditLogService.save(auditLog);
            
            throw new AccessDeniedException("权限不足");
        }
        
        // 4. 执行目标方法
        return pjp.proceed();
    }
}

// 使用示例
@RestController
@RequestMapping("/api/rag")
@RequiredArgsConstructor
public class RagController {
    
    @PostMapping("/query")
    @RequirePermission("rag:query")
    public ResponseEntity<RagResponse> query(@RequestBody ChatRequest request) {
        RagResponse response = ragService.query(request.getQuestion());
        return ResponseEntity.ok(response);
    }
    
    @PostMapping("/document/upload")
    @RequirePermission("rag:document:upload")
    public ResponseEntity<Void> uploadDocument(@RequestParam("file") MultipartFile file) {
        documentService.upload(file);
        return ResponseEntity.ok().build();
    }
    
    @DeleteMapping("/document/{docId}")
    @RequirePermission("rag:document:delete")
    public ResponseEntity<Void> deleteDocument(@PathVariable String docId) {
        documentService.delete(docId);
        return ResponseEntity.ok().build();
    }
}

第 3 章 文档级权限控制

3.1 文档权限模型

复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                       文档级权限控制模型                                 │
├─────────────────────────────────────────────────────────────────────────┤
│  文档 → 部门权限 + 角色权限 + 用户权限 + 时间有效期                       │
│                                                                        │
│  示例:                                                                 │
│  文档 A → 财务部 (可读) + 部门经理 (可写) + 张三 (完全控制) + 30 天有效    │
│  文档 B → 全公司 (可读) + 管理员 (可写) + 永久有效                       │
│  文档 C → 仅本人 (可读可写) + 7 天有效                                   │
└─────────────────────────────────────────────────────────────────────────┘

3.2 文档权限服务实现

java 复制代码
// DocumentPermissionService.java
@Service
@RequiredArgsConstructor
@Slf4j
public class DocumentPermissionService {
    
    private final DocumentPermissionRepository permissionRepository;
    private final UserService userService;
    
    /**
     * 检查用户对文档的访问权限
     */
    public boolean hasAccess(String userId, String docId, PermissionLevel requiredLevel) {
        // 1. 检查是否是文档所有者
        if (isDocumentOwner(userId, docId)) {
            return true;
        }
        
        // 2. 检查是否是系统管理员
        if (isSystemAdmin(userId)) {
            return true;
        }
        
        // 3. 检查用户级权限
        Optional<DocumentPermission> userPerm = permissionRepository
            .findByDocIdAndUserId(docId, userId);
        if (userPerm.isPresent() && !userPerm.get().isExpired() 
            && userPerm.get().getPermissionLevel() >= requiredLevel.getValue()) {
            return true;
        }
        
        // 4. 检查角色级权限
        User user = userService.findById(userId);
        for (Role role : user.getRoles()) {
            Optional<DocumentPermission> rolePerm = permissionRepository
                .findByDocIdAndRoleId(docId, role.getId());
            if (rolePerm.isPresent() && !rolePerm.get().isExpired()
                && rolePerm.get().getPermissionLevel() >= requiredLevel.getValue()) {
                return true;
            }
        }
        
        // 5. 检查部门级权限
        Optional<DocumentPermission> deptPerm = permissionRepository
            .findByDocIdAndDepartmentId(docId, user.getDepartmentId());
        if (deptPerm.isPresent() && !deptPerm.get().isExpired()
            && deptPerm.get().getPermissionLevel() >= requiredLevel.getValue()) {
            return true;
        }
        
        return false;
    }
    
    /**
     * 获取用户可访问的文档 ID 列表(用于向量检索过滤)
     */
    public List<String> getAccessibleDocIds(String userId) {
        User user = userService.findById(userId);
        Set<String> docIds = new HashSet<>();
        
        // 1. 用户级权限文档
        List<DocumentPermission> userPerms = permissionRepository.findByUserId(userId);
        userPerms.stream()
            .filter(p -> !p.isExpired())
            .forEach(p -> docIds.add(p.getDocId()));
        
        // 2. 角色级权限文档
        for (Role role : user.getRoles()) {
            List<DocumentPermission> rolePerms = permissionRepository.findByRoleId(role.getId());
            rolePerms.stream()
                .filter(p -> !p.isExpired())
                .forEach(p -> docIds.add(p.getDocId()));
        }
        
        // 3. 部门级权限文档
        List<DocumentPermission> deptPerms = permissionRepository.findByDepartmentId(user.getDepartmentId());
        deptPerms.stream()
            .filter(p -> !p.isExpired())
            .forEach(p -> docIds.add(p.getDocId()));
        
        // 4. 公开文档
        List<DocumentPermission> publicPerms = permissionRepository.findByUserId(null);
        publicPerms.stream()
            .filter(p -> !p.isExpired())
            .forEach(p -> docIds.add(p.getDocId()));
        
        return new ArrayList<>(docIds);
    }
    
    /**
     * 设置文档权限
     */
    public void setPermission(String docId, PermissionRequest request) {
        DocumentPermission permission = DocumentPermission.builder()
            .docId(docId)
            .userId(request.getUserId())
            .roleId(request.getRoleId())
            .departmentId(request.getDepartmentId())
            .permissionLevel(request.getPermissionLevel())
            .expireAt(request.getExpireAt())
            .build();
        
        permissionRepository.save(permission);
        log.info("文档权限已设置:docId={}, target={}, level={}", 
                docId, request.getTarget(), request.getPermissionLevel());
    }
}

// PermissionLevel 枚举
public enum PermissionLevel {
    READ(1, "可读"),
    WRITE(2, "可写"),
    FULL_CONTROL(3, "完全控制");
    
    private final int value;
    private final String description;
    
    PermissionLevel(int value, String description) {
        this.value = value;
        this.description = description;
    }
    
    public int getValue() { return value; }
}

// PermissionRequest DTO
@Data
@Builder
public class PermissionRequest {
    private String userId;
    private Long roleId;
    private Long departmentId;
    private PermissionLevel permissionLevel;
    private LocalDateTime expireAt;
    
    public String getTarget() {
        if (userId != null) return "USER:" + userId;
        if (roleId != null) return "ROLE:" + roleId;
        if (departmentId != null) return "DEPT:" + departmentId;
        return "PUBLIC";
    }
}

3.3 向量检索权限过滤

java 复制代码
// SecureVectorStore.java
@Service
@RequiredArgsConstructor
@Slf4j
public class SecureVectorStore {
    
    private final VectorStore vectorStore;
    private final DocumentPermissionService permissionService;
    
    /**
     * 带权限过滤的向量检索
     */
    public List<Document> similaritySearch(String userId, 
                                           String query, 
                                           int topK) {
        // 1. 获取用户可访问的文档 ID 列表
        List<String> accessibleDocIds = permissionService.getAccessibleDocIds(userId);
        
        if (accessibleDocIds.isEmpty()) {
            log.warn("用户 {} 无任何文档访问权限", userId);
            return Collections.emptyList();
        }
        
        // 2. 构建过滤条件
        SearchRequest request = SearchRequest.query(query)
            .withTopK(topK * 2)  // 多检索一些,过滤后保证 topK
            .withFilterExpression(buildFilterExpression(accessibleDocIds));
        
        // 3. 执行检索
        List<Document> results = vectorStore.similaritySearch(request);
        
        // 4. 二次权限校验(防御性编程)
        List<Document> authorizedResults = results.stream()
            .filter(doc -> permissionService.hasAccess(userId, doc.getId(), PermissionLevel.READ))
            .limit(topK)
            .collect(Collectors.toList());
        
        log.info("向量检索:用户={}, 原始结果={}, 过滤后={}", 
                userId, results.size(), authorizedResults.size());
        
        return authorizedResults;
    }
    
    /**
     * 构建 Milvus 过滤表达式
     */
    private String buildFilterExpression(List<String> docIds) {
        // Milvus 过滤表达式语法:doc_id in ["id1", "id2", ...]
        String ids = docIds.stream()
            .map(id -> "\"" + id + "\"")
            .collect(Collectors.joining(", "));
        return "doc_id in [" + ids + "]";
    }
}

3.4 文档权限管理界面

vue 复制代码
<!-- DocumentPermissionModal.vue -->
<template>
  <el-dialog v-model="visible" title="文档权限管理" width="600px">
    <el-form :model="form" label-width="100px">
      <el-form-item label="权限类型">
        <el-radio-group v-model="form.targetType">
          <el-radio label="user">用户</el-radio>
          <el-radio label="role">角色</el-radio>
          <el-radio label="department">部门</el-radio>
          <el-radio label="public">公开</el-radio>
        </el-radio-group>
      </el-form-item>
      
      <el-form-item v-if="form.targetType === 'user'" label="选择用户">
        <el-select v-model="form.userId" placeholder="请选择用户" filterable>
          <el-option
            v-for="user in users"
            :key="user.id"
            :label="user.username"
            :value="user.id"
          />
        </el-select>
      </el-form-item>
      
      <el-form-item v-if="form.targetType === 'role'" label="选择角色">
        <el-select v-model="form.roleId" placeholder="请选择角色">
          <el-option
            v-for="role in roles"
            :key="role.id"
            :label="role.roleName"
            :value="role.id"
          />
        </el-select>
      </el-form-item>
      
      <el-form-item v-if="form.targetType === 'department'" label="选择部门">
        <el-select v-model="form.departmentId" placeholder="请选择部门">
          <el-option
            v-for="dept in departments"
            :key="dept.id"
            :label="dept.name"
            :value="dept.id"
          />
        </el-select>
      </el-form-item>
      
      <el-form-item label="权限级别">
        <el-select v-model="form.permissionLevel">
          <el-option label="可读" :value="1" />
          <el-option label="可写" :value="2" />
          <el-option label="完全控制" :value="3" />
        </el-select>
      </el-form-item>
      
      <el-form-item label="有效期">
        <el-date-picker
          v-model="form.expireAt"
          type="datetime"
          placeholder="选择过期时间"
          :clearable="true"
        />
      </el-form-item>
    </el-form>
    
    <template #footer>
      <el-button @click="visible = false">取消</el-button>
      <el-button type="primary" @click="handleSubmit">确定</el-button>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'

const visible = defineModel<boolean>('visible', { default: false })
const emit = defineEmits<{ success: [] }>()

const form = reactive({
  targetType: 'user',
  userId: null,
  roleId: null,
  departmentId: null,
  permissionLevel: 1,
  expireAt: null
})

const users = ref([])
const roles = ref([])
const departments = ref([])

const handleSubmit = async () => {
  try {
    await permissionApi.setPermission(props.docId, form)
    ElMessage.success('权限设置成功')
    visible.value = false
    emit('success')
  } catch (error) {
    ElMessage.error('设置失败')
  }
}
</script>

第 4 章 数据脱敏与加密

4.1 敏感数据识别规则

java 复制代码
// SensitiveDataPattern.java
@Component
public class SensitiveDataPattern {
    
    // 身份证号
    public static final Pattern ID_CARD = Pattern.compile(
        "(^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$)"
    );
    
    // 手机号
    public static final Pattern PHONE = Pattern.compile(
        "(^1[3-9]\\d{9}$)"
    );
    
    // 邮箱
    public static final Pattern EMAIL = Pattern.compile(
        "([a-zA-Z0-9._-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+)"
    );
    
    // 银行卡号
    public static final Pattern BANK_CARD = Pattern.compile(
        "(^\\d{16,19}$)"
    );
    
    // 地址(简化)
    public static final Pattern ADDRESS = Pattern.compile(
        "(.*?[省市区县].*?[路街道].*?)"
    );
}

// SensitiveDataTypes 枚举
public enum SensitiveDataType {
    ID_CARD("身份证号", 5, 5),      // 前 5 后 5
    PHONE("手机号", 3, 4),          // 前 3 后 4
    EMAIL("邮箱", 2, 4),            // 前 2 后 4
    BANK_CARD("银行卡", 4, 4),      // 前 4 后 4
    ADDRESS("地址", 2, 2),          // 前 2 后 2
    NAME("姓名", 1, 1);             // 姓 +*+ 名
    
    private final String description;
    private final int keepPrefix;
    private final int keepSuffix;
    
    SensitiveDataType(String description, int keepPrefix, int keepSuffix) {
        this.description = description;
        this.keepPrefix = keepPrefix;
        this.keepSuffix = keepSuffix;
    }
}

4.2 数据脱敏服务

java 复制代码
// DataMaskingService.java
@Service
@Slf4j
public class DataMaskingService {
    
    /**
     * 智能脱敏(自动识别敏感数据类型)
     */
    public String mask(String text) {
        if (text == null || text.isEmpty()) {
            return text;
        }
        
        String result = text;
        
        // 身份证脱敏
        result = maskPattern(result, SensitiveDataPattern.ID_CARD, SensitiveDataType.ID_CARD);
        
        // 手机号脱敏
        result = maskPattern(result, SensitiveDataPattern.PHONE, SensitiveDataType.PHONE);
        
        // 邮箱脱敏
        result = maskPattern(result, SensitiveDataPattern.EMAIL, SensitiveDataType.EMAIL);
        
        // 银行卡脱敏
        result = maskPattern(result, SensitiveDataPattern.BANK_CARD, SensitiveDataType.BANK_CARD);
        
        return result;
    }
    
    /**
     * 按模式脱敏
     */
    private String maskPattern(String text, Pattern pattern, SensitiveDataType type) {
        Matcher matcher = pattern.matcher(text);
        StringBuffer sb = new StringBuffer();
        
        while (matcher.find()) {
            String matched = matcher.group();
            String masked = maskValue(matched, type);
            matcher.appendReplacement(sb, masked);
        }
        matcher.appendTail(sb);
        
        return sb.toString();
    }
    
    /**
     * 脱敏具体值
     */
    private String maskValue(String value, SensitiveDataType type) {
        if (value.length() <= type.keepPrefix + type.keepSuffix) {
            return "*".repeat(value.length());
        }
        
        String prefix = value.substring(0, type.keepPrefix);
        String suffix = value.substring(value.length() - type.keepSuffix);
        int maskLength = value.length() - type.keepPrefix - type.keepSuffix;
        
        return prefix + "*".repeat(maskLength) + suffix;
    }
    
    /**
     * 文档内容脱敏(用于向量存储前)
     */
    public String maskDocumentContent(String content) {
        // 可选:是否对向量库存储的内容进行脱敏
        // 方案 1:完全脱敏(安全性高,检索精度略降)
        // 方案 2:保留原始内容,检索结果脱敏(推荐)
        return content;  // 方案 2:存储原始内容
    }
    
    /**
     * 检索结果脱敏(返回给用户前)
     */
    public Document maskDocument(Document doc) {
        String maskedContent = mask(doc.getContent());
        return new Document(
            doc.getId(),
            maskedContent,
            doc.getMetadata()
        );
    }
}

// 使用示例
@RestController
@RequestMapping("/api/rag")
@RequiredArgsConstructor
public class RagController {
    
    private final SecureVectorStore vectorStore;
    private final DataMaskingService maskingService;
    
    @PostMapping("/query")
    public ResponseEntity<RagResponse> query(@RequestBody ChatRequest request) {
        String userId = SecurityContextHolder.getContext().getAuthentication().getName();
        
        // 1. 带权限的向量检索
        List<Document> docs = vectorStore.similaritySearch(userId, request.getQuestion(), 5);
        
        // 2. 检索结果脱敏
        List<Document> maskedDocs = docs.stream()
            .map(maskingService::maskDocument)
            .collect(Collectors.toList());
        
        // 3. 生成回答
        String answer = ragService.generateAnswer(request.getQuestion(), maskedDocs);
        
        RagResponse response = new RagResponse(answer, maskedDocs, false);
        return ResponseEntity.ok(response);
    }
}

4.3 字段级加密(AES-256)

java 复制代码
// FieldEncryptionAspect.java
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class FieldEncryptionAspect {
    
    private final EncryptionService encryptionService;
    
    /**
     * 加密标注字段
     */
    @Around("@annotation(EncryptField)")
    public Object encryptFields(ProceedingJoinPoint pjp, EncryptField annotation) throws Throwable {
        Object result = pjp.proceed();
        
        if (result == null) {
            return null;
        }
        
        // 递归处理返回对象
        encryptObject(result, annotation.fields());
        
        return result;
    }
    
    /**
     * 解密标注字段
     */
    @Around("@annotation(DecryptField)")
    public Object decryptFields(ProceedingJoinPoint pjp, DecryptField annotation) throws Throwable {
        Object result = pjp.proceed();
        
        if (result == null) {
            return null;
        }
        
        decryptObject(result, annotation.fields());
        
        return result;
    }
    
    private void encryptObject(Object obj, String[] fields) {
        if (obj == null) return;
        
        Class<?> clazz = obj.getClass();
        for (String fieldName : fields) {
            try {
                Field field = clazz.getDeclaredField(fieldName);
                field.setAccessible(true);
                Object value = field.get(obj);
                if (value instanceof String) {
                    String encrypted = encryptionService.encrypt((String) value);
                    field.set(obj, encrypted);
                }
            } catch (Exception e) {
                log.warn("加密字段失败:{}", fieldName, e);
            }
        }
    }
}

// EncryptionService.java
@Service
@Slf4j
public class EncryptionService {
    
    @Value("${encryption.aes-key}")
    private String aesKey;
    
    private static final String ALGORITHM = "AES/GCM/NoPadding";
    private static final int GCM_TAG_LENGTH = 128;
    private static final int GCM_IV_LENGTH = 12;
    
    /**
     * AES-256-GCM 加密
     */
    public String encrypt(String plaintext) throws Exception {
        if (plaintext == null) return null;
        
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        byte[] iv = generateSecureRandomBytes(GCM_IV_LENGTH);
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
        
        cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), spec);
        byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        
        // IV + Ciphertext 一起存储
        byte[] combined = new byte[iv.length + ciphertext.length];
        System.arraycopy(iv, 0, combined, 0, iv.length);
        System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);
        
        return Base64.getEncoder().encodeToString(combined);
    }
    
    /**
     * AES-256-GCM 解密
     */
    public String decrypt(String ciphertext) throws Exception {
        if (ciphertext == null) return null;
        
        byte[] combined = Base64.getDecoder().decode(ciphertext);
        byte[] iv = Arrays.copyOfRange(combined, 0, GCM_IV_LENGTH);
        byte[] actualCiphertext = Arrays.copyOfRange(combined, GCM_IV_LENGTH, combined.length);
        
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
        
        cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec);
        byte[] plaintext = cipher.doFinal(actualCiphertext);
        
        return new String(plaintext, StandardCharsets.UTF_8);
    }
    
    private SecretKey getSecretKey() {
        byte[] keyBytes = Decoders.BASE64.decode(aesKey);
        return new SecretKeySpec(keyBytes, "AES");
    }
    
    private byte[] generateSecureRandomBytes(int length) {
        SecureRandom secureRandom = new SecureRandom();
        byte[] bytes = new byte[length];
        secureRandom.nextBytes(bytes);
        return bytes;
    }
}

// 使用示例
@Data
public class DocumentDTO {
    private String id;
    
    @EncryptField  // 自定义注解
    private String content;
    
    private Map<String, Object> metadata;
}

第 5 章 审计日志系统

5.1 审计日志数据模型

sql 复制代码
-- 审计日志表
CREATE TABLE audit_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    request_id VARCHAR(100) NOT NULL,      -- 请求 ID(链路追踪)
    user_id VARCHAR(100) NOT NULL,          -- 用户 ID
    username VARCHAR(100),                  -- 用户名
    action VARCHAR(100) NOT NULL,           -- 操作类型
    resource VARCHAR(255),                  -- 操作资源
    resource_id VARCHAR(100),               -- 资源 ID
    method VARCHAR(20),                     -- HTTP 方法
    uri VARCHAR(500),                       -- 请求 URI
    ip_address VARCHAR(50),                 -- 客户端 IP
    user_agent VARCHAR(500),                -- 用户代理
    request_body TEXT,                      -- 请求体(脱敏)
    response_status INT,                    -- 响应状态码
    response_body TEXT,                     -- 响应体(脱敏)
    latency_ms INT,                         -- 请求耗时(毫秒)
    status VARCHAR(20),                     -- SUCCESS/FAILED
    reason VARCHAR(500),                    -- 失败原因
    risk_level TINYINT DEFAULT 0,           -- 风险等级 0-5
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user (user_id),
    INDEX idx_action (action),
    INDEX idx_resource (resource),
    INDEX idx_created (created_at),
    INDEX idx_status (status),
    INDEX idx_risk (risk_level)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 审计日志分区(按月分区)
ALTER TABLE audit_log 
PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
    PARTITION p202601 VALUES LESS THAN (202602),
    PARTITION p202602 VALUES LESS THAN (202603),
    PARTITION p202603 VALUES LESS THAN (202604),
    -- ... 更多分区
    PARTITION pmax VALUES LESS THAN MAXVALUE
);

5.2 审计日志切面

java 复制代码
// AuditLogAspect.java
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class AuditLogAspect {
    
    private final AuditLogService auditLogService;
    private final DataMaskingService maskingService;
    
    @Around("@annotation(auditLog)")
    public Object logAudit(ProceedingJoinPoint pjp, AuditLog auditLog) throws Throwable {
        long startTime = System.currentTimeMillis();
        String requestId = AuditLogContext.getRequestId();
        String userId = AuditLogContext.getUserId();
        
        HttpServletRequest request = getRequest();
        
        // 构建审计日志
        AuditLogEntry entry = AuditLogEntry.builder()
            .requestId(requestId)
            .userId(userId)
            .action(auditLog.action())
            .resource(auditLog.resource())
            .method(request != null ? request.getMethod() : null)
            .uri(request != null ? request.getRequestURI() : null)
            .ipAddress(getClientIp(request))
            .userAgent(request != null ? request.getHeader("User-Agent") : null)
            .startTime(startTime)
            .build();
        
        try {
            // 记录请求体(脱敏)
            Object[] args = pjp.getArgs();
            if (args.length > 0) {
                String requestBody = maskSensitiveData(JsonMapper.toJson(args[0]));
                entry.setRequestBody(requestBody);
            }
            
            // 执行目标方法
            Object result = pjp.proceed();
            
            // 记录响应
            long endTime = System.currentTimeMillis();
            entry.setLatencyMs((int) (endTime - startTime));
            entry.setResponseStatus(200);
            entry.setStatus("SUCCESS");
            entry.setResponseBody(maskSensitiveData(JsonMapper.toJson(result)));
            
            // 异步保存日志
            auditLogService.saveAsync(entry);
            
            return result;
            
        } catch (Exception e) {
            long endTime = System.currentTimeMillis();
            entry.setLatencyMs((int) (endTime - startTime));
            entry.setResponseStatus(500);
            entry.setStatus("FAILED");
            entry.setReason(e.getMessage());
            
            // 异步保存日志
            auditLogService.saveAsync(entry);
            
            throw e;
        }
    }
    
    private String maskSensitiveData(String json) {
        if (json == null || json.length() > 10000) {
            return null;  // 过大不记录
        }
        return maskingService.mask(json);
    }
    
    private HttpServletRequest getRequest() {
        try {
            return ((ServletRequestAttributes) RequestContextHolder
                .currentRequestAttributes()).getRequest();
        } catch (Exception e) {
            return null;
        }
    }
    
    private String getClientIp(HttpServletRequest request) {
        if (request == null) return null;
        
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

// @AuditLog 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditLog {
    String action();      // 操作类型
    String resource();    // 操作资源
}

// 使用示例
@RestController
@RequestMapping("/api/rag")
@RequiredArgsConstructor
public class RagController {
    
    @PostMapping("/query")
    @AuditLog(action = "RAG_QUERY", resource = "rag:document")
    public ResponseEntity<RagResponse> query(@RequestBody ChatRequest request) {
        // ...
    }
    
    @PostMapping("/document/upload")
    @AuditLog(action = "DOCUMENT_UPLOAD", resource = "rag:document")
    public ResponseEntity<Void> upload(@RequestParam("file") MultipartFile file) {
        // ...
    }
    
    @DeleteMapping("/document/{docId}")
    @AuditLog(action = "DOCUMENT_DELETE", resource = "rag:document")
    public ResponseEntity<Void> delete(@PathVariable String docId) {
        // ...
    }
}

5.3 审计日志查询 API

java 复制代码
// AuditLogController.java
@RestController
@RequestMapping("/api/admin/audit")
@RequiredArgsConstructor
@RequirePermission("admin:audit:view")
public class AuditLogController {
    
    private final AuditLogService auditLogService;
    
    /**
     * 查询审计日志
     */
    @GetMapping("/logs")
    public ResponseEntity<PageResult<AuditLogVO>> queryLogs(
            @RequestParam(required = false) String userId,
            @RequestParam(required = false) String action,
            @RequestParam(required = false) String status,
            @RequestParam(required = false) Integer riskLevel,
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        AuditLogQuery query = AuditLogQuery.builder()
            .userId(userId)
            .action(action)
            .status(status)
            .riskLevel(riskLevel)
            .startTime(startTime)
            .endTime(endTime)
            .page(page)
            .size(size)
            .build();
        
        PageResult<AuditLogVO> result = auditLogService.query(query);
        return ResponseEntity.ok(result);
    }
    
    /**
     * 导出审计日志
     */
    @PostMapping("/export")
    public ResponseEntity<Void> exportLogs(
            @RequestBody AuditLogQuery query,
            HttpServletResponse response) throws IOException {
        
        response.setContentType("application/vnd.ms-excel");
        response.setHeader("Content-Disposition", 
            "attachment; filename=audit_logs_" + System.currentTimeMillis() + ".xlsx");
        
        auditLogService.export(query, response.getOutputStream());
        return ResponseEntity.ok().build();
    }
    
    /**
     * 获取审计统计
     */
    @GetMapping("/statistics")
    public ResponseEntity<AuditStatistics> getStatistics(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
        
        AuditStatistics stats = auditLogService.getStatistics(startTime, endTime);
        return ResponseEntity.ok(stats);
    }
}

5.4 高风险操作告警

java 复制代码
// RiskAlertService.java
@Service
@RequiredArgsConstructor
@Slf4j
public class RiskAlertService {
    
    private final AuditLogService auditLogService;
    private final AlertNotificationService notificationService;
    
    // 高风险操作定义
    private static final Set<String> HIGH_RISK_ACTIONS = Set.of(
        "DOCUMENT_DELETE",
        "DOCUMENT_EXPORT",
        "PERMISSION_CHANGE",
        "USER_ROLE_CHANGE",
        "BATCH_QUERY"
    );
    
    /**
     * 检查并触发风险告警
     */
    @Async
    @EventListener
    public void handleAuditLog(AuditLogEvent event) {
        AuditLogEntry entry = event.getEntry();
        
        // 1. 计算风险等级
        int riskLevel = calculateRiskLevel(entry);
        entry.setRiskLevel(riskLevel);
        
        // 2. 高风险操作告警
        if (riskLevel >= 4) {
            sendHighRiskAlert(entry);
        }
        
        // 3. 异常行为检测
        detectAnomaly(entry);
    }
    
    /**
     * 计算风险等级(0-5)
     */
    private int calculateRiskLevel(AuditLogEntry entry) {
        int risk = 0;
        
        // 高风险操作 +2
        if (HIGH_RISK_ACTIONS.contains(entry.getAction())) {
            risk += 2;
        }
        
        // 失败操作 +1
        if ("FAILED".equals(entry.getStatus())) {
            risk += 1;
        }
        
        // 非工作时间 +1
        if (isNonWorkingHour(entry.getCreatedAt())) {
            risk += 1;
        }
        
        // 非常用 IP +1
        if (isUnusualIp(entry.getUserId(), entry.getIpAddress())) {
            risk += 1;
        }
        
        // 频繁操作 +1
        if (isFrequentOperation(entry.getUserId(), entry.getAction())) {
            risk += 1;
        }
        
        return Math.min(risk, 5);
    }
    
    /**
     * 发送高风险告警
     */
    private void sendHighRiskAlert(AuditLogEntry entry) {
        Alert alert = Alert.builder()
            .alertName("HIGH_RISK_OPERATION")
            .severity("CRITICAL")
            .instance(entry.getUserId())
            .currentValue(entry.getAction())
            .threshold("Risk Level >= 4")
            .duration("Immediate")
            .timestamp(entry.getCreatedAt())
            .build();
        
        // 钉钉告警
        notificationService.sendDingTalkAlert(alert);
        
        // 邮件告警(安全团队)
        notificationService.sendEmailAlert(alert, securityTeamEmails);
        
        log.warn("高风险操作告警:userId={}, action={}, riskLevel={}", 
                entry.getUserId(), entry.getAction(), entry.getRiskLevel());
    }
    
    /**
     * 异常行为检测
     */
    private void detectAnomaly(AuditLogEntry entry) {
        // 1. 检测短时间内大量查询
        long queryCount = auditLogService.countByUserAndAction(
            entry.getUserId(), "RAG_QUERY", 
            entry.getCreatedAt().minusMinutes(5), entry.getCreatedAt()
        );
        
        if (queryCount > 100) {
            log.warn("异常行为检测:用户 {} 5 分钟内查询 {} 次", entry.getUserId(), queryCount);
            // 触发限流或临时封禁
        }
        
        // 2. 检测敏感文档访问
        // ...
    }
}

第 6 章 安全测试与合规验证

6.1 安全测试清单

markdown 复制代码
## 🔒 RAG 系统安全测试清单

### 认证与授权
- [ ] 未登录用户无法访问受保护接口
- [ ] Token 过期后自动拒绝访问
- [ ] 无效 Token 返回 401
- [ ] 越权访问返回 403
- [ ] 管理员权限正确隔离

### 文档权限
- [ ] 用户只能访问授权文档
- [ ] 部门权限正确生效
- [ ] 角色权限正确生效
- [ ] 权限过期后自动失效
- [ ] 向量检索结果正确过滤

### 数据脱敏
- [ ] 身份证号正确脱敏
- [ ] 手机号正确脱敏
- [ ] 邮箱正确脱敏
- [ ] 检索结果脱敏生效
- [ ] 日志中敏感信息脱敏

### 审计日志
- [ ] 所有操作都有日志记录
- [ ] 日志无法被篡改
- [ ] 日志保留期限符合要求
- [ ] 高风险操作触发告警
- [ ] 日志查询功能正常

### 渗透测试
- [ ] SQL 注入测试通过
- [ ] XSS 攻击测试通过
- [ ] CSRF 攻击测试通过
- [ ] 提示词注入测试通过
- [ ] 暴力破解防护生效

6.2 提示词注入防护

java 复制代码
// PromptInjectionFilter.java
@Component
@Slf4j
public class PromptInjectionFilter {
    
    // 常见提示词注入模式
    private static final List<Pattern> INJECTION_PATTERNS = List.of(
        Pattern.compile("(?i)ignore\\s+previous\\s+instructions"),
        Pattern.compile("(?i)you\\s+are\\s+now\\s+in\\s+developer\\s+mode"),
        Pattern.compile("(?i)bypass\\s+(all|any)\\s+(rules|restrictions)"),
        Pattern.compile("(?i)output\\s+(your|the)\\s+(system|initial)\\s+prompt"),
        Pattern.compile("(?i)print\\s+(your|the)\\s+(instructions|rules)"),
        Pattern.compile("(?i)act\\s+as\\s+(another|different)\\s+(ai|model)"),
        Pattern.compile("(?i)disable\\s+(safety|security)\\s+(filters|checks)")
    );
    
    /**
     * 检测并过滤提示词注入
     */
    public boolean isSafe(String input) {
        if (input == null || input.isEmpty()) {
            return true;
        }
        
        for (Pattern pattern : INJECTION_PATTERNS) {
            if (pattern.matcher(input).find()) {
                log.warn("检测到提示词注入尝试:{}", input.substring(0, Math.min(100, input.length())));
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * 清理输入
     */
    public String sanitize(String input) {
        if (input == null) return null;
        
        // 移除潜在的危险字符
        String sanitized = input
            .replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]", "")
            .trim();
        
        // 限制长度
        if (sanitized.length() > 10000) {
            sanitized = sanitized.substring(0, 10000);
        }
        
        return sanitized;
    }
}

// 在 RAG 服务中使用
@Service
@RequiredArgsConstructor
public class RagService {
    
    private final PromptInjectionFilter injectionFilter;
    
    public RagResponse query(String userId, String question) {
        // 1. 输入清理
        String sanitizedQuestion = injectionFilter.sanitize(question);
        
        // 2. 注入检测
        if (!injectionFilter.isSafe(sanitizedQuestion)) {
            throw new SecurityException("检测到潜在的提示词注入攻击");
        }
        
        // 3. 正常处理
        // ...
    }
}

6.3 等保 2.0 合规检查表

控制项 要求 RAG 系统实现 状态
身份鉴别 双因素认证 JWT + MFA
访问控制 权限最小化 RBAC + 文档级权限
安全审计 操作可追溯 全链路审计日志
数据完整性 防篡改 日志签名 + 区块链存证
数据保密性 加密存储 AES-256 字段加密
备份恢复 数据备份 每日备份 + 异地容灾
入侵防范 攻击检测 高风险操作告警

第 7 章 生产环境安全部署

7.1 安全配置清单

yaml 复制代码
# application-security.yml
security:
  jwt:
    secret: ${JWT_SECRET:}  # 必须从环境变量获取
    expiration: 3600000     # 1 小时
    refresh-expiration: 604800000  # 7 天
  
  encryption:
    aes-key: ${AES_KEY:}    # 必须从环境变量获取
  
  cors:
    allowed-origins: https://rag.yourdomain.com
    allow-credentials: true
  
  rate-limit:
    enabled: true
    requests-per-minute: 60
    burst-size: 10
  
  audit:
    enabled: true
    retention-days: 365
    high-risk-threshold: 4

7.2 Docker 安全配置

dockerfile 复制代码
# Dockerfile
FROM eclipse-temurin:21-jre-alpine

# 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# 复制 JAR
COPY target/rag-system.jar app.jar

# 设置权限
RUN chown -R appuser:appgroup /app

# 切换到非 root 用户
USER appuser

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java", "-jar", "app.jar"]

7.3 Nginx 安全配置

nginx 复制代码
# /etc/nginx/conf.d/rag-security.conf
server {
    listen 443 ssl http2;
    server_name rag.yourdomain.com;
    
    # SSL 配置
    ssl_certificate /etc/nginx/ssl/rag.crt;
    ssl_certificate_key /etc/nginx/ssl/rag.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    
    # 安全头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline';" always;
    
    # 限流
    limit_req_zone $binary_remote_addr zone=rag_limit:10m rate=60r/m;
    
    location / {
        limit_req zone=rag_limit burst=10 nodelay;
        proxy_pass http://rag-frontend:80;
    }
    
    location /api/ {
        limit_req zone=rag_limit burst=10 nodelay;
        proxy_pass http://rag-backend:8080/;
        
        # IP 黑白名单
        allow 10.0.0.0/8;
        allow 192.168.0.0/16;
        deny all;
    }
}

本专题完

📌 核心要点总结

  1. RBAC + ABAC 混合权限模型满足企业级权限需求
  2. 文档级权限控制需与向量检索过滤结合
  3. 数据脱敏应在检索结果返回前执行
  4. 审计日志需记录全链路操作,保留至少 365 天
  5. 高风险操作(删除、导出、权限变更)需实时告警
  6. 提示词注入防护是 LLM 应用特有的安全需求
  7. JWT Token 需设置合理过期时间,支持刷新机制
    📖 下专题预告:《RAG 系统运维与故障排查:从部署到监控的全流程指南》------ 详解容器编排、日志聚合、故障诊断、灾备恢复
相关推荐
想不到一个好的ID2 小时前
OpenClaw 初学者必看指南
人工智能
zzh940772 小时前
GPT-4o与Gemini 3镜像站背后的算力与工程:大模型训练基础设施拆解
人工智能·深度学习·架构
菌菌的快乐生活2 小时前
在 WPS 中设置 “第一章”“第二章” 这类一级编号标题自动跳转至新页面
前端·javascript·wps
npupengsir2 小时前
nano vllm代码详解
人工智能·算法·vllm
xiaohe072 小时前
Maven Spring框架依赖包
java·spring·maven
CyanMind2 小时前
IsaacLab 训练范式探索(一):让机器人拥有“记忆”的 RNN 策略
人工智能·rnn·机器人
hh随便起个名2 小时前
useRef和useState对比
前端·javascript·react
Hello_Embed2 小时前
LVGL 入门(十五):接口优化
前端·笔记·stm32·单片机·嵌入式
翼龙云_cloud2 小时前
阿里云渠道商:百炼模型选型指南 性能与成本全解析
人工智能·阿里云·云计算