一. 前言
在软件开发中,往往需要给第三方提供接口服务,一般通过SOAP协议或者HTTP协议来传输数据,本文不对SOAP协议进行研究,针对HTTP协议进行对外接口通过设计,不过设计思想可以通用。
二. 设计
1. 首先系统会创建一个账号:密钥id,密钥secret,有效结束时间,状态(0:正常,1:停用),访问方法集合(空即可访问全部接口),签名sign则是通过一定的规则产生。
2. 先设计一个通用接收字段
字段 | 类型 | 说明 | 备注 |
---|---|---|---|
accessKeyId | String | 密钥ID | |
sign | String | 签名 | |
accessDate | String | 访问时间 | yyyy-MM-dd HH:mm:ss(访问时间不能与服务器时间相差太多,具体差值系统设置) |
3. 签名加密算法定义(可简可复,可以自定义调节)
简单签名定义如下:
accessDateStr为字符类型格式化
sign = md5(accessKeySecret + accessKeyId + accessKeySecret + accessDateStr)
4. 账号授权,系统可以设置每个方法的权限,如果该账号没有被赋予接口访问权限,则不允许访问。
5. 核验数据有效性,对每条数据都必须进行有效性核验,具体验证流程:
1)验证账号是否存在
2)验证账号是否有效
3)验证账号是否到期
5)验证是否有接口访问权限
6)验证访问时间是否有效
7)验证签名是否有效
6. 接口访问数据记录,对每次接口访问的数据单独进行日志记录。
三. 签名生成方式
对所有API请求参数(包括公共参数和业务参数,但除去sign参数和byte[]类型的参数),根据参数名称的ASCII码表的顺序排序。
如:foo:1, bar:2, foo_bar:3, foobar:4排序后的顺序是bar:2, foo:1, foo_bar:3, foobar:4。
将排序好的参数名和参数值拼装在一起,根据上面的示例得到的结果为:bar2foo1foo_bar3foobar4。
把拼装好的字符串采用utf-8编码,使用MD5算法摘要。在拼装的字符串前后加上accessSecret后,再进行摘要,如:md5(secret+bar2foo1foo_bar3foobar4+secret)
将摘要得到的字节流结果使用十六进制表示,如:hex(“helloworld”.getBytes(“utf-8”)) = “68656C6C6F776F726C64”
public static String genSign(Map<String, String> map, String secret) {
Map<String, String> sMap = sortByKey(map);
StringBuffer buffer = new StringBuffer(secret);
for (Map.Entry<String, String> itm : sMap.entrySet()) {
buffer.append(itm.getKey()).append(itm.getValue());
}
buffer.append(secret);
log.info(buffer.toString());
try {
return StringUtils.upperCase(DigestUtils.md5DigestAsHex(buffer.toString().getBytes("utf-8")));
} catch (Exception e) {
}
return null;
}
private static Map<String, String> sortByKey(Map<String, String> map){
// 创建一个带有比较器的TreeMap
Map<String, String> treeMap = new TreeMap<>(String::compareTo);
// 将你的map传入treeMap
treeMap.putAll(map);
return treeMap;
}
四. 测试
1. 正常访问
{
"accessKeyId": "a123456",
"sign": "f9595449a3799d938b8d255cde3d6b9c",
"accessDate": "2020-03-01 10:30:00",
"nm": "测试数据名称"
}
{
"code": 0,
"data": {
"sign": "f9595449a3799d938b8d255cde3d6b9c",
"accessKeyId": "a123456",
"accessDate": "2020-03-01 10:30:00",
"nm": "测试数据名称"
}
}
2. 用户密钥不存在
{
"accessKeyId": "a1234569999999",
"sign": "f9595449a3799d938b8d255cde3d6b9c",
"accessDate": "2020-03-01 10:30:00",
"nm": "测试数据名称"
}
{
"code": 400,
"msg": "用户密钥不存在"
}
3. 签名不正常
{
"accessKeyId": "a123456",
"sign": "f9595449a3799d938b8d255cde3d6b9c1",
"accessDate": "2020-03-01 10:30:00",
"nm": "测试数据名称"
}
{
"code": 400,
"msg": "签名不正确"
}
4. 访问时间错误,现在时间为2020-03-01 10:30:00
{
"accessKeyId": "a123456",
"sign": "f9595449a3799d938b8d255cde3d6b9c",
"accessDate": "2020-03-01 10:10:00",
"nm": "测试数据名称"
}
{
"code": 400,
"msg": "请求时间过于提前"
}
{
"accessKeyId": "a123456",
"sign": "f9595449a3799d938b8d255cde3d6b9c",
"accessDate": "2020-03-01 10:50:00",
"nm": "测试数据名称"
}
{
"code": 400,
"msg": "请求时间过于延后"
}
还有其他错误返回就不一一列举了
五. 实现
1. 签名基础类
/**
* <p>
* 签名基础类
* </p>
*
* @author yuyi (1060771195@qq.com)
*/
@SuppressWarnings("deprecation")
@Data
public class BaseSignRo implements Serializable {
private static final long serialVersionUID = 8126572563688838556L;
@ApiModelProperty(value = "签名")
@NotEmpty(message = "签名不能为空", groups = {AddGrp.class, UpdGrp.class})
private String sign;
@ApiModelProperty(value = "密钥ID")
@NotEmpty(message = "密钥ID不能为空", groups = {AddGrp.class, UpdGrp.class})
private String accessKeyId;
@ApiModelProperty(value = "访问时间")
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
// @JsonDeserialize(using = DateJsonDeserializer.class)
@NotEmpty(message = "访问时间不能为空", groups = {AddGrp.class, UpdGrp.class})
private Date accessDate;
}
2. 测试业务类
/**
* <p>
* 测试签名
* </p>
*
* @author yuyi (1060771195@qq.com)
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TestSignRo extends BaseSignRo {
private static final long serialVersionUID = 5811444046840617970L;
@ApiModelProperty(value = "测试参数")
private String nm;
}
3. 签名验证类
package yui.comn.web.utils;
import java.text.ParseException;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import lombok.extern.slf4j.Slf4j;
import yui.bss.sys.en.SysApiEn;
import yui.comn.api.co.SysApiCo;
import yui.comn.api.ro.BaseSignRo;
import yui.comn.utils.BssExpUtils;
import yui.comn.utils.DateUtils;
import yui.comn.utils.HttpRequestUtils;
import yui.comn.utils.MD5Util;
/**
* <p>
* 签名验证工具类
* </p>
*
* @author yuyi
*/
@Slf4j
public class SignUtils {
public static String prodSign(BaseSignRo signRo, String accessKeySecret) {
// String accessDateStr = DateUtils.format(signRo.getAccessDate(), DateUtils.FULL_ST_FORMAT);
return MD5Util.encode(String.format("%s%s%s%s", accessKeySecret,
signRo.getAccessKeyId(), accessKeySecret, signRo.getAccessDate()));
}
public static void checkSign(BaseSignRo signRo, SysApiCo apiCo) {
// 验证账号是否存在
checkAccessKey(apiCo);
// 验证账号是否有效
checkStatus(apiCo);
// 验证账号是否到期
checkVldToTm(apiCo);
// 验证是否有接口访问权限
checkMethod(apiCo);
// 验证访问时间是否有效
checkAccessDate(signRo);
// 验证签名是否有效
checkSign(signRo, apiCo.getAkSecret());
}
private static void checkSign(BaseSignRo signRo, String accessKeySecret) {
String sign = prodSign(signRo, accessKeySecret);
if (!StringUtils.equals(sign, signRo.getSign())) {
BssExpUtils.error("签名不正确", log);
}
}
private static void checkAccessKey(SysApiCo apiCo) {
if (null == apiCo) {
BssExpUtils.error("用户密钥不存在", log);
}
}
private static void checkStatus(SysApiCo apiCo) {
if (apiCo.getStatus() == SysApiEn.Status.DISABLE.cd()) {
BssExpUtils.error("用户密钥停用", log);
}
}
@SuppressWarnings("deprecation")
private static void checkMethod(SysApiCo apiCo) {
String methodStr = apiCo.getMethod();
if (StringUtils.isNotBlank(methodStr)) {
HttpServletRequest request = HttpRequestUtils.getHttpServletRequest();
String reqtMethod = StringUtils.replaceAll(StringUtils.substring(request.getRequestURI(), 1), "/", ".");
methodStr = StringUtils.replaceAll(methodStr, ",", ",");
String[] methods = StringUtils.split(methodStr, ",");
boolean authz = false;
for (String method : methods) {
if (StringUtils.equals(StringUtils.trim(method), reqtMethod)) {
authz = true;
break;
}
}
if (!authz) {
BssExpUtils.error("没有访问该方法权限", log);
}
}
}
private static void checkAccessDate(BaseSignRo signRo) {
Date accessDate = DateUtils.formatDate(signRo.getAccessDate(), DateUtils.FULL_ST_FORMAT);
Date ftDateBeg = DateUtils.getDate(accessDate, 0, 0, 0, 0, -10, 0); //减去X分钟
Date ftDateEnd = DateUtils.getDate(accessDate, 0, 0, 0, 0, 10, 0); //增加X分钟
if (DateUtils.compareMill(ftDateBeg, DateUtils.getCurrentTime()) < 0) {
BssExpUtils.error("请求时间过于延后", log);
}
if (DateUtils.compareMill(ftDateEnd, DateUtils.getCurrentTime()) > 0) {
BssExpUtils.error("请求时间过于提前", log);
}
}
private static void checkVldToTm(SysApiCo apiCo) {
Date vldToTm = apiCo.getVldToTm();
if (null != vldToTm && DateUtils.compareMill(vldToTm, DateUtils.getCurrentTime()) > 0) {
BssExpUtils.error("账号到期", log);
}
}
}
4. 接口实现
/**
* <p>
* 系统测试接口
* </p>
*
* @author yuyi (1060771195@qq.com)
*/
@Api(value="系统测试")
@RestController
@RequestMapping("sys/test")
public class SysTestController extends BaseController {
@Reference
private SysApiMgr sysApiMgr;
@Log(type = LogType.SYS_API_LOG)
@ApiOperation(value = "测试签名请求")
@PostMapping("api")
public Object api(@RequestBody TestSignRo signRo) {
SysApiCo sysApiCo = sysApiMgr.getSysApiCo(signRo.getAccessKeyId());
SignUtils.checkSign(signRo, sysApiCo);
return build(signRo);
}
}
5.管理后台