Java 对第三方提供通用接口设计

2021年11月22日 阅读数:7
这篇文章主要向大家介绍Java 对第三方提供通用接口设计,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

一. 前言

    在软件开发中,每每须要给第三方提供接口服务,通常经过SOAP协议或者HTTP协议来传输数据,本文不对SOAP协议进行研究,针对HTTP协议进行对外接口经过设计,不过设计思想能够通用。java

二. 设计

    1.  首先系统会建立一个帐号:密钥id,密钥secret,有效结束时间,状态(0:正常,1:停用),访问方法集合(空便可访问所有接口),签名sign则是经过必定的规则产生。web

    2. 先设计一个通用接收字段算法

字段 类型 说明 备注
accessKeyId String 密钥ID  
sign String 签名  
accessDate String 访问时间 yyyy-MM-dd HH:mm:ss(访问时间不能与服务器时间相差太多,具体差值系统设置)

    3. 签名加密算法定义(可简可复,能够自定义调节)apache

        简单签名定义以下:api

        accessDateStr为字符类型格式化服务器

        sign = md5(accessKeySecret + accessKeyId + accessKeySecret + accessDateStr)app

    4. 帐号受权,系统能够设置每一个方法的权限,若是该帐号没有被赋予接口访问权限,则不容许访问。工具

    5. 核验数据有效性,对每条数据都必须进行有效性核验,具体验证流程:测试

        1)验证帐号是否存在ui

        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.管理后台