用AI写代码,我差点把漏洞发上线:血泪总结的10个教训

0 阅读10分钟

三周前,我差点在生产环境搞出一个安全事故。

起因很简单:让Cursor写了一个文件上传接口,跑了一下,没报错,直接提交了。

结果被同事review的时候揪出来:

 // AI生成的代码
 String savePath = uploadDir + fileName; // fileName来自用户输入
 File file = new File(savePath);
 file.transferFrom(channel);

fileName是用户传进来的,如果传../../etc/passwd,文件就写到服务器根目录去了。

这是一个经典的路径穿越漏洞

功能完全正常,测试全部通过,但任何一个懂安全的人看一眼就知道这是定时炸弹。

那天之后,我把过去3个月用Cursor踩过的坑,全部整理了一遍。

一共10个。每一个都是真实交过的学费。


第一组:认知层的坑

这组坑最普遍。几乎每个刚开始用AI编程工具的人都会踩。

坑1:把Cursor当"更好的补全"在用

我最开始的用法是这样的:

 写几个字 → 等AI补全 → 不满意就删 → 重写

用了两周,感觉"也就那样",效率提升有限,还经常被AI的补全打断思路。

后来才明白:这个用法完全用错了方向。

Cursor的核心能力不是补全,是生成

正确的用法是:

 描述清楚需求 → AI生成完整代码块 → 我来review和调整

两种用法的效率差距,不是10%,是10倍。

一句话改法: 不要等AI补全,要主动用Ctrl+K描述需求,让AI一次生成完整的函数或模块。


坑2:Prompt写得太模糊,然后怪AI不好用

这是我听到最多的抱怨:

"AI写的代码太烂了,还不如自己写。"

我每次听到这个,都想问一句:你的Prompt写的是什么?

模糊Prompt:

 写一个登录接口

AI生成的:

 public String login(String username, String password) {
     User user = userMapper.findByUsername(username);
     if (user != null && user.getPassword().equals(password)) {
         return "success";
     }
     return "fail";
 }

密码明文比较,没有token,没有异常处理,没有日志。能跑,但没法用。

清楚的Prompt:

 写一个用户登录接口,要求:
 1. 用户名+密码登录,密码用BCrypt校验
 2. 登录失败超过5次,锁定账户30分钟(用Redis存失败次数)
 3. 登录成功返回JWT token,有效期7天
 4. 记录登录日志:IP、User-Agent、时间、成功/失败
 5. 异常统一用BusinessException抛出,返回Result<T>包装
 6. 技术栈:Spring Boot 3, MyBatis-Plus, Redis

AI生成的: 80%可以直接用。

AI的质量上限,取决于你Prompt的质量上限。

一句话改法: 写Prompt之前,先想清楚如果让一个新同事来做这个功能,你会告诉他哪些信息。把这些信息全写进去。


坑3:以为AI"懂"你的项目

这个坑很隐蔽。

Cursor确实能读取你的项目文件,但它不知道你们团队的规范、你的代码风格、你的命名习惯。

结果就是:

 // 你的项目规范
 public class UserService implements IUserService { ... }
 // 返回值统一用 Result<T>
 // 异常统一用 BusinessException
 ​
 // AI生成的(没有规范约束时)
 public class UserServiceImpl implements UserService { ... }
 // 直接返回实体类
 // 用 RuntimeException 抛异常

每次生成完都要手动调整,改来改去,效率反而变低了。

解决方案是在项目根目录建一个 .cursorrules 文件:

 # 项目规范
 ​
 ## 命名规范
 - 接口以I开头:IUserService
 - 实现类不加Impl后缀:UserService
 - DTO类名以DTO结尾
 ​
 ## 代码规范
 - 所有接口返回 Result<T>
 - 异常统一用 BusinessException 抛出
 - 日志用 @Slf4j,不用 System.out.println
 - 常量抽到对应模块的 Constants 类
 ​
 ## 技术栈
 - Spring Boot 3.x / JDK 17
 - MyBatis-Plus 3.5.x
 - Redis(缓存和分布式锁)

配置之后,AI生成的代码直接符合规范,review工作量少了一半。

一句话改法: 第一次用Cursor接入新项目,先花30分钟写好 .cursorrules,后面省的时间是10倍。


第二组:安全和质量的坑

这组坑最危险。踩了可能直接影响线上。

坑4:AI生成的代码能跑,但有安全漏洞

开头的故事就是这个坑。

我把它完整复盘一遍。

AI生成的文件上传接口(问题版本):

@PostMapping("/upload")
public Result<String> upload(@RequestParam MultipartFile file) {
    String fileName = file.getOriginalFilename(); // 直接用用户传入的文件名
    String savePath = uploadDir + fileName;        // 路径直接拼接
    file.transferTo(new File(savePath));
    return Result.success(savePath);
}

三个问题:

  1. getOriginalFilename() 返回的是用户控制的值,可以是 ../../etc/passwd
  2. 没有文件大小限制,可以上传几个G的文件把磁盘撑爆
  3. 没有文件类型校验,可以上传可执行文件

修复之后:

@PostMapping("/upload")
public Result<String> upload(@RequestParam MultipartFile file) {
    // 1. 文件大小校验
    if (file.getSize() > MAX_FILE_SIZE) {
        throw new BusinessException("文件大小超过限制");
    }
    
    // 2. 文件类型校验
    String contentType = file.getContentType();
    if (!ALLOWED_TYPES.contains(contentType)) {
        throw new BusinessException("不支持的文件类型");
    }
    
    // 3. 文件名用UUID重新生成,不用用户传入的
    String ext = FilenameUtils.getExtension(file.getOriginalFilename());
    String safeFileName = UUID.randomUUID() + "." + ext;
    String savePath = uploadDir + safeFileName;
    
    file.transferTo(new File(savePath));
    return Result.success(safeFileName);
}

AI不会主动帮你考虑安全性,除非你在Prompt里明确要求。

一句话改法: 涉及文件操作、用户输入、权限校验的代码,Prompt里加一句"需要考虑安全性,列出可能的安全风险并处理"。


坑5:并发场景AI经常给你挖坑

AI写出来的代码,在单线程下完全没问题。一到并发,就出幺蛾子。

我上个月做消息通知系统时,AI生成了一个重试逻辑:

// AI生成的(有问题)
public void retryFailed() {
    List<NotifyRecord> failedList = notifyRecordMapper.selectFailed();
    for (NotifyRecord record : failedList) {
        if (record.getRetryCount() < 3) {
            doNotify(record);
            record.setRetryCount(record.getRetryCount() + 1);
            notifyRecordMapper.updateById(record);
        }
    }
}

问题:如果这个方法被多个线程同时执行(比如定时任务多实例部署),同一条记录会被重复发送。

修复后:

// 加分布式锁 + 数据库乐观锁
public void retryFailed() {
    List<NotifyRecord> failedList = notifyRecordMapper.selectFailed();
    for (NotifyRecord record : failedList) {
        String lockKey = "notify:retry:" + record.getId();
        // 抢锁,抢不到说明已有其他实例在处理
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 5, TimeUnit.MINUTES);
        if (Boolean.TRUE.equals(locked)) {
            try {
                doNotify(record);
                notifyRecordMapper.incrementRetryCount(record.getId());
            } finally {
                redisTemplate.delete(lockKey);
            }
        }
    }
}

一句话改法: 定时任务、库存扣减、余额变更、状态流转这类场景,Prompt里明确加上"需要考虑并发安全,使用分布式锁或乐观锁"。


坑6:AI不会主动帮你考虑性能

AI的目标是"功能正确",不是"性能最优"。

最常见的是N+1查询问题。

// AI生成的(N+1查询)
public List<OrderVO> getOrders(Long userId) {
    List<Order> orders = orderMapper.selectByUserId(userId);
    return orders.stream().map(order -> {
        OrderVO vo = new OrderVO();
        BeanUtils.copyProperties(order, vo);
        // 每个订单都查一次用户信息
        User user = userMapper.selectById(order.getUserId());
        vo.setUserName(user.getName());
        // 每个订单都查一次商品信息
        Product product = productMapper.selectById(order.getProductId());
        vo.setProductName(product.getName());
        return vo;
    }).collect(Collectors.toList());
}

100个订单 = 201次数据库查询。

一句话改法: 涉及列表查询,Prompt里加"注意避免N+1查询,使用批量查询"。或者review时重点看循环内部有没有数据库调用。


坑7:让AI写测试,但测试是假的

这个坑最难发现。

我让Cursor写单元测试,它生成了一大堆,覆盖率看起来很高。

但仔细看:

@Test
void testLogin_success() {
    // Mock了所有依赖
    when(userMapper.findByUsername("test")).thenReturn(mockUser);
    when(passwordEncoder.matches(any(), any())).thenReturn(true);
    when(jwtUtil.generateToken(any())).thenReturn("mock-token");
    
    Result<String> result = userService.login("test", "123456");
    
    // 只验证了返回值不为空
    assertNotNull(result);
}

测试通过了,但什么都没有真正验证:

  • 没有验证返回的token是不是预期格式
  • 没有验证登录日志是否记录了
  • 没有验证失败次数是否被重置

这种测试,写了等于没写,但覆盖率数字很好看。

一句话改法: 让AI写完测试后,追问一句:"这些测试有没有只验证了mock的返回值,而没有验证真实的业务逻辑?"让AI自己检查。


第三组:习惯层的坑

这组坑最隐蔽,也是从"会用"到"用好"之间最难跨越的鸿沟。

坑8:一次让AI写太多,代码直接失控

刚开始用Cursor的时候,我会这么写Prompt:

帮我实现整个用户模块:注册、登录、修改密码、找回密码、
注销账户、用户信息管理、头像上传、第三方登录

AI生成了一大坨代码,洋洋洒洒800行。

然后我发现:

  • 注册和登录的密码校验逻辑写了两套
  • 日志记录风格不一致,有的用中文有的用英文
  • 异常处理方式三种混用
  • 我根本没办法整体review,只能硬着头皮提交

正确做法:

第一步:帮我实现用户注册功能
[review完,满意了]
第二步:帮我实现用户登录,密码校验复用注册里的逻辑
[review完,满意了]
第三步:继续,修改密码功能

分步骤生成,每步都是可控的,每步都能review,最终质量好得多。

一句话改法: 单次Prompt生成的代码,不要超过100行。超过了就拆。


坑9:看不懂AI的代码,但直接用了

这个习惯很危险。

AI有时候会生成一些你没见过的写法:

// AI生成的
CompletableFuture.allOf(
    CompletableFuture.runAsync(() -> sendEmail(user)),
    CompletableFuture.runAsync(() -> sendSms(user))
).join();

如果你不熟悉CompletableFuture,可能看了一眼觉得"能跑就行"就提交了。

但这里有个问题:.join() 会阻塞当前线程,如果email或者短信服务超时,整个请求就卡死了。正确做法是加超时控制。

你不理解这段代码,你就发现不了这个问题。

我现在遇到看不懂的写法,一定会先问清楚:

你这里用了CompletableFuture.allOf,能解释一下:
1. 这样写的原因是什么?
2. 有没有潜在的风险?
3. 有没有更简单的替代方案?

一句话改法: 设立一条铁律:看不懂的代码,不提交。


坑10:效率上去了,但停止了深度思考

这是最隐蔽的坑,也是我认为最致命的。

用了AI之后,写代码确实快了。但我发现自己开始懒得思考了。

以前设计一个接口,我会想:

  • 这个接口的入参合理吗?
  • 返回值够不够用?
  • 将来扩展会不会有问题?

现在有时候是:

  • 让AI生成一个接口
  • 能跑了
  • 提交

然后两周后,业务需求一变,发现接口设计有根本性的问题,改起来成本极高。

AI提升的是执行速度,但思考这件事,没有任何捷径。

效率高了,省出来的时间,应该用来思考更深的问题:

  • 这个方案是最好的吗?
  • 还有没有更简单的实现?
  • 三个月后再看这段代码,我还能看懂吗?

一句话改法: 每天留出一段"无AI时间",专门用来想架构、想设计、想需求背后的问题。


写在最后

总结一下这10个坑:

认知层:

  1. 把Cursor当补全工具 → 要当成"程序员"用
  2. Prompt太模糊 → 像给新同事交代任务一样写清楚
  3. 没配 .cursorrules → 花30分钟配好,省几十小时

安全和质量:

  1. 没考虑安全性 → Prompt里明确要求,代码里重点review
  2. 并发场景没处理 → 明确要求加锁,识别并发场景
  3. 没考虑性能 → review时重点看N+1和大数据量
  4. 测试是假的 → 让AI自我检查测试的有效性

习惯层:

  1. 一次生成太多 → 单次不超过100行,分步骤来
  2. 看不懂的代码直接用 → 看不懂就问,不提交
  3. 停止深度思考 → 保留"无AI时间"

AI让写代码变容易了,但让写代码变得更考验人了。

以前代码写得慢,你有足够的时间思考。

现在代码生成很快,反而需要你更主动地去思考那些AI不会替你想的问题。

这10个坑,我全踩过。

希望你少踩几个。


💬 回复「工具」获取我完整的AI开发工具清单(含配置方法)

💬 回复「交流」加入AI开发者交流群,一起踩坑一起成长


后端AI实验室 不讲概念,只谈实战 代码开源,每周更新

扫码_搜索联合传播样式-标准色版.png