0


API安全之路:从黑客攻防到签名认证体系

API安全之路:从黑客攻防到签名认证体系

文章目录

前情提要

​ 在系统安全方面,我们必须认识到没有绝对安全的系统。这是我在大学学习时听得最多的一句话,那时对于信息安全的理解还相当抽象。然而,随着工作经验的积累,我对这句话的理解也变得更加深刻。当一个系统足够完善时,人类往往成为其中最大的漏洞。许多电影如《我是谁》等作品也深刻地展现了这一情景。

​ 在互联网行业,我们往往首先追求让服务“跑起来”,随后再使其“跑得更好”。我们不仅要保证业务的正常运行,更要关注系统的安全性,这也是提升产品竞争力的重要一环。每隔一段时间,由于安全问题而引发的重大事故层出不穷。这种问题,不仅会带来数据丢失,不能正常提供服务带来的后果这种直接损失,也会降低用户的信任度。有些问题的发生往往因为一个不起眼的问题,这也印证了“木桶理论”,即一个体系的安全程度取决于最薄弱的一环。每一次重大事故都是给后来者的一次警示,也为整个行业的安全意识树立了鲜活的教材。

​ 作为一个开发者,我们的责任不仅是确保程序能为用户提供更好、更优质的服务,同时也需要在代码层面充分考虑系统的安全性。当然可能涉及到很多专业性很强的东西,成体系的讲起来需要丰富的经验和系统的学习,在这里主要围绕着 API 签名认证做笔记。但也会提起一些其他的可能会常用的东西。我相信这个系列会继续填坑的。在信使问题中(以知信使一定会偷看,如何保证安全的送信),我们程序员即是写信的一方也可以是不道德的信使,总会找到偷看信的办法的,只要知道怎么去保密那绝对安全就不会存在。

API 是什么

​ API(Application Programming Interface)是一组定义了软件和程序之间交互的规范和协议。它允许不同的软件系统之间进行通信和互操作。API可以用于不同编程语言、库和框架,使它们能够相互调用和交换数据。

API安全与授权中常见的问题
  1. 身份验证(Authentication):- 基本认证(Basic Authentication):客户端发送用户名和密码的组合,服务器验证后提供访问权限。 - 实现方式:在HTTP请求头部添加 Authorization 字段,值为 Basic base64Encode(username:password)。- 令牌认证(Token Authentication):客户端通过令牌来验证身份。 - 实现方式:服务器颁发令牌,客户端在每次请求中携带该令牌。常用的有JWT(JSON Web Token)。- OAuth认证:用于授权第三方应用程序访问资源所有者的资源。 - 实现方式:集成OAuth2.0认证流程,包括授权码模式、密码模式、客户端模式等。- JWT认证:使用JSON Web Tokens进行身份验证,适用于跨域身份验证和信息传输。 - 实现方式:服务器颁发带有用户信息的JWT,客户端在每次请求中携带JWT,服务器验证JWT的合法性。
  2. 授权(Authorization):- 角色和权限控制:基于用户角色或权限控制对API的访问。 - 实现方式:在用户信息中添加角色或权限信息,API通过拦截器或中间件验证用户权限。- OAuth 授权:控制第三方应用程序对资源的访问权限。 - 实现方式:OAuth2.0中的授权服务器负责颁发访问令牌,资源服务器验证令牌的合法性。- 访问令牌(Access Token):用于确定用户是否有权访问特定资源。 - 实现方式:OAuth2.0中,客户端使用访问令牌请求资源服务器,资源服务器验证令牌有效性。
  3. API密钥(API Keys):- 客户端在请求中携带API密钥,服务器验证密钥的合法性。
  4. SSL/TLS:- 配置服务器使用SSL/TLS协议来保护API的数据传输。
  5. 数字签名:- 使用私钥对请求进行签名,服务器使用公钥验证签名的合法性。
  6. 防火墙和WAF:- 配置防火墙和Web应用程序防火墙来阻止恶意请求和攻击。
  7. 黑白名单:- 维护一个IP地址的黑名单和白名单,只允许白名单中的IP访问API。
  8. 限速和配额:- 使用配额和限速策略来限制特定用户或应用程序对API的调用频率。
  9. 安全日志和监控:- 记录API的访问日志,监控异常请求,以及对API的访问情况。
  10. 安全审计:- 定期审计API的安全措施和策略,确保其符合最佳实践。
  11. 持续安全性评估:- 定期对API进行安全性评估,发现并修复潜在的安全漏洞。
  12. 异常处理:- 处理恶意请求和异常情况,确保API的稳定性和可靠性。
常见问题解释
1. 身份认证

​ 是最常见的一种,如果你要记录用户信息,或者说系统那些情况下能用,就必须做身份认证,大多数网站盈利来源为自身产品,衍生价值,比如流量大的话可以打广告,比如做平台提供服务赚钱,为用户提供服务,现有场景下很少有不需要用户不进行登录就能体验到全部共功能的平台了,如果这样唯一的来源也只剩下流量广告了,身份人证也能为用户提供更优质更个性化的服务。可以自己设计身份认证体系,同时也可以引入一些安全框架来提高身份认证的安全性可靠性。

​ 如何从身份认证上下手攻击那,最简单的一点就是伪造,在写接口进行测试的时候,有些接口需要身份认证,只要去已有请求中复制一个回来请求时携带就好了,当然在其中可以通过使用短时效的访问令牌,实现令牌的刷新机制。

2. 授权

​ 角色和权限,对不同资源划分不同的身份,只能访问自身拥有权限的数据资源,如有些接口必须登录才能访问,有些接口只有管理员权限才能使用,如有需要设计一套复杂的权限认证体系,虽然大多数系统分为用户和管理员就够了,OAuth 授权 减少了信息的授予,从而减少意外。

  1. 角色和权限划分:这是保证系统安全的基础,通过合理划分角色和分配相应的权限,可以限制用户对不同资源的访问。
  2. 登录认证:确保敏感操作只对已经认证的用户开放。
  3. 管理员权限:对于涉及到系统管理和配置的接口,限制只有管理员才能使用。
  4. OAuth授权:采用OAuth等现成的授权协议,避免自行设计认证方式,提高安全性。
  5. 最小权限原则:用户只能拥有访问所需资源的最低权限,避免过度授权。
  6. 信息授权减少:保护用户的隐私信息,只提供访问必要信息的权限。
  7. 敏感数据保护:对于敏感数据,采用加密等方式进行保护,避免泄露。
  8. 审计和监控:记录用户操作行为,及时发现异常。
  9. 系统漏洞修复:定期进行安全审计和漏洞扫描,及时修复发现的安全问题。
  10. 持续改进:随着系统的发展,及时更新认证机制,采纳最新的安全技术。
3. API 密钥

​ 远程调用第三方接口的时候最常用的方式就是这种方式了,开通第三方服务,获取密钥,使用第三方提供的 SDK 或者按照接口提供者要求的格式自己写调用。API 密钥中的 AK SK 就相当于用户名密码,那为什么不用密码做唯一标识别,嗯这个问题早先应该是存在过的,有些邮件服务就曾以密码做标识,但一旦密码泄露了,影响的是整个用户账号,而 API 密钥只影响这这一 API ,将风险降到了最低,用户的密码无论在任何时候不应该以明文存储,不在服务器之间传递。

  1. 粒度控制:API 密钥的使用范围比密码要小得多。泄露了密码可能会导致整个用户账号的安全受到威胁,而泄露了 API 密钥,只会影响到与该密钥相关的 API。
  2. 风险最小化:API 密钥的使用受到了严格的控制和权限限制,可以通过配置权限来限制某个密钥可以访问的资源和操作,从而将风险降到最低。
  3. 密码安全性:密码是用来登录用户账户的,应该以加密形式存储。如果将密码用于 API 认证,那么就需要以明文形式存储,这样会增加安全风险。
  4. 更灵活的授权机制:API 密钥可以与其他认证机制结合,实现更灵活的授权策略,例如使用 OAuth 进行授权。
  5. 更好的追溯性:API 密钥的使用可以被记录和追溯,方便进行审计和监控,及时发现异常活动。
4.黑白名单

​ 内网环境下的系统安全等级会提高一大截,毕竟要入侵第一要求就是网络环境,外网状态下根本无法访问到,就像电影中演的一样,连这你的网线去入侵你了。黑白名单和乐观锁悲观锁挺像的,黑名单认为除了名单内的都是好人可访问,白名单认为除了名单之内的都是坏人不可信。黑白名单也是最常用的安全机制之一。

有些 cp 味道了

黑名单: 世界这么大,我无条件信任任何人,除非它伤害我。

白名单: 世界这么大,我只信任你。

5.限速和配额

在面对大规模流量时可以通过以下手段对接口进行保护。

  1. 网关负载均衡:在接入层,可以使用诸如Nginx等网关,通过负载均衡策略,将流量均匀地分发给多个服务器,避免某一台服务器承受过大压力。
  2. 应用层网关负载均衡:对于Spring Cloud Gateway等应用层网关,同样可以采用负载均衡机制,将流量分散到多个微服务实例上。
  3. Redis限流器:通过在单一接口中引入Redis限流器,我们可以限制在单位时间内允许处理的请求数量,有效控制了接口的访问频率。
  4. 消息队列的削峰填谷:使用消息队列可以将突发的大量请求先暂时存储在队列中,再逐个处理,有效平滑了服务器的压力,保证了稳定运行。

​ 配额即限定总量,没了就没了,这点在第三方接口调用的时候体现的很明显。花钱买额度,要不就开通 vip 做任务,啊有点像 cpu 培养用户习惯。

限速(Rate Limiting)

  1. 定义:限速是指限制用户或应用程序在一定时间内可以访问API的次数或频率。
  2. 目的:防止恶意用户或应用程序通过大量请求消耗服务器资源,保证API的稳定性和可靠性。
  3. 实现方式: - 基于IP:根据IP地址来限制访问速率。- 基于用户/应用程序:对每个用户或应用程序进行独立的速率限制。- 令牌桶算法:通过维护一个令牌桶,用户在访问时需要获取令牌,如果令牌桶为空则无法继续访问。

配额(Quota)

  1. 定义:配额是指允许用户或应用程序在一定时间内的总访问次数或数据量。
  2. 目的:通过设定配额,可以更精细地控制用户或应用程序的访问行为,确保资源的合理利用。
  3. 实现方式: - 时间窗口:设定一个固定的时间窗口(如一分钟),在此时间窗口内用户可以进行的访问次数是有限制的。
API 签名认证设计与实现

为该篇笔记的主要内容,API 签名认证主要是用于判别用户是否有权限,以下以一个具体场景为例分析实现一套实现。

AK SK 的颁发
颁发形式
  1. 单 AK SK : 开发简单,一个用户使用一套 AK SK 使用它替代密码,开发和使用上都简单,但是 粒度控制 范围还是太大,AK SK 泄露会导致该用户在平台下的所有服务都可以被随意调用。虽然开发难度降低但是安全性也相对下降。 开发难度低,用户使用 ak sk 开发更方便,可以在用户初始值是给定 ak sk,无需用户单独去跑流程
  2. 对每一 API 接口 使用单独的 ak sk : 开发维护难度相对提高,更安全,但是却增加了用户的使用难度。
AK SK 的设计原则

参数 1: accessKey: 调用的标识 userA,userB (复杂、无序、无规律)

参数 2: secretKey: 密钥 (复杂、无序、无规律) 该参数不能放到请求头中

实际开发
用户唯一性 AK SK

现有用户表新增 accessKey secretKey 字段

altertableuseradd accessKey varchar(256)nullcomment'accessKey'after accessKey;altertableuseradd secretKey varchar(256)nullcomment'secretKey'after accessKey;

已有注册登录功能新增 accessKey secretKey字段

需要改变功能 注册时自动颁发 accessKey secretKey 给用户提供接重置 accessKey accessKey 意外泄露后重新生成。
请添加图片描述

注册功能改造

这里使用了固定盐值 + 用户唯一标示 + 随机数的方式生成的 accessKey

如果觉得大家都是同一个盐有些不保险,也可以在数据库中增加一列在用户注册的时候随机生成随机位数的数做盐值,在每次登录验证的时候查出来进行验证。

当盐值不为一之后,每次用户更改密码可以也让研制更新,问题就是麻烦,不过这种不把鸡蛋放在一个篮子里的行为确实可以增加程序的安全性。

// 2. 加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());

// 3. 插入数据
User user = new User();
user.setUserAccount(userAccount);
user.setUserPassword(encryptPassword);

Long userId = user.getId();

// 3. 分配 accessKey, secretKey
String accessKey = DigestUtil.md5Hex(SALT  + userId + RandomUtil.randomNumbers(5));
String secretKey = DigestUtil.md5Hex(SALT + userId + RandomUtil.randomNumbers(8));
user.setAccessKey(accessKey);
user.setSecretKey(secretKey);

boolean saveResult = this.save(user);

新注册用户后用户自主分配 accessKey secretKey

请添加图片描述

执行单次任务,或写一次出发的接口将原用户赋予 accessKey secretKey

这里写了个单次接口触发,触发数据执行成功,这里查出 没有 accessKey 的然后按照 id 计算 accessKey secretKey 分配。

    @GetMapping("/dataSynchronization")
    public void dataSynchronization() {
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.select("id").isNull("accessKey");

        List<User> list = userService.list(userQueryWrapper);

        if (list.isEmpty()) return;

        List<User> userListUsers = new ArrayList<>();
        list.forEach(user -> {
            User updateUser = new User();
            Long userId = user.getId();
            String accessKey = DigestUtil.md5Hex(SALT  + userId + RandomUtil.randomNumbers(5));
            String secretKey = DigestUtil.md5Hex(SALT + userId + RandomUtil.randomNumbers(8));

            updateUser.setAccessKey(accessKey);

            updateUser.setSecretKey(secretKey);
            updateUser.setId(userId);
            userListUsers.add(updateUser);
        });

        userService.updateBatchById(userListUsers);

    }

请添加图片描述

新增 accessKey secretKey 重置功能

@PostMapping("/resetAccessKey")publicBaseResponse<Boolean>resetAccessKey(@RequestBodyDeleteRequest deleteRequest,HttpServletRequest request){if(deleteRequest ==null|| deleteRequest.getId()<=0){thrownewBusinessException(ErrorCode.PARAMS_ERROR);}Boolean resetAccessKey = userService.resetAccessKey(deleteRequest, request);returnResultUtils.success(resetAccessKey);}
    @Override
    public Boolean resetAccessKey(DeleteRequest deleteRequest, HttpServletRequest request) {
        User loginUser = getLoginUser(request);
        Long loginUserId = loginUser.getId();
        String userRole = loginUser.getUserRole();
        UserRoleEnum enumByValue = UserRoleEnum.getEnumByValue(userRole);

        if (loginUserId.equals(deleteRequest.getId()) && !UserRoleEnum.ADMIN.equals(enumByValue)) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }

        QueryWrapper<User> userUpdateWrapper = new QueryWrapper<>();
        userUpdateWrapper.eq("id", loginUserId);

        User updateUser = getOne(userUpdateWrapper);
        if (updateUser == null) {
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
        }
        Long userId = updateUser.getId();
        String accessKey = DigestUtil.md5Hex(SALT  + userId + RandomUtil.randomNumbers(5));
        String secretKey = DigestUtil.md5Hex(SALT + userId + RandomUtil.randomNumbers(8));
        updateUser.setAccessKey(accessKey);
        updateUser.setSecretKey(secretKey);

        boolean flage = updateById(updateUser);
        return flage;
    }
每一个 API 一个 accessKey secretKey 需要用户申请开通
这里就简单演示以下申请开通流程,不涉及到付费,前提是我们已经拥有了管理 api 的一套流程,就是可以为用户提供一个可选择的 api 列表,在此列表中申请后,即可基于该 api 生成一份当前用户的 accessKey secretKey 
  1. 已经决定了每一个用户每一个 api 生成一份 accessKey secretKey 那么有多少解决方案那。 1. 还基于 user 表,在 user 表中新增 字段 user_api_ak_sk 以一个 建立为 Text 其中 存 json 字段, 构造一个数据结构,标记着 api 名称 api_accessKey api_secretKey 。查询时候使用模糊查询(影响性能,存储不方便),在需要查看更新之类的操作的时候从表中捞取,内存中计算拼接修改,然后更新字段,有些不方便,有点就是和用户表存储在一起,不用怕数据不一致等问题。2. 新建表维护,记录 api 有关信息用户信息,顺便在这个表内做配额,方便后期计费以及保护我们接口。3. 对于接口来源团队比较多,数据量比较大的场景对于每一个接口划分一张表记录用户_接口信息。当然优缺点都很明显,开发难度增大,对于用户量小没必要做这种事情怪费劲的。

接口信息表

create table interface_info
(
    id             bigint auto_increment comment '主键'
        primary key,
    name           varchar(256)                       not null comment '名称',
    description    varchar(256)                       null comment '描述',
    url            varchar(512)                       not null comment '接口地址',
    requestParams  text                               not null comment '请求参数',
    requestHeader  text                               null comment '请求头',
    responseHeader text                               null comment '响应头',
    status         int      default 0                 not null comment '接口状态(0-关闭,1-开启)',
    method         varchar(256)                       not null comment '请求类型',
    tags           varchar(1024)                      null comment '标签 json 列表',
    userId         bigint                             not null comment '创建人',
    createTime     datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    updateTime     datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    isDelete       tinyint  default 0                 not null comment '是否删除(0-未删, 1-已删)'
)
    comment '接口信息';

用户_接口信息表

create table user_interface_info
(
    id              bigint auto_increment comment '主键'
        primary key,
    userId          bigint                             not null comment '调用用户 id',
    interfaceInfoId bigint                             not null comment '接口 id',
    totalNum        int      default 0                 not null comment '总调用次数',
    leftNum         int      default 0                 not null comment '剩余调用次数',
    status          int      default 0                 not null comment '0-正常,1-禁用',
    accessKey    varchar(512)                           not null comment 'accessKey',
    secretKey    varchar(512)                           not null comment 'secretKey',
    createTime      datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    updateTime      datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    isDelete        tinyint  default 0                 not null comment '是否删除(0-未删, 1-已删)'
)
    comment '用户调用接口关系';

当然这块还存在一点点小问题,比如内部有好多团队,开发了好多接口,使用人数还很多,统一放到一个表里数据量太多了,管理起来也相对费劲,有什么办法能解决这个问题吗? 好吧,这根本不是我能遇见的场景,此刻只是单纯的秀一个原理上的操作。前段时间刚学会。

分表更细粒度拆分信息

请添加图片描述

create table user_interface_info_1695848994195935234
(
    id              bigint auto_increment comment '主键'
        primary key,
    userId          bigint                             not null comment '调用用户 id',
    totalNum        int      default 0                 not null comment '总调用次数',
    leftNum         int      default 0                 not null comment '剩余调用次数',
    status          int      default 0                 not null comment '0-正常,1-禁用',
    accessKey    varchar(512)                           not null comment 'accessKey',
    secretKey    varchar(512)                           not null comment 'secretKey',
    createTime      datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    updateTime      datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    isDelete        tinyint  default 0                 not null comment '是否删除(0-未删, 1-已删)'
)
    comment '用户调用接口关系_1695848994195935234';

开始实现,这里是管理员开通的,并没有引入计费模式等等,可以弄成支付开通,也可以单纯弄一个申请体验的按钮,无非是一个简单的管理系统思路去做。

    @PostMapping("/add")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<Long> addUserInterfaceInfo(@RequestBody UserInterfaceInfoAddRequest userInterfaceInfoAddRequest, HttpServletRequest request) {
        if (userInterfaceInfoAddRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        UserInterfaceInfo userInterfaceInfo = new UserInterfaceInfo();
        BeanUtils.copyProperties(userInterfaceInfoAddRequest, userInterfaceInfo);
        // 校验
        userInterfaceInfoService.validUserInterfaceInfo(userInterfaceInfo, true);
        User loginUser = userService.getLoginUser(request);
        Long userId = userInterfaceInfo.getUserId();
        // 3. 分配 accessKey, secretKey
        String accessKey = DigestUtil.md5Hex(SALT  + userId + RandomUtil.randomNumbers(5));
        String secretKey = DigestUtil.md5Hex(SALT + userId + RandomUtil.randomNumbers(8));

        userInterfaceInfo.setAccessKey(accessKey);
        userInterfaceInfo.setSecretKey(secretKey);

        userInterfaceInfo.setUserId(loginUser.getId());
        boolean result = userInterfaceInfoService.save(userInterfaceInfo);
        if (!result) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR);
        }
        long newUserInterfaceInfoId = userInterfaceInfo.getId();
        return ResultUtils.success(newUserInterfaceInfoId);
    }

成功实现一个用户对不同接口生成不同的 secretKey

请添加图片描述

分表实现不同接口存储不同的用户接口调用数据

难道每一次引入一个新的接口就要手动创建一个表吗?在之前上线的时候也会存好创表 sql 为的就是在上线的时候运维那面能执行,这种不同环境的变化只能记录执行挺麻烦的,全靠人工,但是自从上次 xml 里传递建表语句之后思路就打开了,这些事情都可以交给程序去做啊。

那我们需要什么那,模板 sql 和改造之前的管理 API 的文档,没当新增可用接口后,以模板 sql 去复刻一个新的接口。

模板怎么弄那,第一比较简单的,可以在程序中定义好字符串,空出占位符的位置,新建接口时由接口信息补充模板之后执行建表 sql 。 第二点,在数据库内创建好建表 sql ,程序查询之后加载到内存对模板上信息进行替换执行建表 sql 。当然也可以写个文件从文件读取,不过总体来说就算是一种程序写死和三方读取吧。第三点,我觉得应该有三方框架可以用。

第一种简单好实现,想改模板就得重新发版,第二点灵活性高一些,但是嗯实现难度会高一些。但是嗯维护起来会简单一些,这块不得不提上周我写的垃圾代码,有些地方没考虑扩展性,不过没有绝对合适的策略,只有风格和实际业务的考量。开始实践吧。

  1. 程序内存储

xml 中 sql

<insertid="addInterfaceTable"parameterType="long">
        create table if not exists user_interface_info_${interfaceId}
        (
            id              bigint auto_increment comment '主键'
                primary key,
            userId          bigint                             not null comment '调用用户 id',
            totalNum        int      default 0                 not null comment '总调用次数',
            leftNum         int      default 0                 not null comment '剩余调用次数',
            status          int      default 0                 not null comment '0-正常,1-禁用',
            accessKey    varchar(512)                           not null comment 'accessKey',
            secretKey    varchar(512)                           not null comment 'secretKey',
            createTime      datetime default CURRENT_TIMESTAMP not null comment '创建时间',
            updateTime      datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
            isDelete        tinyint  default 0                 not null comment '是否删除(0-未删, 1-已删)'
        )
            comment '用户调用接口关系_${interfaceId}';
    </insert><selectid="getInterfaceTable"parameterType="long"resultType="string">
        SHOW TABLES LIKE 'user_interface_info_${interfaceId}';
    </select>

接口管理新增接口时添加用户接口表建表操作,当然这样改过之后在在进行颁布 accessKey secretKey 的逻辑也需要改,需要根绝 当前 接口的实际表去查询 查询表逻辑变为 user_interface_info_ + 接口 id 。

 @Transactional
    @Override
    public Long addInterfaceInfo(InterfaceInfoAddRequest interfaceInfoAddRequest, HttpServletRequest request) {
        InterfaceInfo interfaceInfo = new InterfaceInfo();
        BeanUtils.copyProperties(interfaceInfoAddRequest, interfaceInfo);
        // 校验
        validInterfaceInfo(interfaceInfo, true);
        User loginUser = userService.getLoginUser(request);
        interfaceInfo.setUserId(loginUser.getId());

        // 插入接口信息到接口表
        boolean result = save(interfaceInfo);
        if (!result) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR);
        }

        Long interfaceInfoId = interfaceInfo.getId();

        // 创建用户接口表
        boolean success = addInterfaceTable(interfaceInfoId);
        if (!success) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "接口信息表创建失败");
        }

        return interfaceInfoId;
    }
    boolean addInterfaceTable(Long id){
        // 根据模板创建对应接口表格
        interfaceInfoMapper.addInterfaceTable(id);
        // 检查是否创建成功如果返回表名创建成功否则创建失败
        String interfaceTable = interfaceInfoMapper.getInterfaceTable(id);
        return !interfaceTable.isEmpty();
    }

请添加图片描述

当然我们确实实现了分表,但是有一个尴尬的事情发生了,如果我想要操控这些新建的分表的话需要抛弃 mybatis—plus 手写 sql 的方式去实现增删改查,获取可以通过 反射或者 sql 拦截器,当然简单的尝试之后我发现我水平没到这种程度,搜索之后发现 orm 框架和表之间多映射很少有素材,和我在一起的一个很多年开发经验的前端说,大多数框架都是为了主体业务去产生的,有时候看似最蠢的方案才是解决起来效率最高的方案,鱼皮哥提供的也是手写 sql 我纠结了一下午的是事情,如果莽起来手写 sql 早完事了。

查询数据库中的模板表结构进行替换关键字进行创建

之前写过一个食之无味弃之可惜的功能,获取数据库内所有表的表结构。第二种方案也差不多。

  1. 数据库内新建模板库create table user_interface_info_template( id bigint auto_increment comment '主键' primary key, userId bigint not null comment '调用用户 id', totalNum int default 0 not null comment '总调用次数', leftNum int default 0 not null comment '剩余调用次数', status int default 0 not null comment '0-正常,1-禁用', accessKey varchar(512) not null comment 'accessKey', secretKey varchar(512) not null comment 'secretKey', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除(0-未删, 1-已删)') comment '用户调用接口关系_template';
  2. 组合查询 <select id="getUserInterfaceInfoTemplate" resultType="map"> SHOW CREATE TABLE user_interface_info_template </select> <insert id="addInterfaceTables" parameterType="string"> ${interfaceTables} </insert>
  3. 测试 @Test void testCreatSQL(){ Map<String, String> interfaceInfoTemplate = interfaceInfoMapper.getUserInterfaceInfoTemplate(); System.out.println(interfaceInfoTemplate.toString()+"张三"); String value = interfaceInfoTemplate.get("Create Table"); String template = value.replace("template", "123456"); interfaceInfoMapper.addInterfaceTables(template); System.out.println("张三"); }结果如图,成功实现分表,当然分库分表后 accessKey secretKey 颁发机制没有变化,无非时写入到哪个数据源的问题。请添加图片描述
AK SK 的使用(校验签名)

以下案例主要列举一些,知识点和曾经基于 zelinAI 官方文档开发 SDK 的过程,简单说明签名认证机制各个参数是为了防止什么和如何实现一套自己的校验签名机制。详细了解请 点我。

https://github.com/yidiansishiyi/zelinai-client-sdk

请添加图片描述

这是当时的官方文档

https://kaokaofs.feishu.cn/docx/Mq8Hdh1YqoZorEx2cgqcnGOPnof

packagecom.yidiansishiyi.zelinaiclientsdk.utils;importcn.hutool.crypto.SecureUtil;importjava.util.ArrayList;importjava.util.Collections;importjava.util.List;importjava.util.Map;/**
 * 请求体拼接工具类
 *
 * @author sanqi
 */publicclassApiAuthUtils{publicstaticStringgenerateNonce(Map<String,String> body,String appsecret){List<String> sortedKeys =newArrayList<>(body.keySet());Collections.sort(sortedKeys);// 拼接键值对成字符串s1StringBuilder s1Builder =newStringBuilder();for(String key : sortedKeys){String value = body.get(key);if(value !=null&&!value.isEmpty()){if(s1Builder.length()>0){
                    s1Builder.append("&");}
                s1Builder.append(key).append("=").append(value);}}String s1 = s1Builder.toString();String s2 = s1 + appsecret;String md5Sign =SecureUtil.md5(s2.toString());return md5Sign.toLowerCase();}}
packagecom.yidiansishiyi.zelinaiclientsdk.client;importcn.hutool.core.lang.TypeReference;importcn.hutool.core.util.RandomUtil;importcn.hutool.http.HttpRequest;importcn.hutool.json.JSONUtil;importcom.yidiansishiyi.zelinaiclientsdk.common.BaseResponse;importcom.yidiansishiyi.zelinaiclientsdk.model.ZelinAIRequest;importcom.yidiansishiyi.zelinaiclientsdk.model.ZelinAIResponse;importjava.util.HashMap;importjava.util.Map;importstaticcom.yidiansishiyi.zelinaiclientsdk.utils.ApiAuthUtils.generateNonce;/**
 * 客户端调用接口
 *
 * @author sanqi
 */publicclassZelinaiClient{privatestaticfinalStringHOST="https://zelinai.com";privatestaticfinalStringSYNCHRONIZATION="/biz/v1/app/chat/sync";/**
     * appkey: zelinai appkey
     *
     */privatefinalString appkey;/**
     * appsecret: zelinai appsecret
     */privatefinalString appsecret;publicZelinaiClient(String appkey,String appsecret){this.appkey = appkey;this.appsecret = appsecret;}/**
     * @param zelinaiRequest 请求参数
     *
     * @return 基础返回值类封装的ai回复
     */publicBaseResponse<ZelinAIResponse>doChat(ZelinAIRequest zelinaiRequest){String url =HOST+SYNCHRONIZATION;String json =JSONUtil.toJsonStr(zelinaiRequest);String result =HttpRequest.post(url).addHeaders(getHeaderMap(zelinaiRequest)).body(json).execute().body();TypeReference<BaseResponse<ZelinAIResponse>> typeRef =newTypeReference<BaseResponse<ZelinAIResponse>>(){};returnJSONUtil.toBean(result, typeRef,false);}/**
     * 获取请求头
     *
     * @param zelinaiRequest 请求参数
     * @return 请求体
     */privateMap<String,String>getHeaderMap(ZelinAIRequest zelinaiRequest){Map<String,String> hashMap =newHashMap<>();
        hashMap.put("appkey", appkey);String nonce=RandomUtil.randomNumbers(16);
        hashMap.put("nonce", nonce);String timestamp =String.valueOf(System.currentTimeMillis()/1000);
        hashMap.put("timestamp", timestamp);String app_id = zelinaiRequest.getApp_id();String request_id = zelinaiRequest.getRequest_id();String uid = zelinaiRequest.getUid();String content = zelinaiRequest.getContent();
        hashMap.put("app_id",app_id);
        hashMap.put("request_id",request_id);
        hashMap.put("uid",uid);
        hashMap.put("content",content);
        hashMap.put("signature",generateNonce(hashMap, appsecret));
        hashMap.put("Content-Type","application/json;charset=UTF-8");return hashMap;}}

至于为啥我贴了两片代码,一是确实只有写过才能更加清晰的认识到 API 签名认证怎么用 ,二是已经写了好久了,没啥脑子与体力了。

看文档的核心内容,也就是有关鉴权这块的。
请添加图片描述

请添加图片描述

请添加图片描述

除了这些还要带请求参数。

大体说一下为什么要这么做鉴权

  1. 时间戳,为了防止一个请求用好久,ak sk 是无状态的,就是不管谁来只要这个对我就认你。如果我拦截到请求,无限制的使用这个请求更改其中的请求体,去发送,或者我啥也不改,就一遍遍的请求,虽然根据幂等性我得到的结果是一样的好似没啥意义,但是浪费你资源就是我本身的意义,是一种攻击行为,请求重放。
  2. appKey 就是 accessKey 也就是身份标示码,大概看出来你是你,就像身份证,银行卡,登录用的用户名,你得明确告诉我你是谁啊,也不能把它放到最后一个加密过的参数,这玩意就相当于亮身份牌。
  3. nonce 用于混淆下面加密签名的参数,为什么要明确的传递过来那,因为你不传递服务器也不知道啊,为什么要有它那,两种好处,第一,可以和上面的时间戳做配合防止请求重放,在服务器端允许时间戳可用范围内,随机数要唯一,可以大家的随机数都不能相同,用完给它加到集合中,新来的如果随机数在这个范围内,不好意思,我用过了,校验过不了,防止在时间单位内不断重放,那我拦截请求过去改掉他不一样不久好了,抱歉,我传递的所有参数都会以固定的顺序排序之后加密一遍,然后再传递,防止你更改,那我攻击你的时候最后一个参数也自己算一遍不久好了,不好意思,涉及到一个最重要的参数 secretKey 。这个参数只在我生成签名的时候用到。
  4. 其他业务性参数
  5. 签名:
签名算法 :

​ 签名为不可逆转的,其中包含了以上的请求体中所有参数外,还有 secretKey 参数, secretKey 这样按照服务器端加密出来的值,只有同样有着 secretKey 的服务端才能根据以上参数和固定排序以及指定的加密算法生成同样的签名。这样就完成了身份验证,讲除了 secretKey 之外的请求体也加入到签名中也起到了防止攻击方恶意拦截,更改请求参数后欺骗服务提供方服务器的命令,即使你拦截了请求,最多也就原样转发给我,而且还只在规定的时间范围内有用,并且如果我服务器端是 时间戳 + 随机数 两种模式去进行校验的话,一次请求就是单纯的一次性的,用一次请求作废。当然,这里还有涉及到服务器端做校验肯定也会耗费资源的,如果你单纯的想搞我不管有没有事情就单纯的不停攻击我,这面解决办法也只能是判断流量来源地址封 ip 进小黑屋了。

当然,签名认证是 API 接口保护的一种实现,在无需保存登录态的场景下,需要将接口暴露出来为其他人使用,当然内网环境下内部使用一般不用如此麻烦,搞个约定就好了,你带着请求体中带着 xx 我就认你。
最后一个问题,有必要对其进行分表吗? 实际上性能有多大提升,分表后开发难度会增加好多,运维难度也会增加,这个问题只有在具体的应用下才会开出真正的效果,不然就大规模的数据量详细测试,对于这块一直依赖不算太敏感,欠缺太多前置知识点,需要好好学习总结,总体来说,有些时候就是为了学习而学习,多一点技能总归也是好的,终于将这个坑填掉一部分了,接下来可以放松一些了填别的坑,简单的事情做三遍,麻烦的事情尝试三遍。

摘要算法原理(md5)
签名认证认证部分
sdk 的编写与使用
将 开发好的sdk 正式版提到中央仓库,上回文档过不了测试没发,问题还没找到

标签: mybatis 数据库 mysql

本文转载自: https://blog.csdn.net/qq_47923467/article/details/133470310
版权归原作者 糯米白茶 所有, 如有侵权,请联系我们删除。

“API安全之路:从黑客攻防到签名认证体系”的评论:

还没有评论