如何设计开放平台接口与集成chatgpt
文章目录
- 如何设计开放平台接口与集成chatgpt
- 前言
- 一、Token机制
- 生成方式有哪些
- session存在的问题
- JWT如何解决session存在的问题
- 二、AppId、AppSecret
- AppId机制
- 签名机制
- 三 码上实现
- 客户端
- 注意
- 源码地址
- 配置
前言
前一段时间,突发奇想,趁着工作之余。从0开始搭建一套vue+springcloud的个人半成品作品。而其中集成chatgpt以及谷歌翻译对外提供服务访问访问使用到了开放接口方式访问,以此来记录
在正式设计开放平台前,先简单介绍下传统的Token机制做法。。
一、Token机制
Token机制,本质上就是一种身份验证机制。客户端登录成功后,服务器会生成一个令牌(也就是Token)颁发给客户端,之后客户端每次访问服务器都会携带这个令牌来表明自己的身份,从而进行用户身份验证
生成方式有哪些
-
基于对称加密算法
通过对称加密算法加密用户信息,生成Token。这种方式的优点是加密解密速度快,缺点是安全性较低,容易被攻击者破解 -
基于非对称加密算法
通过非对称加密算法生成公钥和私钥,使生成的Token更加安全可靠。私钥用于生成Token,公钥用于验证Token。这种方式的优点是安全性高,缺点是加密解密速度慢 -
基于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");
注意
- 为了方便直接放在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;}
- 其它安全性校验
我们也可以通过增加白名单、黑名单机制、限流、熔断、降级等进行安全性控制
源码地址
github
配置
- application.yml配上redis地址
- OpenAiUtil配上apiKey(chatgpt申请的key),
需要注意国内不能直接连,需要用魔法代理或者直接部署在国外服务器上访问