如何设计开放平台接口与集成chatgpt

1 篇文章 0 订阅
1 篇文章 0 订阅

如何设计开放平台接口与集成chatgpt

前言

前一段时间,突发奇想,趁着工作之余。从0开始搭建一套vue+springcloud的个人半成品作品。而其中集成chatgpt以及谷歌翻译对外提供服务访问访问使用到了开放接口方式访问,以此来记录
chatgpt

谷歌翻译
在正式设计开放平台前,先简单介绍下传统的Token机制做法。。

一、Token机制

Token机制,本质上就是一种身份验证机制。客户端登录成功后,服务器会生成一个令牌(也就是Token)颁发给客户端,之后客户端每次访问服务器都会携带这个令牌来表明自己的身份,从而进行用户身份验证

生成方式有哪些

  1. 基于对称加密算法
    通过对称加密算法加密用户信息,生成Token。这种方式的优点是加密解密速度快,缺点是安全性较低,容易被攻击者破解

  2. 基于非对称加密算法
    通过非对称加密算法生成公钥和私钥,使生成的Token更加安全可靠。私钥用于生成Token,公钥用于验证Token。这种方式的优点是安全性高,缺点是加密解密速度慢

  3. 基于JWT(JSON Web Token)
    JWT是一种基于JSON的标准,用于在网络上安全地传输认证和声明信息。它包含了成为了一个数字签名,以验证信息的完整性和来源。这种方式的优点是方便、安全、灵活。缺点是Token的体积较大、需要在每次请求中携带。

而笔者作品中sso使用的就是JWT(后期详细介绍)

session存在的问题

我们知道传统项目中一般用session进行验证,而session是存在服务端的,并且保存用户登录之后的信息,同时向客户端返回一个session_id保存在Cookie中,客户端每次访问都会携带Cookie中的session_id并与服务端匹配session,从而获得相应数据。
PS:笔者之前做过代理人审批功能。代理人登录后就保存在session中一个代理人key,如果审批中优先判断是否有代理人,有则代理人优先作为节点审批人,反之则正常审批

session这种方式最大的问题就是需要服务端保存数据,如果是跨域、集群访问,需要每台机器都同步session信息,而其中的代价和成本非常大

JWT如何解决session存在的问题

他只将数据记录在客户端,它会生成一种约定好的格式数据,然后服务端把这份数据发送给客户端,客户端之后每次请求带上这份数据,通过解析JWT对其进行验证

PS:关于JWT的详细分析与实战在后面的集成sso中进行详细说明

二、AppId、AppSecret

AppId机制

实际上appId可以看作登录的用户名,AppSecret看作密码,而其本质也是一种token机制。而一般做法就是根据AppId和AppSecret并按照一定的规则来生成一套签名,当请求方带着签名值去请求提供方时,提供方就会验证这个签名,只有验证通过才会继续处理

PS: AppId要全局唯一,AppSecret配对唯一

签名机制

它的作用主要有两个

  • 数据防篡改
    也就是数据在网络传输中防止被修改了,一般使用摘要算法(如MD5加密)对原数据与接口提供方加密后的数据进行比较,看是否一致

  • 身份防冒充
    一般使用非对称加密算法+密钥对方式。即先对数据进行非对称算法计算,在使用私钥加密。 而另一方先使用公钥解密,在使用非对称算法计算,最后进行比较

三 码上实现

有了前面的理论基础,不妨来实操一把。
需求: 服务端套壳chatgpt,直接调用openAI提供的接口。
客户端调用服务端提供的开放接口

客户端

不妨先从客户端开始

@RestController
@RequestMapping(value = "/api/client")
public class ClientController {

    @Value("${client.open.appId}")
    private String appId;

    @Value("${client.open.appSecret}")
    private String appSecret;

    @Value("${client.open.path}")
    private String path;

    @GetMapping(value = "/test/gpt")
    public Result<String> testGpt(String text) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("model", "text-davinci-003");
        jsonObject.put("prompt", text);
        ResponseDTO responseDTO = null;
        try {
            responseDTO = OpenHttpUtil.post(appId, appSecret, path, jsonObject, new ArrayList<>(), null);
            if (responseDTO.getCode().equals(String.valueOf(SystemCodeEnum.K_000000))) {
                List<String> result = (List<String>) responseDTO.getData();
                return Result.success(result.toString());
            }
            return Result.fail(999,responseDTO.getMsg(),responseDTO.getData());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Result.success("success");
    }

其中appid、appSecret为每个客户端单独拥有,path为访问服务端开放平台接口,而核心就是
OpenHttpUtil.post(appId, appSecret, path, jsonObject, new ArrayList<>(), null);

public static ResponseDTO post(String appId, String appSecret, String url, Object param, String contentType, List<Header> headers, Map<String, Object> pathMap, Map<String, File> fileMap) throws IOException {
        RequestDTO requestDTO = initRequest(param);
        List<Header> headerList = initHeader(appId, appSecret, contentType, headers);
        return sendPost(url, requestDTO, contentType, headerList, pathMap, fileMap);
    }

这里我们只需要关注initHeader方法即可

    private static List<Header> initHeader(String appId, String appSecret, String contentType, List<Header> headers) {
        List<Header> headerList = new ArrayList();
        long curr = System.currentTimeMillis();
        String signStr = appSecret + appId + curr + appSecret;
        String sign = DigestUtils.md5Hex(signStr.getBytes());
        int nonce = new Random ().nextInt(100);
        headerList.add(new BasicHeader("appId", appId));
        headerList.add(new BasicHeader("sign", sign));
        headerList.add(new BasicHeader("timeStamp", String.valueOf(curr)));
        headerList.add(new BasicHeader("nonce", String.valueOf(nonce)));
        if (Objects.isNull(contentType)) {
            headerList.add(new BasicHeader("Content-Type", "application/json;charset=UTF-8"));
        } else if (StringUtils.isNotEmpty(contentType) && !contentType.contains("multipart/form-data")) {
            headerList.add(new BasicHeader("Content-Type", contentType));
        }

        if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(headers)) {
            headerList.addAll(headers);
        }

        return headerList;
    }

timestamp
该参数主要可以用来防止同一个请求参数被无限期的使用。
同时服务端也会根据该参数进行有效实践校验

  String timeStamp = request.getHeader("timeStamp");
        // 请求时间有效期校验
        long now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
        if ((now - Long.parseLong(timeStamp)) / 1000 / 60 >= 5) {
            responseDTO.setCode(String.valueOf(SystemCodeEnum.K_400005));
            responseDTO.setMsg("请求过期!");
            return true;
        }

nonce
该参数主要避免避免接口重放攻击,即客户端随机生成一个数,同样的请求只能使用一次。服务端进行控制

       String nonce = request.getHeader("nonce");
        String str = (String) redisUtil.get(appId + "_" + nonce);
        if (StringUtils.isNotEmpty(str)) {
            responseDTO.setCode(String.valueOf(SystemCodeEnum.K_400005));
            responseDTO.setMsg("请求失效!");
            return true;
        }
        redisUtil.set(appId + "_" + nonce, "1", 180L);

sign
这里为了方便,直接先用组合再用摘要算法进行加密,服务端进行比较判断
客户端

        long curr = System.currentTimeMillis();
        String signStr = appSecret + appId + curr + appSecret;
        String sign = DigestUtils.md5Hex(signStr.getBytes());

服务端

String sign = request.getHeader("sign");
        String signStr = appSecret + appId + timeStamp + appSecret;
        String signServer = org.apache.commons.codec.digest.DigestUtils.md5Hex(signStr.getBytes());
        if (!signServer.equals(sign)) {
            responseDTO.setCode(String.valueOf(SystemCodeEnum.K_400001));
            responseDTO.setMsg("验签错误!");
            return true;
        }

如果以上开放接口都验证通过,我们在调特定的服务,如chatgot

 String chat = OpenAiUtil.sendChat(prompt, "user");

注意

  1. 为了方便直接放在map中判断appSecret是否存在
    实际中我们可以单独新建一张开放平台表,将数据预热到redis中,如
CREATE TABLE `sys_common_open_api` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
  `creator_id` varchar(36) DEFAULT NULL COMMENT '创建人id',
  `creator_name` varchar(36) DEFAULT NULL COMMENT '创建人名称',
  `is_del` tinyint DEFAULT '0' COMMENT '是否删除 0:未删除 1:已删除',
  `app_id` varchar(1000) DEFAULT NULL COMMENT 'appId',
  `app_secret` varchar(1000) DEFAULT NULL COMMENT 'appSecret',
  `url` varchar(1000) DEFAULT NULL COMMENT 'url',
  `app_type` tinyint DEFAULT '0' COMMENT '注册的类型 0-翻译接口 1-chatgpt',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `app_id` (`app_id`)
) COMMENT='通用自定义开放平台注册表';

服务端

        Map<Object, Object> appIdSecretMap = redisUtil.hmget("open_app");
        String appSecret = (String) appIdSecretMap.getOrDefault(appId, "");
        if (StringUtils.isEmpty(appSecret)) {
            responseDTO.setCode(String.valueOf(SystemCodeEnum.K_400002));
            responseDTO.setMsg("无接口访问权限!");
            return true;
        }
  1. 其它安全性校验
    我们也可以通过增加白名单、黑名单机制、限流、熔断、降级等进行安全性控制

源码地址

github

配置

  1. application.yml配上redis地址
  2. OpenAiUtil配上apiKey(chatgpt申请的key),
    需要注意国内不能直接连,需要用魔法代理或者直接部署在国外服务器上访问
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值