最近,公司有一些接口需要对合作方开放,需要做参数加密以及合作方身份认证,下面是我的做法:
思想:因为部分接口需要加密,所以通过自定义注解,加上注解的接口才进行校验。
我们会为合作方下发 partnerId 和 privateKey ,客户端和服务端保留这两个信息。
加密方式:
项目加密方式
1,必须提交的参数
POST提交
signature: 签名 //必填
partnerId: 合作者ID //必填
timestamp : 调用接口时的时间戳 单位秒 如 1489660879 //必填
业务参数
2,签名生成规则
将参数(业务参数+partnerId+timestamp+privateKey)按照A-Z的顺序排序后按照KEY=VALUE的方式连接成字符串。
如aa=2323operator=ltpageIndex=1pageSize=10partnerId=6079410844privateKey=4EUJ7HEV8UEEFS5A0dxgtimestamp=1565235925
然后将该字符串MD5后截取3-28位后用sha1加密
sha1(substr(md5(aa=2323operator=ltpageIndex=1pageSize=10partnerId=6079410844privateKey=4EUJ7HEV8UEEFS5A0dxgtimestamp=1565235925),3,28))
以上参数
timestamp可以有效防止别人抓包进去模拟请求,或者脚本批量执行已经出现的请求
signature可以防止别人调用自己的api
privateKey作为秘钥,不允许在网络上传递
partnerId作为合作者唯一标识
具体验证代码如下:
package com.haoma.number.encrypt;
import com.google.common.collect.Lists;
import com.haoma.api.number.entity.auth.AuthUserEntity;
import com.haoma.number.service.auth.IAuthUserService;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* @ClassName WebSignature
* @Description: 调用接口进行验证签名
* @Author yanbo
* @Date 2019/8/7 15:19
**/
public class WebSignature {
/**
* 进行验签看用户是否合法、参数是否合法、是否超时
*
* @param signature 客户端签名后的值
* @param appId 即:下发给第三方调用者的唯一表示
* @param timestamp 时间戳 -秒级
* @return 合法返回空字符;否则返回错误原因
*/
@SuppressWarnings("unchecked")
public static String signature(String signature, String partnerId, IAuthUserService authUserService, String timestamp,
Map<String, String[]> params) {
String errorMsg = "";
if (StringUtils.isBlank(signature)) {
errorMsg = "auth signature blank error";
return errorMsg;
}
if (StringUtils.isBlank(partnerId)) {
errorMsg = "auth partnerId blank error";
return errorMsg;
}
if (StringUtils.isBlank(timestamp)) {
errorMsg = "auth timestamp blank error";
return errorMsg;
}
if (timestamp.trim().length() != 10) {
errorMsg = "auth timestamp length error,unit min ";
return errorMsg;
}
//时间校验,前后最多差4min
long curSeconds = System.currentTimeMillis() / 1000;
Long minMillisDiff = (curSeconds -
NumberUtils.toLong(timestamp, curSeconds)) * 1000;
if (minMillisDiff > 4 * 60 * 1000 || minMillisDiff < -4 * 60 * 1000) {
errorMsg = "auth timestamp fail !";
return errorMsg;
}
//校验partnerId合法性
AuthUserEntity authUserEntity = authUserService.findByPartnerId(partnerId);
if (authUserEntity == null) {
errorMsg = "partnerId is invalid!";
return errorMsg;
}
params.remove("signature");
params.put("privateKey", new String[]{authUserEntity.getPrivateKey()});
//1:排序
List<String> keys = Lists.newArrayList(params.keySet());
Collections.sort(keys, String::compareTo);
//2:拼接
String keyValues = "";
for (int i = 0; i < keys.size(); ++i) {
String key = keys.get(i);
String[] val = params.get(key);
if (val.length > 0) {
keyValues = keyValues + (key + "=" + StringUtils.join(Lists.newArrayList(val), ","));
} else {
keyValues = keyValues + (key + "=");
}
}
//3.加密 按字母顺序排序后格式如下 取3-28位
//signature = sha1(substr(md5(adf=123123bb=56863passwd=123123privateKey=pikb25cc12ee8e677418c738460b05timestamp=1489659289180uname=yanboparam5=sadfasdf),3,28))
String serverSignature = DesUtil.SHA1(DesUtil.getMd5(keyValues).substring(3, 28));
if (!signature.equalsIgnoreCase(serverSignature)) {
errorMsg = "auth signature fail !";
}
return errorMsg;
}
}
自定义注解类:
package com.haoma.util.annotation;
import java.lang.annotation.*;
/**
* @ClassName MustAuthToken
* @Description: 需要authToken验证
* @Author yanbo
* @Date 2019/8/6 15:19
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SignatureAuth {
}
拦截器:
package com.haoma.number.interceptor;
import com.alibaba.fastjson.JSON;
import com.haoma.api.util.bo.ResultBo;
import com.haoma.number.encrypt.WebSignature;
import com.haoma.number.service.auth.IAuthUserService;
import com.haoma.util.annotation.SignatureAuth;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* @author yanbo
* @ClassName: AuthSignInterceptor
* @Description: 进行验签看用户是否合法、参数是否合法、是否超时
* @date 2019年8月8日 下午5:08:48
*/
@Component
public class AuthSignInterceptor extends HandlerInterceptorAdapter {
private static Logger logger = LoggerFactory.getLogger(AuthSignInterceptor.class);
@Autowired
private IAuthUserService authUserService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
SignatureAuth signatureAuth;
if (handler instanceof HandlerMethod) {
signatureAuth = ((HandlerMethod) handler).getMethodAnnotation(SignatureAuth.class);
} else {
return true;
}
//1:加密校验
if (signatureAuth != null) {
logger.trace("=======preHandle=======" + request.getRequestURI());
String signature = request.getParameter("signature");
String partnerId = request.getParameter("partnerId");
String timestamp = request.getParameter("timestamp");
String requestUrl = request.getRequestURI();
logger.info("请求地址:" + requestUrl);
Map<String, String[]> params = request.getParameterMap();
String errorMsg;
if (StringUtils.isNotEmpty(errorMsg = WebSignature.signature(signature, partnerId, authUserService, timestamp, params))) {
ResultBo result = ResultBo.error(errorMsg);
//要加这个否则服务器返回的不是json,有的浏览器还乱码
//response.setContentType(ServletUtils.JSON_TYPE);
response.getWriter().println(JSON.toJSONString(result));
response.getWriter().flush();
response.getWriter().close();
return false;
}
}
// 只有返回true才会继续向下执行,返回false取消当前请求
return true;
}
}
拦截器生效:
@Configuration
public class NumberWebConfig implements WebMvcConfigurer {
private final AuthSignInterceptor authSignInterceptor;
@Autowired
public NumberWebConfig( AuthSignInterceptor authSignInterceptor) {
this.authSignInterceptor = authSignInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//参数校验拦截器
InterceptorRegistration authSign = registry.addInterceptor(authSignInterceptor);
authSign.addPathPatterns("/**");
}
}
加密工具方法
package com.haoma.number.encrypt;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class DesUtil {
public static String getMd5(String text) {
try {
char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f'};
MessageDigest md = MessageDigest.getInstance("md5");
md.update(text.getBytes("UTF-8"));
byte[] bytes = md.digest();
int j = bytes.length;
char[] c = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte b = bytes[i];
c[k++] = hexDigits[b >>> 4 & 0xf];
c[k++] = hexDigits[b & 0xf];
}
return new String(c);
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
//SHA1 加密算法
public static String SHA1(String decript) {
try {
MessageDigest digest = MessageDigest
.getInstance("SHA-1");
digest.update(decript.getBytes());
byte messageDigest[] = digest.digest();
// Create Hex String
StringBuffer hexString = new StringBuffer();
// 字节数组转换为 十六进制 数
for (int i = 0; i < messageDigest.length; i++) {
String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
if (shaHex.length() < 2) {
hexString.append(0);
}
hexString.append(shaHex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return "";
}
}