前言
目前在企业内API的安全问题越发显得突出,如敏感信息泄露,访问控制失效,浪费系统资源等等,有很多的问题其实是可以在API设计的阶段就解决掉的。因此本篇文章就来看看一些常见的设计规范,如签名,加密等等。
建立完善的权限管控机制
目的:防止权限失效导致的敏感信息泄露。
权限问题分为三类,分别是未授权访问,水平越权,垂直越权
实现方式:
- 防范API未授权访问需要做好客户端身份认证,身份认证机制包括AK/SK、Token、Cookie、数字签名等方法,身份认证凭据需要有超时失效机制,一般情况下互联网系统身份认证凭据时间不能超过15分钟。服务端不能依据客户端发送的userid、name、role等简易身份标识判断用户身份。
- 服务端可以针对API接口创建拦截器,对请求接口进行过滤拦截,对当前用户和当前请求做权限判断。访客用户只能访问不含敏感信息的公开接口,如涉及敏感信息功能接口需要跳转到登录页面。
- 防范垂直越权同样可以针对API接口创建拦截器,对请求接口进行过滤拦截,对当前用户和当前请求做权限判断。低权限用户不能访问高权限用户的功能接口,需要切换登录用户为高权限角色。
- 对于水平越权问题本质上还是要对访问数据做权限控制或者对访问资源做用户隔离(sql语句带上userId进行操作),这样就可以保证操作不到其他人的数据了。
资源ID随机化
目的:防止遍历资源ID获取其它用户的信息
实现方式
- 资源ID随机生成,如采用雪花算法生成ID,这样可以提高攻击成本
增加时间戳校验
目的:防止重放攻击
实现方式
- 一次正常的http请求, 响应时间般在1s内完成, 基本上不会超过10s, 服务端根据API接口的响应时间设置阀值,拒绝超过设个时间阈值的客户端请求。
避免调试接口暴露
目的:防止不可控的接口上线
实现方式
- 应用系统测试类、调试类接口只允许内网测试环境使用,严禁对互联网开放,例如:Swagger接口、api-docs接口禁止在互联网环境启用。
- 应用系统监控类接口只允许内网测试环境使用,严禁对互联网开放,例如:Spring Actuator接口页面禁止在互联网环境启用。
签名
目的:防止数据被篡改
实现方法:
- 接口请求方将请求参数、时间戳和密钥拼接成一个字符串
- 使用MD5等hash算法生成签名sign
- 在请求参数或请求头中增加sign参数,传递给API接口
- API接口网关服务验证传递的sign值,与自己生成的sign值对比,若相等则认为是有效请求
时间戳的作用:防止同一次请求被反复利用,增加密钥未破解的可能性,每次请求设置合理的过期时间,如15分钟。
加密
目的:保护敏感数据,如密码、银行卡号等
实现方法:
- 使用AES对称加密算法
- 在前端使用公钥加密用户密码
- 在注册接口中使用密钥解密并做相关校验
加密方式的选择有很多,如AES, RSA等等,可以根据自己实际需要选择合适的加密方式。
IP白名单
目的:防止恶意请求。根据实际情况去做
实现方法:
- 限制请求IP
- 添加IP白名单在API网关服务上
- 防止内部服务器被攻破,需增加WAF (web防火墙软件,如ModSecurity、OpenWAF等)
限流与速率限制
应用系统运行依赖第三方服务,如运营商短信服务、宽带服务等,如果这些接口未限制调用频率,则可能导致攻击者批量请求该接口,导致资源浪费。如发送短信接口,需要请求运营商计费接口,每条短信消耗0.01元,如攻击者可能自定义超长短信内容拆分发送,或批量发送短信验证码,造成公司资金损失,或者批量发送验证码给正常用户,造成短信验证码频繁骚然客户,影响客户体验。
对一些可能产生资源浪费的接口(如发送短信,上传/下载大文件等)限制请求频率,超过一定次数(如3次)需要进行人机交互验证(图形验证码等)
目的:限制单位时间内用户的请求数量,防止API接口被频繁调用,导致敏感信息泄露,资源浪费,服务不可用。
实现方法:
- 对请求IP、请求接口、请求用户做限流
- 使用nginx,redis,gateway ,sentinel 等技术实现限流功能
请求方式
目的:根据实际业务,选择合适的请求方式,如restful风格或自定义风格等等,怎么合适怎么来
具体方法:
- 不建议让前端做主选择api请求方式,(之前遇到过不靠谱的前端非要求后端把一个删除接口从delete请求改成get接口,争论几番后原来是他不太会用axios就给后端找事)
- 个人建议,如果是非常简单的CRUD业务,可以使用restful风格,如 查询/分页接口用GET ,添加/新增接口POST ,修改/更新接口用PUT ,删除接口用 DELETE
- 遇到一些特殊的业务接口,如发布按钮,用POST请求,带上时间戳和其他请求参数;
- 再比如 前端有一个下拉框接口,下拉选择时要支持动态手输新增item (可以参考elementUI中的下拉框组件高级特性),这个时候对后端就有高要求了,要一个接口既支持返回arraylist ,也支持传入一个新item值,当前端没传item值,返回现有的arraylist,当前端输入了一个item值,后端先添加这个item值到数据库,再执行查询返回最新的arraylist给前端,这个后端逻辑不难,但是要注意得用POST接口实现,来标识该接口所做的业务是非幂等的。
参数校验
目的:拦截无效请求,保护系统资源。很多安全漏洞的发生都是由于没有进行恰当的校验产生的
实现方法:
- 校验字段是否为空、字段类型、字段长度、枚举值等
- 使用Hibernate Validator 等框架进行参数校验,使用注解如 (@NotNull @NotBlank @NotEmpty @Size @Max @Min等)对字段进行限制。
请求头设计
目的:将公共参数放入请求头中,便于后端统一拦截处理
实现方法:
- 前端登录成功后,让前端把jwt放到请求头里,后续再请求后端,后端就可以知道请求的用户等信息
- 一些重要的业务请求,让前端在请求头里加上traceId,便于后端做幂等处理
- 一些重要的资源,若没有规定的特殊的请求头,不允许上传或下载
- 一些对外开放的openApi ,限定外部请求时必须加上规定的请求头来做权限验证和请求来源识别
- 可以通过请求头中的user-agent来限定请求来源,如禁止PC端访问,只允许手机端访问
统一异常返回
目的: 避免异常返回结构不统一,便于接口维护
实现方法:
- 在SpringBoot中使用GlobalExceptionHandler处理全局异常
- 项目中有网关如SpringGateway时,所有异常通过API网关捕获并转换成统一的异常结构返回
统一封装返回
目的:以相同格式返回数据,便于前端接收处理,节省前端数据转换处理的代码
实现方法:
构造一个如下2种格式的json返回封装
{ "code": 20001, //业务返回码 "success": true, //标识请求成功/失败 "message": "查询数据成功", //返回的业务处理消息提示 "timestamp" : 1718506205 , //时间戳 "result" : [ ] //返回的数组数据,array内部还可以继续有obj } { "code": 20001, //业务返回码 "success": true, //标识请求成功/失败 "message": "查询数据成功", //返回的业务处理消息提示 "timestamp" : 1718506205 , //时间戳 "result" : { //返回的对象数据,obj内部还可以继续有array "age":28 , "hobby":["reading","working"] } }
请求日志
目的:便于快速分析和定位问题
实现方法:
- 使用过滤器、拦截器或AOP实现记录请求URL、参数、请求头、请求方式、响应数据、响应时间
- 使用traceId 在整个请求日志中打标记
- 也可以把日志转发到ELK中,后续在Kibana平台可视化查看日志
幂等设计
目的:防止多次请求产生错误数据
实现方法:
- 使用数据库唯一索引或redis保存requestId和请求参数来保证幂等性
- 如果使用了MQ组件,要在发送的mq消息内容中自定义messageId 即业务消息id ,并利用本地消息表来实现最终一致性
限制请求的数据量
目的:避免接口超时问题
实现方法:
- 限制查询接口请求查询的数据量(如一次最多返回30条记录)
- 限制批量保存接口入参的数据量(如一次最多同时保存5条)
- 超过限制直接提示用户
压测
目的:了解各接口的QPS情况,确保上线后的稳定性
实现方法:
- 使用Jmeter 或wrk 等测试工具进行压力测试
- 使用Prometheus ,Grafana等监控工具,监控api接口在测试环境的QPS
批量操作
目的:加快处理逻辑,一定程度上可以降低api超时时间
实现案例:
- 后端接口逻辑中,同类型数据执行批量保存,减少jdbc时间
- 上传接口要支持多图上传
- 删除接口要支持按ids删除1个或多个
- 大数据场景下,导入接口要支持单文件或多文件数据批量导入,后台分批次多线程处理写入
- 大数据场景下,导出接口可以分批次多线程查询数据,然后合并结果并导出excel文件
异步处理
目的:同步转异步,当前端一个接口要触发后端多个长逻辑时,经过仔细分析考量后,确认哪些逻辑可以转异步执行的,然后进行异步处理,提升复杂业务逻辑的接口性能。
实现方法:
- 利用MQ组件,一些实时性要求低的操作,把操作数据发送到MQ,然后让MQ消费者订阅后再执行操作处理。这样API接口发送MQ消息后立即返回成功,消息会由MQ消费者异步处理完成。
- 利用juc并发工具来对后端逻辑中的多个操作进行异步编排,如下
- CountDownLatch----允许一个或多个线程等待其他线程完成操作,它可以用来实现线程之间的等待和协调;
- CyclicBarrier----用于实现多个线程之间的屏障,它可以让一组线程等待,直到所有线程都到达指定的屏障点;
- CompletableFuture----jdk8提供的强大的异步编排工具,可以组合多个异步任务,实现串行执行,合并结果,异常处理等操作。
单一职责
目的:一个API接口尽量只做一个单一的业务操作。(实际开发中可能因为赶时间、降低前端难度等原因,经常要后端一个接口做多个复杂业务,这实际是项目技术债务)
设计思路:
- 尽量开发前,产品需求前后端人员有充足的会议讨论,共同设计一个 简单易用同时方便前后端开发的产品。
- 比如,原型设计中多使用 步骤条,穿梭框 等UI组件,把复杂业务流程简单化,而不是一味推给后端搞。建议原型设计多参考 AntDesignUI 和elementUI 提供的各种UI组件,大厂不是平白无故开发这些组件的,那都是为了实现各种需求,搞的设计思路。
- 不要搞方便了自己,麻烦了别人的做法。有些时候,UI的具体实现是 应该前端做es6的查询过滤排序等操作;有的时候,UI的实现中应该后端直接提供过滤、排序后的数据;还有就是 不要让前端同学生成id数据,虽然我知道前端领域有vue-uuid这种组件,但是我认为id数据应该后端产生,前端查询和使用。
- 要根据UI原型和业务需求,综合考量前后端的工作细节项。避免出现 方便了前端,麻烦了后端;或方便了后端,麻烦了前端。
数据脱敏
目的:数据部分加密后展示给前端,用来保护姓名、手机号等隐私数据,防止泄露隐私
实现方法:
- 用星号替代部分内容,如手机号132*****153
- 用预设的文本做内容替代,如 王先生/女士
完善的接口文档或API用例
目的:减少沟通成本,便于前后端对接API
具体方法:
- 使用swagger 或knife4j 实现在线API接口文档,随服务启动而启动,让前端同学自己去看去对接;不过swagger 或knife4j 都对后端代码有侵入性,如果项目没有太高性能要求,可以使用这种方式。
- 后端使用postman自测接口后,导出postman的apijson文件给前端,让前端把apijson文件导入自己的postman中,然后前端根据postman测试用例,自己开发前端axios请求逻辑;但前提是后端在使用postman自测接口时,完善的标记好字段含义,字段示例值等描述信息