资讯动态

代码设计原则与OWASP安全最佳实践:从SOLID到防御性编程的融合落地

发布时间:2026/10/18 1:38:41 来源:尧图企业网站定制
本篇文章需要认真对待我把它当作一次“代码评审”后的经验复盘来写。标题里的 SOLID、DRY、KISS、YAGNI 和 OWASP 最佳实践看起来是几组独立的词但放到真实的项目里它们其实是同一件事的两面一个是“代码怎么设计才不容易烂”一个是“系统怎么兜底才不容易被攻破”。下面我会结合自己实际做过的项目把这套东西拆开讲透。1. 整体设计思路先理解四条原则和一个基线的关系很多人把 SOLID、DRY、KISS、YAGNI 当口号背但真正写代码的时候又不知道什么时候该用提到 OWASP又觉得那是安全团队的事跟普通开发没关系。我想换个角度来聊与其把这些概念当成“规范清单”不如把它们当成写代码时的决策维度。1.1 四条原则本质上是在回答四个问题先给不熟悉的朋友快速过一遍这四个词的含义SOLID是五个面向对象设计原则的缩写分别是单一职责SRP、开闭原则OCP、里氏替换LSP、接口隔离ISP和依赖倒置DIP。它解决的是“类与类之间怎么划分边界、怎么协作”的问题。DRYDont Repeat Yourself解决的是“同一份逻辑不要散落多处否则改一处漏一处”的问题。KISSKeep It Simple, Stupid解决的是“方案复杂度不要超过问题本身的复杂度”的问题。YAGNIYou Aint Gonna Need It解决的是“未来可能用到的功能现在不要提前写因为大概率用不上而且会拖垮现在的设计”的问题。我的理解是SOLID 管的是结构DRY 管的是重复KISS 管的是复杂度YAGNI 管的是范围。四个原则互相配合但侧重点完全不同。1.2 OWASP 不只是“Top 10 名单”而是一套完整的参考基线OWASP开放式Web应用程序安全项目常被提起的是 Top 10 漏洞列表但它真正有价值的是配套的一系列文档ASVS应用安全验证标准、各类 Cheat Sheet速查表、测试指南、代码审查指南。这些文档覆盖了从威胁建模、安全需求、安全设计、安全编码到安全测试的全流程。我在实际项目中把 OWASP 当作一个安全基线字典来用遇到不确定某个功能是否安全就去翻对应的 Cheat Sheet做架构设计时把 ASVS 的 L1 级别作为最低要求写安全相关代码时对照 Top 10 的条目逐项排查。它本身不是一套要生搬硬套的教条而是一份“预防已知风险”的经验库。1.3 原则与安全的交叉点代码质量直接影响攻击面这是我最想强调的一点SOLID、DRY、KISS、YAGNI 和安全性不是两件独立的事而是同一件事的先后关系。一个违反 YAGNI 的系统往往会多出很多“看起来有用但没人用”的功能入口每个入口都是一次攻击面。一个违反 KISS 的系统业务逻辑和框架代码混杂在一起安全响应无处下手修复 Bug 时容易把校验逻辑一并改坏。一个违反 DRY 的系统同样的“用户输入校验”散落在十几个地方安全团队出一道修复通知你根本不知道哪里漏改了。一个违反 SOLID 的系统所有逻辑堆在一个上帝类里任何一处输入的不可信值都可能直接流过核心业务逻辑追踪数据流比登天还难。所以我在做项目时会把“是否遵守基本设计原则”直接纳入安全评审的检查项——这不是凑篇幅而是经验教训。2. 四条原则的实战拆解从理解到落地2.1 SOLID不是“五个规范”而是五把不同的尺子逐条说一下我在实际代码里是怎么理解和应用 SOLID 的。单一职责SRP。我见过最多的反例是“工具类”一个类里放了“字符串处理”“日期格式化”“调用外部接口”“解析配置文件”四个完全无关的功能。表面上看挺整齐实际上每次改动都要把这四个 Diff 同时评审一遍而且改动一个可能炸掉另外三个。我自己的判断标准是给这个类写一句话职责描述如果描述里包含了“和”字基本就是超纲了。比如“负责订单数据的读写和邮件发送”——这句话里“和”字两侧的事就应该拆开。开闭原则OCP。核心是“对扩展开放、对修改关闭”。举个例子老项目里常见这样的代码——一个 purchasePrice 的计算方法里面用 if 判断是普通用户、会员、VIP每种折扣一个分支。下个月运营说加一个“大客户”类型你就得再往里塞一个 if。正确做法是把“折扣策略”抽象成接口不同用户类型实现不同策略类新类型来了直接新增类而不改动旧代码。我实际重构过类似的代码改动前 20 个 if 散布在 5 个文件里改动后策略接口加 3 个实现类主流程逻辑一眼能看懂。里氏替换LSP。这条最容易被忽略。它的意思是子类必须能无缝替换父类不能改变父类行为的语义。最经典的反面教材是“正方形继承长方形”的问题——重写了 setWidth 方法导致宽度和高度联动替换后程序行为就变了。我在项目里常用另一句话来实践它如果子类需要重写父类的方法并改变其输入输出的约定或者需要调用方感知“你是子类而不是父类”那就说明继承关系用错了应该改用组合。接口隔离ISP。这条和下面要讲的“胖接口”问题相关。如果一个接口有 10 个方法而某个实现类只需要其中 2 个其余 8 个只能抛“不支持”异常那这个接口设计就是失败的。正确的思路是把大接口拆成多个特定角色的小接口让实现者只需要实现自己真正用到的能力。依赖倒置DIP。高层模块不应该依赖低层模块两者都应该依赖抽象抽象不应该依赖细节细节应该依赖抽象。翻译成人话业务代码不要直接 new 一个具体的外部服务类比如某个短信服务商或云存储客户端而是定义一个自己的接口让外部实现来满足这个接口。这样未来替换服务商时业务代码一行不用改只改装配的配置。2.2 DRY把“重复”和“抽象”分开看DRY 听起来简单——“不要重复自己”但我在实践中发现很多人对它的理解有两个常见偏差第一个偏差是把 DRY 理解为“代码不要重复”于是把两段长得像但语义完全无关的代码强行合并成一个函数。比如一个把“订单号生成规则”的函数和一个“优惠券码生成规则”的函数恰好都用到了“把数字转成 36 进制”的逻辑于是有人就把它们合成一个通用函数传入一个 type 参数做分支。结果改订单号的规则时这个函数里的分支越来越多变成一坨“万能类”。我自己的实践是区分两种重复逻辑重复semantic duplication和语法重复syntactical duplication。逻辑重复必须消除比如“判断用户是否有权限下单”这段逻辑在 controller、service、repository 三个层各写了一遍必须收敛。语法重复则要看情况——如果两段代码仅仅是长得像但它们的业务语义、变化方向都不同那么强行合并反而制造耦合。真正的 DRY 是“消灭知识点的重复”而不是“消灭字符的重复”。第二个偏差是在项目第一天就追求 DRY。一上来就把所有可能重复的地方都抽象好结果抽象错方向。我现在的习惯是写第二遍重复代码时先忍住写第三遍时明确提炼出一个函数/类的过程。这是被 Andrew Hunt 和 David Thomas 在《程序员修炼之道》里的建议影响的——规则是“三次法则”。2.3 KISS复杂度是个税能少交就少交KISS 原则的执行难点在于你很难在“写一个通用优雅的方案”和“写一个跑得通但看起来笨的方案”之间承认后者是对的。大多数程序员下意识会选择前者因为前者让你觉得自己专业。我在一个订单导出功能上吃过亏。业务方说“需要导出 Excel”我一开始设计了一个支持“表头自定义、样式自定义、多 sheet、模板引擎”的导出框架花了两天时间。结果最后交付时业务方只用了一个默认模板那些框架能力 90% 没用到。后来维护的时候框架自身配置反而成了问题。这就是典型违反 KISS。KISS 的实践标准我总结为三条解决问题的最小可行方案是什么先把这个做出来再考虑扩展。如果现有方案需要文档解释才能让人看懂那它还不够简单。每增加一层抽象必须回答一个问题它现在就能帮我减少多少具体的复杂度如果答案是“将来可能会”那就先不加。有个同事说得很好KISS 不是让你写低级的代码而是让你把高级的解决方案放进合理的最小外壳里。2.4 YAGNI不要为“未来可能”买单YAGNI 和 KISS 经常一起出现但它们的关注点不同。KISS 关注“当前方案复杂度”YAGNI 关注“当前代码范围”。举一个真实案例某模块需要做用户登录设计者为了“方便未来做单点登录”一开始就引入了分布式会话中心把会话状态放到外部缓存里结果登录功能本身还没跑通先被分布式会话的序列化问题拖了两个星期。等到真的需要单点登录时架构早就换了几轮当初的“缓存会话设计”全被推翻重写。这就是为“假想中的未来”付出真实成本。我现在的做法是当你说出“将来可能”这四个字的时候请把这个“将来”具体到某个业务版本和某个触发条件。如果这个触发条件说不清楚那么现在就别做。技术债和过度设计之间有一个很微妙的界限我的经验是如果一件事只是“可能有用”它大概率会让当前的系统更脆弱——因为你为不确定的假设增加了复杂度而这些假设往往不会发生。3. OWASP 安全最佳实践从“了解漏洞”到“写进代码”3.1 先建立安全编码的上下文意识前面聊了代码设计原则现在转到 OWASP。我观察到很多开发者对“安全”的理解停留在“密码要加密”“SQL 要加参数化”这种零散点状认知上。但实际项目中安全问题大多是架构层面的用户输入从哪里来、数据流向哪里、哪些接口需要鉴权、异常信息是否会暴露内部细节。OWASP 最有价值的不是那份 Top 10 名单而是它背后的思维方式把所有外部输入都当成不可信数据然后从数据入口到数据出口全链路做校验。一旦你建立了这个上下文意识再去看那些设计原则会发现它们和安全是天然互补的——比如单一职责让每个数据处理阶段的校验责任清晰依赖倒置让安全过滤组件可以灵活替换和升级。3.2 常见 Top 10 威胁的日常代码对应我平时做代码评审时会在 OWASP Top 10 里挑出几条高频出现的逐一对照代码A01 失效的访问控制、A03 注入、A05 安全配置错误、A07 身份验证与会话管理失败、A09 安全日志与监控失效。这几条在业务代码里出现的频率最高而且我都能用简单的例子说清楚。以注入为例。很多人知道我不用字符串拼接 SQL但当面对“排序字段”这种需求时往往会写String orderBy create_time; if (price.equals(sortParam)) { orderBy price; } else if (sales.equals(sortParam)) { orderBy sales; } String sql SELECT * FROM product ORDER BY orderBy;这段代码用了白名单限制了排序字段其实就避开了注入风险。虽然它看起来“不够优雅”但在 KISS 和安全的双重标准下这种“笨但安全”的写法是正确的。真正的坏例子是直接把用户参数拼进 order by 或表名、列名。再以失效的访问控制为例。很多系统只在“菜单显示”上判断了权限后端接口却没校验“当前用户是否能访问这个用户的订单”。我见过线上事故对一个订单详情接口抓包只改 URL 里的订单 ID就能查看别人的订单。这是 A01 最典型的形态——水平越权。对应的 OWASP 建议是后端每个需要资源持有者信息的接口都必须校验资源的“拥有者属性”而不能仅仅依赖“登录用户 ID 由前端传过来”。3.3 实操落地依赖管理与安全配置除了编写安全代码OWASP 最佳实践里还有一项被我个人视为“即时见效”的工作依赖管理SCASoftware Composition Analysis。我实际带的项目曾经因为一个深层依赖组件存在已知漏洞而被安全团队通报。当时的排查流程是这样的用 OWASP Dependency-Check 对项目做全量扫描生成 HTML 报告。在报告中识别出漏洞组件某版本号小于 2.6 的 JSON 解析库存在反序列化漏洞。更新依赖到修复版本重新跑全量测试。把依赖漏洞扫描接入 CI 流水线定为“中危及以上漏洞阻断发布”。这里我想提醒的是很多开发团队只关注“引入的依赖能干什么”却从不关注“这个依赖版本有什么已知漏洞”。OWASP 的依赖检查并不难接入难的是持续做下去。现在我每个项目都要求把依赖检查作为 Merged Request 的必检项和单元测试、代码风格检查并列。另一个高频问题是安全配置错误A05。Spring 生态常见的配置坑开发环境的调试接口比如 actuator、Swagger被发布到生产环境且未做访问控制错误信息里把堆栈异常直接返回给前端暴露了包名、类名、数据库驱动版本默认密码未修改比如 Redis 默认无密码、管理后台默认 admin/123456缺少安全响应头比如 X-Frame-Options、Content-Security-Policy。这些配置错误在代码里往往只是一行配置的事儿但一旦出问题修复成本不高、影响面却不小。它们恰恰最符合“预防胜于补救”这一安全最基本原则。3.4 安全代码评审清单可直接抄用我把这些年项目里用到的安全评审要点整理成一份清单放在评审模板里。这里分享给大家评审项检查重点输入校验所有外部输入HTTP 参数、文件上传、消息队列、第三方回调是否做了白名单格式校验输出编码JSON/HTML/XML 等输出是否做了上下文相关编码访问控制是否在每个需要资源归属校验的接口都做了水平/垂直越权校验认证与会话密码存储是否使用 BCrypt 等自适应加盐算法会话 ID 是否设置了 HttpOnly、Secure 属性依赖安全是否已跑 SCA 扫描并确认无中危以上漏洞错误处理错误信息是否不暴露堆栈、SQL、内部路径是否统一走日志系统而不是吐给前端日志是否记录了足够的审计日志登录、登出、权限变更、关键业务操作是否对密码、令牌、身份证号做了日志脱敏传输安全敏感接口是否仅允许 HTTPS 访问HSTS 是否开启配置安全调试端点、默认账号、默认密码是否已禁用或修改安全头是否齐备这份清单不是说每一项都必须全做到才能上线而是说如果评审时发现某一项缺失你必须要能讲清楚“为什么这次可以接受这个风险”。很多安全问题就是在这种“说不上为什么但先放着没关系”的过程中埋下的。4. 融合实践一个“订单系统”的代码改造实录理论讲多了得落到代码上。我拿一个具体案例做演示——模拟项目X是一个订单系统它在一次安全扫描和一次代码评审中暴露了不少问题。我把整个改造过程拆解成几步大家可以直接借鉴思路。4.1 改造前一堆违反原则和安全规范的问题改造前的代码有几个典型毛病Controller 层直接写了业务逻辑还顺手拼了 SQL同一个“校验用户是否有权限操作该订单”的逻辑在 3 个 Service 方法里各写了一遍为了“未来支持多支付渠道”引入了一个支付抽象工厂结果两年只上线了一个渠道抽象工厂里的代码比实际支付逻辑还多订单详情接口只校验登录态没校验订单归属错误处理直接把异常堆栈返回给前端日志里却能搜到大量敏感字段的明文。当时评审时的结论是系统“能跑”但改一个功能要懂整个流程而且安全漏洞集中在上方几个点。于是按“原则优先、安全兜底”的思路分两步改造。4.2 第一步用 SOLID 和 KISS 重构结构改造的第一步不是加安全组件而是先把代码结构捋顺。因为如果代码是一坨安全校验该放在哪里根本说不清楚。我用依赖倒置的思路定义了一个订单查询接口和一个订单归属校验接口让外部基础设施去实现它用单一职责把订单创建、订单查询、订单支付拆成三个独立 Service用开闭原则把“支付渠道支持”从抽象工厂改成了策略列表——未来加渠道时只需注册新策略旧逻辑完全不动同时把原来 Controller 里的业务逻辑全部下沉到 Service 层Controller 只负责参数接收和结果返回。这个阶段我把之前那套过度设计的支付抽象工厂砍掉了一多半只保留了当前需要的策略接口严格遵守 YAGNI 和 KISS。改完之后整个代码行数少了大概三分之一但功能没有减少。4.3 第二步用 DRY 收敛重复用 OWASP 补齐安全缺口结构理顺之后我做了两个动作动作一收敛重复逻辑。把散落的三处“订单归属校验”抽成一个公共方法所有入口统一调用。这样安全团队后续若要求加强校验逻辑比如增加风控规则只需要改这一个方法不用再追着十几个调用点逐一打补丁。动作二按 OWASP ASVS 的要求补安全控制。我重点做了四件事所有订单查询接口强制校验当前登录用户是否与该订单关联核心判断放在 Service 层而不是 Controller 层防止因为不同入口遗漏校验。输入校验改成白名单模式。对于订单状态这类枚举值前端传什么我都先映射到后端定义的枚举里映射不了就直接拒绝。错误处理统一改为“通用错误码 日志详情”。生产环境不再向前端返回异常堆栈。日志脱敏。所有涉及手机号、身份证号、支付账户的字段统一用掩码工具处理后再写日志。这一步的好处是结构清晰之后我能准确说出“哪个边界应该拦截什么”不需要靠“大概可能在这里拦截”这么模糊的方式做安全。4.4 改造后的效果与复盘改造完成后的三个月里没有再出现一条与越权相关的安全工单代码评审的效率也提升了不少——以前评审一个改动要通读整个 Controller 和 Service 的上下文现在只需要看改动涉及的那一个类的职责和边界。复盘时我最深的体会是原则先行的重构让安全加固变得低成本、高确定性。反过来说如果先加安全组件而不动结构那些组件的接入位置和调用关系依然是一笔糊涂账安全会和业务继续互相纠缠。5. 常见问题与排查技巧实录我把自己在这类项目里踩过的坑、排查过的问题整理成了一份速查表也把一些“看起来对但实际是坑”的实践写下来。这些都是常规文档里很少提到的。5.1 典型问题速查表典型问题症状根因正确姿势过度抽象类数量呈指数增长每个类只有两行逻辑把 YAGNI 忘了为了“通用”而抽象用到第三次再抽象抽象时必须明确服务谁单一职责被教条化一个业务操作被拆成 8 个 Service调用链像迷宫把“职责”拆得过细违背了 KISS职责拆分的颗粒度以“变更频率是否相同”为准DRY 变成“复制粘贴”三个方法里都有一模一样的三行代码写的时候图快没意识到这是逻辑重复用 code smell 工具或 Code Review 发现后立即收敛接口里塞满方法某个实现类有大量“不支持”方法接口隔离没做到位把大接口拆成多个角色接口依赖扫描形同虚设安全通报里出现自家项目引用的第三方漏洞SCA 没接入 CI或者扫描了但没人跟进接入 CI 且设阈值中危及以上阻断发布错误信息泄露细节前端能看到 SQL 语句和内部路径没有统一异常处理堆栈直接返回统一异常拦截器返回通用错误码详情只进日志这张表看着简单但每一项背后都是真实发生过的事故。我特别想强调最后一行错误信息泄露在 OWASP Top 10 里属于“安全日志与监控失效”的相邻问题它的危害不是直接利用而是降低后续攻击的难度因此一定要从根上处理。5.2 快速排查技巧在代码评审里抓安全和设计问题有些同事问我代码评审时怎么快速发现问题。我分享几个自己惯用的判断技巧其一看方法签名。如果一个方法有 6 个以上参数或者一个参数是 Map/JSON 对象背后大概率存在职责混乱或接口设计不良的问题安全校验也随之变得不可控。其二看类的方法个数和行数。超过 300 行且方法极多这个类几乎必然违反单一职责。把这种类展开里面一定绕不开直接拼 SQL、直接写日志、直接调外部服务等问题安全风险自然水涨船高。其三找硬编码的“关键字符串”。比如直接把用户 ID 从会话里取出来然后从请求参数里拼进 SQL这类代码的安全风险非常高看到就应该标出来。其四顺着数据流找出口。从一个 HTTP 入口开始看参数怎么流转有没有经过校验、有没有经过权限判断、有没有经过转义。如果入口到出口之间没有任何校验层基本已经可以判定这条链路存在注入或越权风险。5.3 我踩过的几个坑与心态调整第一个坑把“不写安全代码”当成 YAGNI。有时候为了赶进度想着“这个页面只有内部人员用不需要严格校验”于是跳过权限判断。结果后来链路改了内部页面变成对外页面漏洞立刻暴露。我现在的心态是安全底线不能用 YAGNI 去砍——YAGNI 解决的是“功能需求预测”而安全是“已知风险防控”两者不是一回事。第二个坑为了实现“绝对 DRY”而过度抽象。我有一次为了保证某个校验逻辑全局唯一把它封装成一个注解加切面的方式代码确实少了。但问题是新同事根本不知道这个注解在哪里声明也不知道如何覆盖它排查问题时花了大量时间找这个切面的逻辑。这告诉我DRY 是好东西但抽象必须保持对团队认知水平的适配。如果团队的认知水平暂时达不到某种抽象的复杂度即使代码物理上满足 DRY也是“伪 DRY”。第三个坑安全扫描工具出了报告不闭环。前文提到的依赖扫描很多时候止步于“生成了一堆报告”但没有真正的处理机制。现在我把这三步做成强制流程扫描报告必须有人确认中危以上漏洞必须有修复计划或风险验收下个迭代必须确认修复结果。没有这个闭环扫描工具只是“做了个样子”。6. 最后分享一个习惯除了上面的经验我还有一个一直坚持的习惯每次新建模块时在 README 里写清楚“这个模块为什么只用这些技术方案不引入更多依赖”。刚开始同事觉得多此一举后来发现这其实是 KISS 和 YAGNI 的很好落地方式——每个不合理的设计都要能回到这里找到当时的决策依据。还有一件事也值得多说一句代码原则与安全实践不是一次性的“大改造”而是要在日常开发中持续维护。我目前比较认可的做法是每个 Merge Request 的评审清单里固定包含“是否新增了重复逻辑”“是否引入了不必要的依赖”“接口是否校验了越权”“错误信息是否会泄露细节”这几项。这些问题不需要全部回答“是”但必须有人有意识地回答。长期坚持下来代码库的质量和安全水平是能明确感知到在提升的。我自己在这个领域带过一个模拟项目经历了从“能跑就行”到“可维护、可审计、对抗已知威胁”的转变前后跨度将近一年。那一年最大的收获不是学会了哪条原则或哪个安全配置而是明白了一个道理原则是给未来省钱的安全是给未来兜底的两者都不能靠“将来再说”来赌。希望这篇复盘对正在做代码质量改进和安全加固的你有参考价值。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑