对外调用接口安全性需要进行接口签名的认证所以实现一个简单的接口签名验证逻辑
一、主要实现点
1.验证接口的时效性
2.验证接口的数据正确性
3.验证AB两个用户调用共同的的私钥ACCESS_KEY
二、签名规则描述:
1.接口请求参数与盐值和时间戳按照自然规则排序
2.参数必须包括当前时间戳 【timestamp】字段、盐值【salt】字段、签名【sign】字段
3.【salt】生成规则:UUID 去【-】即可
4.【sign】生成规则:自然排序的参数拼接ACCESS_KEY 然后MD5加密即可
三、具体实现
1.定义注解
/**
* 接口签名验证
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InterfaceSign {
}
2.核心AOP验证逻辑
/**
* 接口签名验证AOP
*
* @author licl
*/
@Aspect
@Component
public class InterfaceSignAspect {
/**
* ACCESS_KEY
*/
private static final String ACCESS_KEY = "!@#$%^&*()_abcd.com";
/**
* 盐值
*/
private static final String SALT_PARAM = "salt";
/**
* 签名
*/
private static final String SIGN_PARAM = "sign";
/**
* 时间戳
*/
private static final String DATE_PARAM = "timestamp";
/**
* 失效时间 秒 30分钟
*/
private static final int EXPIR_TIME_SECONDS = 30 * 60;
/**
* 失效时间 毫秒 30分钟
*/
private static final long EXPIR_TIME = 30 * 60 * 1000;
/**
* PRE_KEY
*/
private static final String PRE_KEY = "API_SIGN_";
/**
* redis
*/
@Autowired
private RedisTemplateUtil redisTemplateUtil;
@Around("@annotation(interfaceSign)")
public Object around(ProceedingJoinPoint joinPoint, InterfaceSign interfaceSign) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
response.setContentType("application/json;charset=UTF-8");
String method = request.getMethod();
if ("GET".equalsIgnoreCase(method)) {
if (getMethodSignValid(request, response)) {
return joinPoint.proceed();
}
} else if ("POST".equalsIgnoreCase(method)) {
if (postMethodSignValid(request, response)) {
return joinPoint.proceed();
}
}
return null;
}
/**
* get 验证
*/
private boolean getMethodSignValid(HttpServletRequest request, HttpServletResponse response) throws IOException {
long currentTime =System.currentTimeMillis();
String sign = request.getParameter(SIGN_PARAM);
String timestamp = request.getParameter(DATE_PARAM);
String salt = request.getParameter(SALT_PARAM);
if (StringUtils.isAnyBlank(timestamp, sign, salt)) {
JSONObject result = new JSONObject();
result.put("code", MallConsts.REST_CODE_PARAM_ERROR);
result.put("msg", "请求的参数有误");
result.put("data",null);
result.put("success", false);
response.getWriter().write(JSON.toJSONString(result));
return false;
}
Enumeration parameterNames = request.getParameterNames();
TreeMap<String, Object> params = new TreeMap<>(Comparator.naturalOrder());
while (parameterNames.hasMoreElements()) {
String key = (String) parameterNames.nextElement();
if (SIGN_PARAM.equals(key)) {
continue;
}
params.put(key, request.getParameter(key));
}
MD5 md5 = new MD5();
String localSign = md5.digestHex(requestParams(params) + ACCESS_KEY);
if (!sign.equals(localSign)) {
JSONObject result = new JSONObject();
result.put("code", MallConsts.REST_CODE_PARAM_ERROR);
result.put("msg", "签名错误");
result.put("data",null);
result.put("success", false);
response.getWriter().write(JSON.toJSONString(result));
return false;
}
try {
String value= (String)redisTemplateUtil.get(PRE_KEY + salt);
Long time = new Long(timestamp);
if (currentTime - time > EXPIR_TIME || StringUtils.isNotBlank(value)) {
JSONObject result = new JSONObject();
result.put("code", MallConsts.REST_CODE_SIGN_EXPIRED_ERROR);
result.put("msg", "签名已失效");
result.put("data",null);
result.put("success", false);
response.getWriter().write(JSON.toJSONString(result));
return false;
}
redisTemplateUtil.set(PRE_KEY + salt, salt, EXPIR_TIME_SECONDS);
} catch (Exception e) {
return false;
}
return true;
}
/**
* post 验证
*/
private boolean postMethodSignValid(HttpServletRequest request, HttpServletResponse response) throws IOException {
long currentTime =System.currentTimeMillis();
RequestWrapper requestWrapper = new RequestWrapper(request);
String requestBody = getStringFromStream(requestWrapper);
Map<String, Object> dataMap = (Map<String, Object>) JSON.toJavaObject(JSONObject.parseObject(requestBody), Map.class);
TreeMap<String, Object> params = new TreeMap<>(Comparator.naturalOrder());
for (String key : dataMap.keySet()) {
if (SIGN_PARAM.equals(key)) {
continue;
}
params.put(key, dataMap.get(key));
}
String sign = (String) dataMap.get(SIGN_PARAM);
String timestamp = dataMap.get(DATE_PARAM) == null ? null : dataMap.get(DATE_PARAM) + "";
String salt = (String) dataMap.get(SALT_PARAM);
if (StringUtils.isAnyBlank(timestamp, sign, salt)) {
JSONObject result = new JSONObject();
result.put("code", MallConsts.REST_CODE_PARAM_ERROR);
result.put("msg", "请求的参数有误");
result.put("data",null);
result.put("success", false);
response.getWriter().write(JSON.toJSONString(result));
return false;
}
MD5 md5 = new MD5();
String localSign = md5.digestHex(requestParams(params) + ACCESS_KEY);
if (!sign.equals(localSign)) {
JSONObject result = new JSONObject();
result.put("code", MallConsts.REST_CODE_SIGN_ERROR);
result.put("msg", "签名错误");
result.put("data",null);
result.put("success", false);
response.getWriter().write(JSON.toJSONString(result));
return false;
}
try {
String value= (String)redisTemplateUtil.get(PRE_KEY + salt);
Long time = new Long(timestamp);
if (currentTime - time > EXPIR_TIME || StringUtils.isNotBlank(value)) {
JSONObject result = new JSONObject();
result.put("code", MallConsts.REST_CODE_SIGN_EXPIRED_ERROR);
result.put("msg", "签名已失效");
result.put("data",null);
result.put("success", false);
response.getWriter().write(JSON.toJSONString(result));
return false;
}
redisTemplateUtil.set(PRE_KEY + salt, salt, EXPIR_TIME_SECONDS);
} catch (Exception e) {
return false;
}
return true;
}
/**
* 参数格式化
*/
private static String requestParams(TreeMap<String, Object> treeMap) {
return treeMap.toString()
.replaceAll(", ", "&")
.replaceAll("[{}]", "");
}
private String getStringFromStream(HttpServletRequest req) {
ServletInputStream is;
try {
is = req.getInputStream();
int nRead = 1;
int nTotalRead = 0;
byte[] bytes = new byte[10240];
while (nRead > 0) {
nRead = is.read(bytes, nTotalRead, bytes.length - nTotalRead);
if (nRead > 0) {
nTotalRead = nTotalRead + nRead;
}
}
return new String(bytes, 0, nTotalRead, StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
public static void main(String[] args) {
//第三方接口调用本地接口测试
String salt = UUID.randomUUID().toString().replaceAll("-", "");
long timestamp = System.currentTimeMillis();
TreeMap<String, Object> param = new TreeMap<>(Comparator.naturalOrder());
param.put("userName", "测试");
param.put("userAge", "99");
param.put("timestamp", timestamp);
param.put("salt", salt);
param.put("sign", new MD5().digestHex(requestParams(param) + ACCESS_KEY));
String response = HttpUtil.get("http://xxx/api", param);
}
}