由于项目需要开发第三方接口给多个供应商,为保证Api接口的安全性,遂采用Api接口签名验证。
1、服务端生成一对 accessKey/secretKey密钥对,将 accessKey公开给客户端,将 secretKey 保密。
2、客户端使用 secretKey和一些请求参数(如时间戳、请求内容等),使用 MD5 算法生成签名。
3、客户端将 accessKey、签名和请求参数一起发送给服务端。
4、服务端使用 和收到的请求参数,使用 MD5 算法生成签名。
5、服务端比较客户端发来的签名和自己生成的签名是否相同,如果相同,则认为请求是可信的,否则认为请求是不可信的。
secretKey不进行网络传输,只用于本地MD5运算
计算步骤
用于计算签名的参数在不同接口之间会有差异,但算法过程固定如下4个步骤。
将
请求参数对按key进行字典升序排序,得到有序的参数对列表N 将列表N中的参数对按URL键值对的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL键值拼接过程value部分需要URL编码,URL编码算法用大写字母,例如%E8,而不是小写%e8
将应用密钥以app_key为键名,组成URL键值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密钥)
对字符串S进行MD5运算,将得到的MD5值所有字符转换成大写,得到接口请求签名
注意事项
不同接口要求的参数对不一样,计算签名使用的参数对也不一样
参数名区分大小写,参数值为空不参与签名
URL键值拼接过程value部分需要URL编码
public class SignUtil { /** * 签名算法 * 1. 计算步骤 * 用于计算签名的参数在不同接口之间会有差异,但算法过程固定如下4个步骤。 * 将请求参数对按key进行字典升序排序,得到有序的参数对列表N * 将列表N中的参数对按URL键值对的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL键值拼接过程value部分需要URL编码,URL编码算法用大写字母,例如%E8,而不是小写%e8 * 将应用密钥以app_key为键名,组成URL键值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密钥) * 对字符串S进行MD5运算,将得到的MD5值所有字符转换成大写,得到接口请求签名 * 2. 注意事项 * 不同接口要求的参数对不一样,计算签名使用的参数对也不一样 * 参数名区分大小写,参数值为空不参与签名 * URL键值拼接过程value部分需要URL编码 * @return 签名字符串 */ private static String getSign(Map map, String secretKey) { List > infoIds = new ArrayList<>(map.entrySet()); Collections.sort(infoIds, new Comparator >() { public int compare(Map.Entry arg0, Map.Entry arg1) { return (arg0.getKey()).compareTo(arg1.getKey()); } }); StringBuffer sb = new StringBuffer(); for (Map.Entry m : infoIds) { if(null == m.getValue() || StringUtils.isNotBlank(m.getValue().toString())){ sb.append(m.getKey()).append("=").append(URLUtil.encodeAll(m.getValue().toString())).append("&"); } } sb.append("secret-key=").append(secretKey); return MD5.create().digestHex(sb.toString()).toUpperCase(); } //获取随机值 private static String getNonceStr(int length){ //生成随机字符串 String str="zxcvbnmlkjhgfdsaqwertyuiopQWERTYUIOPASDFGHJKLZXCVBNM1234567890"; Random random=new Random(); StringBuffer randomStr=new StringBuffer(); // 设置生成字符串的长度,用于循环 for(int i=0; i //从62个的数字或字母中选择 int number=random.nextInt(62); //将产生的数字通过length次承载到sb中 randomStr.append(str.charAt(number)); } return randomStr.toString(); } //签名验证方法 public static boolean signValidate(Map map,String secretKey,String sign){ String mySign = getSign(map,secretKey); return mySign.equals(sign); } }
@Configuration public class SignInterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(signInterceptor()) .addPathPatterns("/openapi/**");//只拦截openapi前缀的接口 } //交给spring管理 SignInterceptor bean //不然下边 private OpenApiApplyMapper applyMapper;注入为null @Bean public SignInterceptor signInterceptor(){ return new SignInterceptor(); } }
public class KeyGenerator { private static final int KEY_LENGTH = 32; // 指定生成的key长度为32字节 public static String generateAccessKey() { SecureRandom random = new SecureRandom(); byte[] bytes = new byte[KEY_LENGTH / 2]; // 生成的字节数要除以2 random.nextBytes(bytes); return Base64.getEncoder().encodeToString(bytes).replace("/", "").replace("+", "").substring(0, 20); } public static String generateSecretKey() { SecureRandom random = new SecureRandom(); byte[] bytes = new byte[KEY_LENGTH]; random.nextBytes(bytes); return Base64.getEncoder().encodeToString(bytes).replace("/", "").replace("+", "").substring(0, 40); } }
public class SignInterceptor implements HandlerInterceptor { private static final String ACCESSKEY = "access-key";//调用者身份唯一标识 private static final String TIMESTAMP = "time-stamp";//时间戳 private static final String SIGN = "sign";//签名 private static final String NONCE = "nonce";//随机值 @Resource private OpenApiApplyMapper applyMapper; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if(checkSign(request, response)){//签名认证 return HandlerInterceptor.super.preHandle(request, response, handler); } return false; } /** * 验证签名 * @param request * @param response * @return * @throws Exception */ private boolean checkSign(HttpServletRequest request,HttpServletResponse response)throws Exception { response.setContentType("application/json"); response.setCharacterEncoding("utf8"); String ip = IPUtils.getIpAddr(request); FzyLogUtil.infoSafe("开放接口", "访问时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL()); String accessKey = request.getHeader(ACCESSKEY); String timestamp = request.getHeader(TIMESTAMP); String nonce = request.getHeader(NONCE); String sign = request.getHeader(SIGN); if (!StringUtils.isNotBlank(accessKey)) { response.getWriter().write(JSON.toJSONString(ResultUtil.fail("accessKey无效"))); FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:accessKey无效"); return false; } if (StringUtils.isBlank(sign)) { response.getWriter().write(JSON.toJSONString(ResultUtil.fail("签名无效"))); FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:签名无效"); return false; } OpenApiDetailDO openApiDetailDO = applyMapper.selectOneByAccessKey(accessKey); if (openApiDetailDO == null) { response.getWriter().write(JSON.toJSONString(ResultUtil.fail("accessKey不存在"))); FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:accessKey不存在"); return false; } if (StringUtils.isNotBlank(openApiDetailDO.getBlackList())) { for (String bIp : openApiDetailDO.getBlackList().split(",")) { if (bIp.equals(ip)) {//黑名单 response.getWriter().write(JSON.toJSONString(ResultUtil.fail("拒绝请求"))); FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:黑名单拒绝请求"); return false; } } } if (StringUtils.isNotBlank(openApiDetailDO.getWhiteList())) { boolean flag = false; for (String bIp : openApiDetailDO.getWhiteList().split(",")) { if (bIp.equals(ip)) {//白名单 flag = true; break; } } if(!flag){ response.getWriter().write(JSON.toJSONString(ResultUtil.fail("拒绝请求"))); FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:白名单未符合拒绝请求"); return false; } } if ("0".equals(openApiDetailDO.getInvokeStatus() + "")) { response.getWriter().write(JSON.toJSONString(ResultUtil.fail("访问权限已被冻结"))); FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:访问权限已被冻结"); return false; } if (!"1".equals(openApiDetailDO.getApiStatus() + "")) { response.getWriter().write(JSON.toJSONString(ResultUtil.fail("接口异常,暂停访问"))); FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:接口异常,暂停访问"); return false; } if (!StringUtils.isNotBlank(timestamp)) { response.getWriter().write(JSON.toJSONString(ResultUtil.fail("时间戳无效"))); FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:时间戳无效"); return false; } else if (openApiDetailDO.getTimeOut() != null) { if (System.currentTimeMillis() - Long.valueOf(timestamp) > openApiDetailDO.getTimeOut() * 1000) { response.getWriter().write(JSON.toJSONString(ResultUtil.fail("请求已过期"))); FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:请求已过期"); return false; } ; } MaphashMap = new HashMap<>(); String queryStrings = request.getQueryString();//获取url后边拼接的参数 if (queryStrings != null) { for (String queryString : queryStrings.split("&")) { String[] param = queryString.split("="); if (param.length == 2) { hashMap.put(param[0], param[1]); } } } hashMap.put(ACCESSKEY, accessKey); hashMap.put(TIMESTAMP, timestamp); if (StringUtils.isNotBlank(nonce)) { hashMap.put(NONCE, nonce); } String secretKey = openApiDetailDO.getSecretKey(); String body = new RequestWrapper(request).getBody(); if (StringUtils.isNotBlank(body)) { Map map = JSON.parseObject(body); if (map != null) { hashMap.putAll(map); } } if (!SignUtil.signValidate(hashMap, secretKey, sign)) {//认证失败 response.getWriter().write(JSON.toJSONString(ResultUtil.fail("认证失败"))); FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:认证失败"); return false; } return true; } }
通过过滤器解决
@Component @WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/") @Order(10000) public class HttpServletRequestFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; String contentType = request.getContentType(); String method = "multipart/form-data"; if (contentType != null && contentType.contains(method)) { // 将转化后的 request 放入过滤链中 request = new StandardServletMultipartResolver().resolveMultipart(request); } request = new RequestWrapper((HttpServletRequest) servletRequest); //获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中 // 在chain.doFiler方法中传递新的request对象 if(null == request) { filterChain.doFilter(servletRequest, servletResponse); } else { filterChain.doFilter(request, servletResponse); } } @Override public void destroy() { } }