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

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

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

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

java 复制代码
// 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生成的:

java 复制代码
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确实能读取你的项目文件,但它不知道你们团队的规范、你的代码风格、你的命名习惯。

结果就是:

java 复制代码
// 你的项目规范
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生成的文件上传接口(问题版本):

java 复制代码
@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. 没有文件类型校验,可以上传可执行文件

修复之后:

java 复制代码
@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生成了一个重试逻辑:

java 复制代码
// 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);
        }
    }
}

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

修复后:

java 复制代码
// 加分布式锁 + 数据库乐观锁
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查询问题。

java 复制代码
// 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写单元测试,它生成了一大堆,覆盖率看起来很高。

但仔细看:

java 复制代码
@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有时候会生成一些你没见过的写法:

java 复制代码
// 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分钟配好,省几十小时

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

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


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

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

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

这10个坑,我全踩过。

希望你少踩几个。


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

相关推荐
zzb158017 小时前
Claude Agent SDK 深度剖析:依赖、权衡与架构选择
人工智能·python·ai
zjjsctcdl17 小时前
Spring Boot 经典九设计模式全览
java·spring boot·设计模式
会算数的⑨17 小时前
Spring AI Alibaba 学习(四):ToolCalling —— 从LLM到Agent的华丽蜕变
java·开发语言·人工智能·后端·学习·saa·ai agent
程序员鱼皮17 小时前
我的免费 OpenClaw 零基础教程,爆了!
ai·程序员·编程·ai编程·openclaw
一直都在57217 小时前
Java线程池
java·开发语言
亚马逊云开发者17 小时前
article
java·开发语言
云烟成雨TD17 小时前
Spring AI 1.x 系列【11】基于 PromptTemplate 构建一站式 AI 写作助手
java·人工智能·spring
小旭952717 小时前
Spring 纯注解配置与 SpringBoot 入门详解
java·开发语言·spring boot·后端·spring
ADRU17 小时前
SSE 到底是什么?它和 HTTP 有什么关系?Java/Spring 怎么实现流式输出(可直接上手)
java·spring·http
de_wizard17 小时前
Spring Boot 整合 Keycloak
java·spring boot·后端