1.签名验证
平台提供apiKey和apiSecret给第三方,apiKey可明文传输,表明身份并用作加密参数之一,apiSecret双方都私密保存,根据apiKey查找并用作加密参数之一。
逻辑简单来说就是:双方都知道apiKey(公钥)和apiSecret(密钥),每次调用接口,双方都根据公钥和密钥进行加密,得到sign签名值,第三方把sign做为参数传给平台,平台在后台把自己加密的sign和参数sign对比是否相等即可。
1.1签名加密
为了登录时效性,需要增加参数timestamp(时间戳)以便验证是否登录过期
加密字符串可自定义拼接方式,加密方式可以选任意,比如MD5,如下
/**
* 签名 apiKey+"&"+userId+"&"+apiSecret+"&"+timestamp 拼接成的字符串, 然后进行 MD5 运算, 得到 sign 值
* @return sign
*/
public static String signature(String apiKey, String userId, String apiSecret, Long timestamp) {
StringBuffer buffer = new StringBuffer();
buffer.append(apiKey + "&").append(userId + "&").append(apiSecret + "&").append(String.valueOf(timestamp));
return Tools.md5Encrypt(buffer.toString());
}
这里得到的sign值,用于第三方调接口时与参数sign进行比较,相同则通过。
当然,签名也可以按照实际需求增加更多的参数,比如用户类型等,如下:
/**
* 签名 apiKey+"&"+userId+"&"+apiSecret+"&"+timestamp+"&"+dataType 拼接成的字符串, 然后进行 MD5 运算, 得到 sign 值
* @return sign
*/
public static String signature(String apiKey, String userId, String apiSecret, Long timestamp, Integer dataType) {
StringBuffer buffer = new StringBuffer();
buffer.append(apiKey + "&").append(userId + "&").append(apiSecret + "&")
.append(String.valueOf(timestamp) + "&").append(String.valueOf(dataType));
return Tools.md5Encrypt(buffer.toString());
}
完整代码,签名工具类
package net.firstelite.commons.util;
import org.apache.commons.lang3.StringUtils;
/**
* @Author: Larry
* @Date: 2023/5/22 11:00
* 单点登录签名工具类
*/
public class SingleUtil {
private static final int EXPIRE_TIME = 30; //过期时间 分钟
/**
* 验证是否过期
* @return
*/
public static boolean isExpire(Long timestamp) {
// if (StringUtils.isBlank(timestamp)) {
// return false;
// }
// if (!timestamp.matches("^[0-9]*$")) {
// return false;
// }
if (System.currentTimeMillis() - timestamp > EXPIRE_TIME * 60 * 1000) {
return true; //过期
}
return false;
}
/**
* 签名 apiKey+"&"+userId+"&"+apiSecret+"&"+timestamp 拼接成的字符串, 然后进行 MD5 运算, 得到 sign 值
* @return sign
*/
public static String signature(String apiKey, String userId, String apiSecret, Long timestamp) {
StringBuffer buffer = new StringBuffer();
buffer.append(apiKey + "&").append(userId + "&").append(apiSecret + "&").append(String.valueOf(timestamp));
return Tools.md5Encrypt(buffer.toString());
}
/**
* 签名 apiKey+"&"+userId+"&"+apiSecret+"&"+timestamp+"&"+dataType 拼接成的字符串, 然后进行 MD5 运算, 得到 sign 值
* @return sign
*/
public static String signature(String apiKey, String userId, String apiSecret, Long timestamp, Integer dataType) {
StringBuffer buffer = new StringBuffer();
buffer.append(apiKey + "&").append(userId + "&").append(apiSecret + "&")
.append(String.valueOf(timestamp) + "&").append(String.valueOf(dataType));
return Tools.md5Encrypt(buffer.toString());
}
public static void main(String[] args) {
System.out.println(System.currentTimeMillis());
System.out.println(signature("3e44cbf4c78d4d7e891c", "ee8f354ed8634e64bb5c", "4a8aebe1527c471296f3", 1694071099344L));
System.out.println(signature("3e44cbf4c78d4d7e891c", "6a4d6da287cf4ffd93d8", "4a8aebe1527c471296f3", 1695027256825L, 2));
}
}
1.2签名验证
第三方接入系统,调用平台接口,传参为signature定义的参数,除过apiSecret,外加1.1提到的sign值,例如获取用户登录信息接口
@RequestMapping(value = "/getUserInfo", method = RequestMethod.GET)
@ApiOperation(value = "获取登录用户信息", notes = "获取登录用户信息")
@ApiResponses({@ApiResponse(code = 200, response = UserInfo.class, message = "返回信息:点击下方Model查看注释详情")})
public Result getUserInfo(String apiKey, String userId, Long timestamp, String sign) {
Result result = new Result();
try {
if (StringUtils.isBlank(apiKey)) return fail("apiKey不能为空");
if (StringUtils.isBlank(userId)) return fail("用户ID不能为空");
if (Objects.isNull(timestamp)) return fail("时间戳不能为空");
if (StringUtils.isBlank(sign)) return fail("签名不能为空");
if (SingleUtil.isExpire(timestamp)) return fail("签名已过期,请重新尝试");
//从自身系统根据apiKey查询保密的apiSecret
RelatedSystemInfo systemInfo = systemInfoService.getOne(new LambdaQueryWrapper<RelatedSystemInfo>()
.eq(RelatedSystemInfo::getApiKey, apiKey));
if (Objects.isNull(systemInfo) || Objects.isNull(systemInfo.getApiSecret())) return fail("未找到相关apiKey");
String apiSecret = systemInfo.getApiSecret();
//将参数签名后与sign进行比较,相同则通过,反之验证失败
if (!Objects.equals(SingleUtil.signature(apiKey, userId, apiSecret, timestamp), sign))
return fail("签名验证失败");
//验证通过
//进入业务逻辑,查询用户信息,根据自身系统自定义UserInfo类
UserInfo userInfo = new UserInfo();
//...
result.setData(userInfo);
} catch (Exception e) {
e.printStackTrace();
return except(e);
}
return result;
}
到这里估计有人就问,这里的userId哪里来的,接着看第二部分,用户数据同步
2.用户数据同步
当我们的签名方式及验证都完成通过之后,就需要把平台的用户数据同步给第三方,这样双方才能知道是谁要登录系统。
2.1增加数据同步接口
这里的接口可按照需求,增加相应的参数来限制数据权限,比如用户类型,用户级别,用户所属单位等。平台不可能把所有数据都给第三方,这里的userId是平台提供给第三方的默认或者初始userId,当然也可以用apiKey代替,意义上就是用来区分需要获取哪些数据返回给第三方。
@RequestMapping(value = "/getUserData", method = RequestMethod.GET)
@ApiOperation(value = "获取用户数据", notes = "获取用户数据")
@ApiResponses({@ApiResponse(code = 200, response = UserDataVO.class, message = "返回信息:点击下方Model查看注释详情")})
public Result getUserData(String apiKey, Integer dataType, String userId, Long timestamp, String sign) {
try {
if (StringUtils.isBlank(apiKey)) return fail("apiKey不能为空");
if (Objects.isNull(dataType)) return fail("数据类型不能为空");
if (StringUtils.isBlank(userId)) return fail("用户ID不能为空");
if (Objects.isNull(timestamp)) return fail("时间戳不能为空");
if (StringUtils.isBlank(sign)) return fail("签名不能为空");
if (SingleUtil.isExpire(timestamp)) return fail("签名已过期,请重新尝试");
RelatedSystemInfo systemInfo = systemInfoService.getOne(new LambdaQueryWrapper<RelatedSystemInfo>()
.eq(RelatedSystemInfo::getApiKey, apiKey));
if (Objects.isNull(systemInfo) || Objects.isNull(systemInfo.getApiSecret())) return fail("未找到相关apiKey");
String apiSecret = systemInfo.getApiSecret();
if (!Objects.equals(SingleUtil.signature(apiKey, userId, apiSecret, timestamp, dataType), sign))
return fail("签名验证失败");
//验证通过
//进入业务逻辑,查询用户数据信息,根据自身系统自定义UserDataVO类
UserDataVO userDataVO = new UserDataVO();
//...
result.setData(userDataVO);
} catch (Exception e) {
e.printStackTrace();
return except(e);
}
return result;
}