方案
timestamp+uid方案
uid指唯一的随机字符串,用来标识每个被签名的请求。通过为每个请求提供一个唯一的标识符,服务器能够防止请求被多次使用(记录所有用过的nonce以阻止它们被二次使用)。
流程说明
签名生成
1、调用方对上行的业务参数进行Aes方式加密,密钥由被调用方提供
2、并按照请求参数名(不包含时间戳)的字母升序排列非空请求参数,最后拼接时间戳timestamp,使用URL键值对的格式(即a=valuea&b=valueb…)拼接成字符串
3、进行MD5运算,并将得到的字符串所有字符转换为大写,得到sign值。
实现
业务参数加密示例
//AES加密后的手机号码
String phoneNum = URLEncoder.encode(Base64Util.encrypt("18618115555",secretKey,uid),CharCodingUtil.INPUT_CHARSET);
加解密方法
/** 解密密钥 */
protected final static String key = "~!@#$13#";
/**
* 加密
* @param input
* @param key
* @param iv 为uid
* @throws Exception
*/
public static String encrypt(String input, String key, String iv) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec k = new SecretKeySpec(key.getBytes(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, k, new IvParameterSpec(iv.getBytes()));
byte[] e = cipher.doFinal(input.getBytes("utf-8"));
return new String(Base64.encodeBase64(e));
}
/**
* 解密
*/
public static String decrypt(String base, String key, String iv) throws Exception {
byte[] keyBytes = Base64.decodeBase64(base);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec k = new SecretKeySpec(key.getBytes(), "AES");
cipher.init(Cipher.DECRYPT_MODE, k, new IvParameterSpec(iv.getBytes()));
byte[] e = cipher.doFinal(keyBytes);
return new String(e, "UTF-8");
}
生成签名代码示例
String tempParam ="deviceid="+deviceid+"&from="+from+"&phonenum="+phonenum+"&uid="+uid+"×tamp="+time;
String sign=DigestUtils.md5Hex((tempParam+accessKey+uid+time).getBytes("UTF-8")).toUpperCase();
http post请求示例
Map<String, String> params = Maps.newHashMap();
//16位随机串
String uid = RandomStringUtils.randomAlphanumeric(16);
params.put("uid", uid);
//时间戳
String time = System.currentTimeMillis()+"";
params.put("timestamp",time);
//AES加密后的手机号码
String phoneNum = URLEncoder.encode(Base64Util.encrypt("18618115555",secretKey,uid),CharCodingUtil.INPUT_CHARSET);
params.put("phonenum", phoneNum);
//AES加密后的渠道来源
String from = URLEncoder.encode(Base64Util.encrypt("QUTOUTIAO",secretKey,uid),CharCodingUtil.INPUT_CHARSET);
params.put("from",from);
//AES加密后的设备ID
String deviceid = URLEncoder.encode(Base64Util.encrypt("devid1234567890",secretKey,uid),CharCodingUtil.INPUT_CHARSET);
params.put("deviceid",deviceid);
//根据生成规则经过MD5加密后的签名
String tempParam = "deviceid="+deviceid+"&from="+from+"&phonenum="+phoneNum+"&uid="+uid+"×tamp="+time;
String sign = DigestUtils.md5Hex(CharCodingUtil.charset(tempParam + accessKey+uid+time, CharCodingUtil.INPUT_CHARSET)).toUpperCase();
params.put("sign",sign);
//将params进行json对象序列化一次作为"reqData"的value
Map pm = Maps.newHashMap();
pm.put("reqData",JSON.toJSONString(params));
//http请求
HttpClientUtil.post("http://xxxx", pm,5000, 5000, "UTF-8");
字符编码代码示例
/** 字符编码 */
public final static String INPUT_CHARSET = "UTF-8";
/**
* 编码转换
* @param content 内容
* @param charset 编码方式
*/
public static byte[] charset(String content, String charset) {
if (charset == null || "".equals(charset)) {
return content.getBytes();
}
try {
return content.getBytes(charset);
} catch (Throwable e) {
throw new RuntimeException("指定的编码集不匹配,目前指定的编码集:" + charset);
}
}
服务方拦截
拦截代码示例
/**
* 签名基础校验
*/
protected BusiResponse verifSign(QttRequest params) throws Exception{
if(params==null){
retRes(SysCode.PARAM_ERROR,"参数为空",null);
}
//1、验证签名是否正确
String timestamp = params.getTimestamp();
String sign = params.getSign();
String uid = params.getUid();
String phonenum = params.getPhonenum();
String deviceid = params.getDeviceid();
String from = params.getFrom();
//1、判断时间戳是否符合格式要求
if (StringUtils.isEmpty(timestamp) || !StringUtils.isNumeric(timestamp)) {
LoggerUtil.info("请求时间戳不合法!");
return retRes(SysCode.PARAM_ERROR,"请求时间戳不合法",null);
}
//2、判断时间戳是否在超时
long ts = Long.parseLong(timestamp);
if (System.currentTimeMillis() - ts > expiredTime) {
LoggerUtil.info("请求过期!");
return retRes(SysCode.PARAM_ERROR,"请求过期",null);
}
//3、判断签名是否存在
if (StringUtils.isEmpty(sign)) {
LoggerUtil.info("sign签名为空,认证无法通过!");
return retRes(SysCode.PARAM_ERROR,"sign签名为空,认证无法通过!",null);
}
//4、判断手机号是否存在
if (StringUtils.isEmpty(phonenum)) {
LoggerUtil.info("phonenum为空,认证无法通过!");
return retRes(SysCode.PARAM_ERROR,"phonenum为空,认证无法通过!",null);
}
//5、判断uid是否存在 是否已经使用过
Long uidVal = r2mRedis.setnx(uid,yes);
if (StringUtils.isEmpty(uid) || (uidVal!=null && uidVal==0)) {
LoggerUtil.info("uid为空或已请求过,认证无法通过");
return retRes(SysCode.PARAM_ERROR,"uid为空或已请求过,认证无法通过!",null);
}
//6、判断from是否存在
if (StringUtils.isEmpty(from)) {
LoggerUtil.info("from为空,认证无法通过!");
return retRes(SysCode.PARAM_ERROR,"from为空,认证无法通过!",null);
}
//7、判断deviceId是否存在
if (StringUtils.isEmpty(deviceid)) {
LoggerUtil.info("deviceid为空,认证无法通过!");
retRes(SysCode.PARAM_ERROR,"deviceid为空,认证无法通过!",null);
}
/**
* 8、验签
*/
Map<String, String> map_ = (Map<String, String>) JsonUtil.toMap(JsonUtil.toJSON(params));
if(StringUtils.isBlank(params.getCpachflag())){
map_.remove("cpachflag");
}
String signStrVal = genSignStrVal(map_,timestamp);
String mysign = DigestUtils.md5Hex(CharCodingUtil.charset(signStrVal+accessKey+uid+timestamp, CharCodingUtil.INPUT_CHARSET)).toUpperCase();
if(!sign.equals(mysign)){
LoggerUtil.info("签名不匹配,mysign:{},sign:{}",mysign,sign);
return retRes(SysCode.PARAM_ERROR,"签名匹配未通过",null);
}
return retRes(SysCode.SUCCESS,"基础校验成功",null);
}
签名生成代码示例
/**
* 生成签名串值
* @param params 参数数组
* @param timestamp 时间戳
* @return 签名串值
*/
protected static String genSignStrVal(Map<String, String> params,String timestamp) throws Exception {
boolean append = false;
StringBuilder temp = new StringBuilder();
if (params!=null || params.size()<=0) {
Set<String> keysSet = params.keySet();
Object[] keys = keysSet.toArray();
Arrays.sort(keys);
for (Object key : keys) {
if(signKey.equalsIgnoreCase(key.toString()) || TIMESTAMP_KEY.equalsIgnoreCase(key.toString())) {
LoggerUtil.info("过滤的key="+key.toString());
continue;
}
if (!append) {
append = true;
} else {
temp.append("&");
}
temp.append(key).append("=");
Object value = params.get(key);
if (value == null || value.equals("")) continue;
String valueString = "";
if (null != value) {
valueString = String.valueOf(value);
}
temp.append(URLEncoder.encode(valueString,CharCodingUtil.INPUT_CHARSET));
}
}
if (append) {
temp.append("&");
}
temp.append(TIMESTAMP_KEY).append("=").append(timestamp);
LoggerUtil.info("生成的签名temp="+temp.toString());
return temp.toString();
}
业务参数解密示例
/**
* 进行解密并校验其格式
*/
String phoneNum = Base64Util.decrypt(params.getPhonenum(),secretKey,params.getUid());
if(StringUtils.isBlank(phoneNum) || !PhoneUtil.isCommonPhoneLegal(phoneNum)){
LoggerUtil.info("手机号格式异常,认证无法通过!");
return retRes(SysCode.PARAM_ERROR,"手机号格式异常",null);
}