0


超详细!完整版!基于spring对外开放接口的签名认证方案(拦截器方式)

文章目录

1、场景

由于项目需要开发第三方接口给多个供应商,为保证Api接口的安全性,遂采用Api接口签名验证。

2、接口防御措施

  1. 请求发起时间得在限制范围内
  2. 请求的用户是否真实存在
  3. 是否存在重复请求
  4. 请求参数是否被篡改

3、签名认证逻辑

1、服务端生成一对 accessKey/secretKey密钥对,将 accessKey公开给客户端,将 secretKey 保密。

2、客户端使用 secretKey和一些请求参数(如时间戳、请求内容等),使用 MD5 算法生成签名。

3、客户端将 accessKey、签名和请求参数一起发送给服务端。

4、服务端使用 和收到的请求参数,使用 MD5 算法生成签名。

5、服务端比较客户端发来的签名和自己生成的签名是否相同,如果相同,则认为请求是可信的,否则认为请求是不可信的。
secretKey不进行网络传输,只用于本地MD5运算

4、签名算法规则

计算步骤
用于计算签名的参数在不同接口之间会有差异,但算法过程固定如下4个步骤。
将<key, value>请求参数对按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编码

5、代码示例

1、sign工具类

publicclassSignUtil{/**
     * 签名算法
     * 1. 计算步骤
     * 用于计算签名的参数在不同接口之间会有差异,但算法过程固定如下4个步骤。
     * 将<key, value>请求参数对按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 签名字符串
     */privatestaticStringgetSign(Map<String,Object> map,String secretKey){List<Map.Entry<String,Object>> infoIds =newArrayList<>(map.entrySet());Collections.sort(infoIds,newComparator<Map.Entry<String,Object>>(){publicintcompare(Map.Entry<String,Object> arg0,Map.Entry<String,Object> arg1){return(arg0.getKey()).compareTo(arg1.getKey());}});StringBuffer sb =newStringBuffer();for(Map.Entry<String,Object> 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();}//获取随机值privatestaticStringgetNonceStr(int length){//生成随机字符串String str="zxcvbnmlkjhgfdsaqwertyuiopQWERTYUIOPASDFGHJKLZXCVBNM1234567890";Random random=newRandom();StringBuffer randomStr=newStringBuffer();// 设置生成字符串的长度,用于循环for(int i=0; i<length;++i){//从62个的数字或字母中选择int number=random.nextInt(62);//将产生的数字通过length次承载到sb中
            randomStr.append(str.charAt(number));}return randomStr.toString();}//签名验证方法publicstaticbooleansignValidate(Map<String,Object> map,String secretKey,String sign){String mySign =getSign(map,secretKey);return mySign.equals(sign);}}

2、定义拦截器

@ConfigurationpublicclassSignInterceptorConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(signInterceptor()).addPathPatterns("/openapi/**");//只拦截openapi前缀的接口}//交给spring管理 SignInterceptor bean //不然下边 private OpenApiApplyMapper applyMapper;注入为null@BeanpublicSignInterceptorsignInterceptor(){returnnewSignInterceptor();}}

3、生成accessKey、secretKey 工具类

publicclassKeyGenerator{privatestaticfinalint KEY_LENGTH =32;// 指定生成的key长度为32字节publicstaticStringgenerateAccessKey(){SecureRandom random =newSecureRandom();byte[] bytes =newbyte[KEY_LENGTH /2];// 生成的字节数要除以2
        random.nextBytes(bytes);returnBase64.getEncoder().encodeToString(bytes).replace("/","").replace("+","").substring(0,20);}publicstaticStringgenerateSecretKey(){SecureRandom random =newSecureRandom();byte[] bytes =newbyte[KEY_LENGTH];
        random.nextBytes(bytes);returnBase64.getEncoder().encodeToString(bytes).replace("/","").replace("+","").substring(0,40);}}

4、signInterceptor类

publicclassSignInterceptorimplementsHandlerInterceptor{privatestaticfinalString ACCESSKEY ="access-key";//调用者身份唯一标识privatestaticfinalString TIMESTAMP ="time-stamp";//时间戳privatestaticfinalString SIGN ="sign";//签名privatestaticfinalString NONCE ="nonce";//随机值@ResourceprivateOpenApiApplyMapper applyMapper;@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{if(checkSign(request, response)){//签名认证returnHandlerInterceptor.super.preHandle(request, response, handler);}returnfalse;}/**
     * 验证签名
     * @param request
     * @param response
     * @return
     * @throws Exception
     */privatebooleancheckSign(HttpServletRequest request,HttpServletResponse response)throwsException{
        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无效");returnfalse;}if(StringUtils.isBlank(sign)){
            response.getWriter().write(JSON.toJSONString(ResultUtil.fail("签名无效")));FzyLogUtil.errorSafe("开放接口请求失败","时间:"+LocalDateTime.now()+",IP:"+ ip +",访问接口:"+ request.getRequestURL()+"错误信息:签名无效");returnfalse;}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不存在");returnfalse;}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()+"错误信息:黑名单拒绝请求");returnfalse;}}}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()+"错误信息:白名单未符合拒绝请求");returnfalse;}}if("0".equals(openApiDetailDO.getInvokeStatus()+"")){
            response.getWriter().write(JSON.toJSONString(ResultUtil.fail("访问权限已被冻结")));FzyLogUtil.errorSafe("开放接口请求失败","时间:"+LocalDateTime.now()+",IP:"+ ip +",访问接口:"+ request.getRequestURL()+"错误信息:访问权限已被冻结");returnfalse;}if(!"1".equals(openApiDetailDO.getApiStatus()+"")){
            response.getWriter().write(JSON.toJSONString(ResultUtil.fail("接口异常,暂停访问")));FzyLogUtil.errorSafe("开放接口请求失败","时间:"+LocalDateTime.now()+",IP:"+ ip +",访问接口:"+ request.getRequestURL()+"错误信息:接口异常,暂停访问");returnfalse;}if(!StringUtils.isNotBlank(timestamp)){
            response.getWriter().write(JSON.toJSONString(ResultUtil.fail("时间戳无效")));FzyLogUtil.errorSafe("开放接口请求失败","时间:"+LocalDateTime.now()+",IP:"+ ip +",访问接口:"+ request.getRequestURL()+"错误信息:时间戳无效");returnfalse;}elseif(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()+"错误信息:请求已过期");returnfalse;};}Map<String,Object> hashMap =newHashMap<>();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 =newRequestWrapper(request).getBody();if(StringUtils.isNotBlank(body)){Map<String,Object> 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()+"错误信息:认证失败");returnfalse;}returntrue;}}

5、SignInterceptor 获取body里参数后,接口的controller会获取不到body的参数了,会报错

通过过滤器解决

@Component@WebFilter(filterName ="HttpServletRequestFilter", urlPatterns ="/")@Order(10000)publicclassHttpServletRequestFilterimplementsFilter{@Overridepublicvoidinit(FilterConfig filterConfig)throwsServletException{}@OverridepublicvoiddoFilter(ServletRequest servletRequest,ServletResponse servletResponse,FilterChain filterChain)throwsIOException,ServletException{HttpServletRequest request =(HttpServletRequest) servletRequest;String contentType = request.getContentType();String method ="multipart/form-data";if(contentType !=null&& contentType.contains(method)){// 将转化后的 request 放入过滤链中
            request =newStandardServletMultipartResolver().resolveMultipart(request);}
        request =newRequestWrapper((HttpServletRequest) servletRequest);//获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中// 在chain.doFiler方法中传递新的request对象if(null== request){
            filterChain.doFilter(servletRequest, servletResponse);}else{
            filterChain.doFilter(request, servletResponse);}}@Overridepublicvoiddestroy(){}}
标签: spring boot java 安全

本文转载自: https://blog.csdn.net/Coldmood/article/details/131390668
版权归原作者 Coldmood 所有, 如有侵权,请联系我们删除。

“超详细!完整版!基于spring对外开放接口的签名认证方案(拦截器方式)”的评论:

还没有评论