这里写目录标题
一、前言
- 最近在对接外部平台的接口,对方提供了一个sdk,开发中直接引入到项目中。自己只需要组装参数,直接调用sdk中的方法就可以实现接口对接。
- sdk中包含了参数校验,加密验签,Http接口调用,还提供了各个接口入参和出参的实体类。 这些为我们减少很多的开发量,同时将一些不合规的入参直接在客户端过滤掉,减少资源浪费。还提供了一套完整的加密验签方法,避免了双发对接中因为加解密验签方法的不同导致的一些问题。很大程度上提高了我们的对接中开发联调的成本。
- sdk优点这么多,下面我们来开发一个简单的sdk吧。
二、步骤概述
-
首先我们开发一下服务端: 服务端是一个SpringBoot项目提供了一个用户列表查询的接口。入参包含调用方分唯一编号(appId),加密的入参信息(data),入参的加解密方式使用RSA非对称加密算法。
-
其次开发SDK: SDK是一个普通的maven项目,提供用户列表查询接口的Http调用方法,还有接口的参数校验,入参的加密,返回信息的解密。最后将项目打成jar包。
-
最后开发客户端调试: 客户端是一个SpringBoot项目,项目中引入上面打包好的SDK,组装参数,调用用户列表查询接口。
三、服务端开发
其实服务端很简单了,就是一个普通的@RestController接口,入参是Json格式。业务相关的入参要加密传递。这里我们服务端使用私钥加解密,客户端使用公钥加解密。
3.1代码实现
3.1.1 服务端入口(SysUserApiController)
这个类是接收客户端请求的入口,继承了ApiAbstract
package com.lh.api;
import com.alibaba.fastjson.JSONObject;
import com.lh.common.BaseResponse;
import com.lh.entity.api.HopeRequest;
import com.lh.entity.api.HopeResponse;
import com.lh.entity.common.PageModel;
import com.lh.entity.sys.SysUser;
import com.lh.entity.sys.SysUserDTO;
import com.lh.service.SysUserService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/api/user/")
public class SysUserApiController extends ApiAbstract<BaseResponse<PageModel<SysUser>>> {
@Resource
private SysUserService sysUserService;
@Override
public BaseResponse<PageModel<SysUser>> work(JSONObject o) {
return sysUserService.queryUserList(JSONObject.toJavaObject(o, SysUserDTO.class));
}
@PostMapping("/queryUserList")
public HopeResponse queryUserList(@RequestBody HopeRequest hopeRequest) {
return process(hopeRequest);
}
}
3.1.2 接口的抽象类(ApiAbstract)
这个类提供一些公用的方法,私钥的获取,入参的解密,出参的加密等。
package com.lh.api;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.lh.entity.api.HopeRequest;
import com.lh.entity.api.HopeResponse;
import com.lh.utils.RsaUtil;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public abstract class ApiAbstract<R> {
private static final Map<String, String> PRIVATE_KEY_MAP = new HashMap<>();
static {
PRIVATE_KEY_MAP.put("000001", "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKWVEnI3IDn+Q4qkX5VXfbYg3t+jCfPkYyW2OdYsck8T7F26xVgfP/cLRyTbuy/uYc/ROsns8vFQfJ5Qn08Y5nZ/KsFGKQo0Thx7WKE2BDGisgnrwb7vR4fI6vrmZDFFhriVQ4cww8eCJw3/a8YKPAv8RPx5BTTIhECg6c7xq8C5AgMBAAECgYBqqH6sUbQRhyYsnTrwsN3KPpwxxAy6LKhs91eEDTdIltXPfmp2HBMQ6BPYuy11b4FT4zuSLG5FYQoU1Gv6AFkaegxvTPXiaXLAIaeAdIPycwCYpHO3FoyUlfTDA5YuCVgXxwRhIUuJBPyFEi5nVKgadnE2vSl/sV6U7/adAWMELQJBAOi+HoI0eP33SZCiqXyJyABdXG2ZWJqN3PYWfvOafuqLSUT2WuVW2qiWZhzdMboY8FhvUVMWpdoM9LB+5yG6w68CQQC2IOXgIONfwCSwp4jDA8RRgcRkEtN8IFhtE6ezDt4Alqc5kmg0bRARSq/QDV3OxFw59jniif2XfE9TYcXlLZQXAkBYk7KsvyelEC4RO7bhTNavPNjmZUwRVhhYF+us7omK3gO+mTuyuMFzE/o81foM51zha0w6IQ5x1Chz23g/oALFAkEApU6e1xuCxPnFU4H8o+TbX4+FDb/4rIU/PWHFqMQpxCb5iUvVwR/soYVEpnY7WtznSMSwLWpJ/iEabvgjgqwn6QJAb21uWtuRTuBBQj1nTk9YgyiALHk4xSQqMEOGHFVEGQ5NAmMgSCZq6XbfwCZ4lBIyXGjrRJs6q4WsEBO9BDfdGw==");
}
/**
* 统一处理
*
* @param hopeRequest 统一请求
* @return 统一响应
*/
public HopeResponse process(HopeRequest hopeRequest) {
try {
String privateKey = getPrivateKey(hopeRequest.getAppId());
JSONObject o = before(hopeRequest.getData(), privateKey);
System.out.println(o);
R r = work(o);
return after(r, privateKey);
} catch (Exception e) {
e.printStackTrace();
log.error("ServerAbstract process exception! hopeRequest={}", hopeRequest, e);
return HopeResponse.builder()
.success(false)
.message("系统异常")
.build();
}
}
/**
* 处理前置解密
*
* @param data 请求密文
* @param privateKey 秘钥
* @return 请求参数
*/
private JSONObject before(String data, String privateKey) {
return JSON.parseObject(RsaUtil.decryptPrivate(privateKey, data));
}
/**
* 业务处理
*
* @param o 入参
* @return 返回
*/
abstract R work(JSONObject o);
/**
* 后置处理加密
*
* @param r 业务数据
* @param privateKey 秘钥
* @return 统一返回
*/
private HopeResponse after(R r, String privateKey) {
return HopeResponse.builder()
.success(true)
.message("交易成功")
.data(RsaUtil.encryptPrivate(privateKey, JSON.toJSONString(r)))
.build();
}
/**
* 通过appId获取对应的私钥,不同的接入方提供不同的公私钥。
* 实际业务开发中这些会存在文件中或者配置中心中如阿波罗,这里简单实现
*
* @param appId appId
* @return 私钥
*/
private String getPrivateKey(String appId) {
return PRIVATE_KEY_MAP.get(appId);
}
}
3.1.3 接口入参实体类(HopeRequest)
这个是接口的入参,这里简单演示,一个客户端唯一编号(用来获取对应的私钥),一个是加密的入参。
package com.lh.entity.api;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HopeRequest {
/**
* 客户端唯一编号
*/
private String appId;
/**
* 加密后业务相关的入参
*/
private String data;
}
3.1.4 接口返回实体类(HopeResponse)
这个是服务端接口的返回信息。返回的字段有成功表示,提示信息,还有业务相关的查询结果(查询到的用户信息)加密的信息。
package com.lh.entity.api;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HopeResponse {
/**
* 是否成功
*/
private boolean success;
/**
* 返回信息
*/
private String message;
/**
* 业务相关的返回信息,私钥加密之后的
*/
private String data;
}
3.1.5 具体的业务实现层(SysUserService)
这里是具体的业务层,通过入参去数据库查询对应的结果,在封装成实体类返回,这里不再深入。PageMode是用查询的结果封装的分页实体类。BaseResponse是业务层处理的结果有返回码和提示信息。其实这里不用封装这么多层,我这个是用以前的老代码简单的做个示例。
package com.lh.service;
import com.lh.common.BaseResponse;
import com.lh.entity.sys.SysUser;
import com.lh.entity.common.PageModel;
import com.lh.entity.sys.SysUserDTO;
public interface SysUserService {
BaseResponse<PageModel<SysUser>> queryUserList(SysUserDTO dto);
}
3.1.6 业务相关的入参(SysUserDTO)
这个类是真正传递业务相关查询条件的实体类。PageBase 是一个分页相关的类,参数为pageSize和pageIndex。
package com.lh.entity.sys;
import com.lh.entity.common.PageBase;
import lombok.Data;
@Data
public class SysUserDTO extends PageBase {
/**
* 昵称
*/
private String userId;
/**
* 用户名称
*/
private String userName;
/**
* 用户手机号
*/
private String userPhone;
/**
* 用户邮箱
*/
private String userEmail;
/**
* 用户状态 0、正常 1、锁定 2、注销
*/
private Integer status;
}
3.1.7 RSA加解密的工具类(RsaUtil)
package com.lh.utils;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
@Slf4j
public class RsaUtil {
/**
* 算法加解密算法
*/
private static final String ALGORITHM = "RSA";
/**
* 最大加密字节数,超出最大字节数需要分组加密
*/
private static final Integer MAX_ENCRYPT_BLOCK = 117;
/**
* 最大解密字节数,超出最大字节数需要分组解密
*/
private static final Integer MAX_DECRYPT_BLOCK = 128;
/**
* 请求报文公钥解密
*
* @param publicKeyString 公钥
* @param text 报文
* @return 加密报文
*/
public static String encryptPublic(String publicKeyString, String text) {
try {
PublicKey publicKey = getPublicKey(publicKeyString);
return encryptRSA(publicKey, text);
} catch (Exception e) {
e.printStackTrace();
log.error("RsaUtil encryptPublic exception! publicKeyString={} text={}", publicKeyString, text);
return null;
}
}
/**
* 应答报文公钥解密
*
* @param publicKeyString 公钥
* @param text 应答密文
* @return 解密报文
*/
public static String decryptPublic(String publicKeyString, String text) {
try {
PublicKey publicKey = getPublicKey(publicKeyString);
return decryptRSA(publicKey, text);
} catch (Exception e) {
e.printStackTrace();
log.error("RsaUtil decryptPublic exception! publicKeyString={} text={}", publicKeyString, text);
return null;
}
}
/**
* 请求报文私钥解密
*
* @param privateKeyString 私钥
* @param text 报文
* @return 加密报文
*/
public static String encryptPrivate(String privateKeyString, String text) {
try {
PrivateKey privateKey = getPrivateKey(privateKeyString);
return encryptRSA(privateKey, text);
} catch (Exception e) {
e.printStackTrace();
log.error("RsaUtil encryptPrivate exception! publicKeyString={} text={}", privateKeyString, text);
return null;
}
}
/**
* 应答报文私钥解密
*
* @param privateKeyString 私钥
* @param text 应答密文
* @return 解密报文
*/
public static String decryptPrivate(String privateKeyString, String text) {
try {
PrivateKey privateKey = getPrivateKey(privateKeyString);
return decryptRSA(privateKey, text);
} catch (Exception e) {
e.printStackTrace();
log.error("RsaUtil decryptPrivate exception! privateKeyString={} text={}", privateKeyString, text);
return null;
}
}
/**
* RSA 加密
*
* @param key 密钥
* @param text 原文
* @return 密文
* @throws Exception 异常
*/
private static String encryptRSA(Key key, String text) throws Exception {
// 创建加密对象
Cipher cipher = Cipher.getInstance(ALGORITHM);
// 对加密进行初始化 第一个参数是加密模式,第二个参数是你想用的公钥加密还是私钥加密
cipher.init(Cipher.ENCRYPT_MODE, key);
// 分段加密
byte[] make = doCrypt(text.getBytes(), cipher, MAX_ENCRYPT_BLOCK);
return Base64.encode(make);
}
/**
* RSA 解密
*
* @param key 密钥
* @param text 密文
* @return 明文
* @throws Exception 异常
*/
private static String decryptRSA(Key key, String text) throws Exception {
// 创建加解密对象
Cipher cipher = Cipher.getInstance(ALGORITHM);
// 对解密进行初始化 第一个参数是加密模式,第二个参数是你想用的公钥解密还是私钥解密
cipher.init(Cipher.DECRYPT_MODE, key);
//分段解密
byte[] make = doCrypt(Base64.decode(text), cipher, MAX_DECRYPT_BLOCK);
return new String(make);
}
/**
* 分段加解密
*
* @param data 要加解密的内容数组
* @param cipher 加解密对象
* @param maxBlock 分段大小
* @return 结果
* @throws IllegalBlockSizeException 异常
* @throws BadPaddingException 异常
*/
private static byte[] doCrypt(byte[] data, Cipher cipher, Integer maxBlock) throws IllegalBlockSizeException, BadPaddingException {
int inputLength = data.length;
// 标识
int offSet = 0;
byte[] resultBytes = {};
byte[] cache;
while (inputLength - offSet > 0) {
if (inputLength - offSet > maxBlock) {
cache = cipher.doFinal(data, offSet, maxBlock);
offSet += maxBlock;
} else {
cache = cipher.doFinal(data, offSet, inputLength - offSet);
offSet = inputLength;
}
resultBytes = Arrays.copyOf(resultBytes, resultBytes.length + cache.length);
System.arraycopy(cache, 0, resultBytes, resultBytes.length - cache.length, cache.length);
}
return resultBytes;
}
/**
* 获取私钥
*
* @param privateKeyString 私钥路径
* @return 私钥
*/
private static PrivateKey getPrivateKey(String privateKeyString) throws Exception {
// 创建key的工厂
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
// 创建私钥key的规则
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(privateKeyString));
// 返回私钥对象
return keyFactory.generatePrivate(keySpec);
}
/**
* 获取公钥
*
* @param publicKeyString 公钥
* @return 公钥
* @throws Exception 异常
*/
private static PublicKey getPublicKey(String publicKeyString) throws Exception {
// 创建key的工厂
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
// 创建公钥key的规则
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.decode(publicKeyString));
// 返回公钥对象
return keyFactory.generatePublic(keySpec);
}
}
3.2 启动项目
现在服务端已经开发好了,启动项目等待sdk开发好客户端调用吧~