在软件开发中,往往需要给第三方提供接口服务,一般通过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. 接口访问数据记录,对每次接口访问的数据单独进行日志记录。
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. 签名基础类
/**
*
* 签名基础类
*
*
* @author yuyi ([email protected])
*/
@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. 测试业务类
/**
*
* 测试签名
*
*
* @author yuyi ([email protected])
*/
@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;
/**
*
* 签名验证工具类
*
*
* @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. 接口实现
/**
*
* 系统测试接口
*
*
* @author yuyi ([email protected])
*/
@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.管理后台