首页 > 其他分享 >api接口使用MD5加密加盐签名校验

api接口使用MD5加密加盐签名校验

时间:2023-03-07 18:33:29浏览次数:49  
标签:return String request 校验 api 签名 new public MD5

最近一个A系统需要向B系统推送数据,因为数据每天不一定有多少,有时候多有时候少,且由UGC生成,需要B系统做一些处理,用mq比较麻烦,且公司用的付费rocketmq。除了重要数据一般不使用mq同步数据,所以该用接口调用的方式,A系统需要向B系统推送数据,所以需要B提供接口,A直接将数据通过接口的方式推过去,因为对外的接口所以需要对api接口的参数进行签名校验,防止篡改请求于攻击。
这里使用最简单的md5加密,稍微复杂一点的可以使用RSA通过公钥私钥来进行签名,再保险一点可以使用每个用户的公钥私钥加时间戳等等来达到安全保证

MD5加密算法

MD5信息摘要算法(MD5 Message-Digest Algorithm)种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value)用来确保信息传输完整一致性。
特点是非对称加密,不可逆,知道密文,无法知道原文,
长度固定,任意长度的数据,算出来的md5都是长度固定的
细微性,相同值的md5值一样相同,对原文进行任何改动,md5会有很大的变化
但是像简单的一些值md5可以被解密出来,我们使用接口签名时主要防止恶意请求,如果别人知道了接口的数据,以及请求参数的格式,用md5算法一加密,就可以轻松的请求成功,所以我们固定盐值,盐值只有服务提供方和接收方知道,一定程度上可以保证接口的安全性
这里因为是单个微服务提供接口,不是全局的接口使用,所以不需要网关签名,直接在项目里添加过滤器进行拦截做签名处理
主要原理为,与请求的客户端约定签名格式,在讲固定格式的参数值加固定盐值进行md5加密,将参数值与md5值一直传递;服务端接受到请求同样也会对请求的参数根据约定的格式进行md5签名;最后将服务端签名后的值与客户端传过来的签名值进行校验,如果相同,则通过,如果签名值不一样,则校验失败返回请求失败标识;

拦截器

这里通过WebMvcConfiger来注册进去一个签名拦截器
addInterceptor:需要一个实现HandlerInterceptor接口的拦截器实例
addPathPatterns:用于设置拦截器的过滤路径规则;addPathPatterns("/**")对所有请求都拦截
excludePathPatterns:用于设置不需要拦截的过滤规则
所以这里我们先写一个拦截器

@Slf4j
public class PhpSignatureInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Signature signatureAnnotation = handlerMethod.getMethodAnnotation(Signature.class);
        if (null == signatureAnnotation) {
            return true;
        }

        String sign = request.getHeader("sign");
        if (StrUtil.isBlank(sign)) {
            log.error("sign is empty, check sign failed");
            writeErrorResponseCode(response);
            return false;
        }
        if (ServletUtil.isGetMethod(request)) {
            String queryString = request.getQueryString();
            Map<String, String> paramMap = HttpUtil.decodeParamMap(queryString, StandardCharsets.UTF_8);
            return checkSignature(sign, paramMap, response);
        }
        if (ServletUtil.isPostMethod(request)) {
            String contentType = request.getContentType();
            Map<String, String> parameterMap;
            if (StrUtil.equals(contentType, MediaType.APPLICATION_JSON_VALUE)) {
                RequestBodyCopyFilter.RequestBodyWrapper wrapper = (RequestBodyCopyFilter.RequestBodyWrapper) request;
                String body = wrapper.getBody();
                if (log.isDebugEnabled()) {
                    log.info("post request body: {}", body);
                }
                parameterMap = SignUtils.signParseMap(body);
            } else {
                parameterMap = ServletUtil.getParamMap(request);
            }
            return checkSignature(sign, parameterMap, response);

        }
        return true;
    }

    private static final String MD5_SALT = "gLvzYnRhwCFKG_WP";

    private static final MD5 getMd5() {
        return new MD5(MD5_SALT.getBytes(StandardCharsets.UTF_8));
    }

    private boolean checkSignature(String signStr, Map<String, String> paramMap, HttpServletResponse response) {
        return checkSignature(signStr, SignUtils.mapConvertString(paramMap), response);
    }

    private boolean checkSignature(String signStr, String params, HttpServletResponse response) {
        if (log.isDebugEnabled()) {
            log.info("sign is {}, params is {}", signStr, params);
        }
        String str = getMd5().digestHex(params, StandardCharsets.UTF_8);
        if (Objects.equals(signStr, str)) {
            return true;
        }
        writeErrorResponseCode(response);
        return false;
    }

    private void writeErrorResponseCode(HttpServletResponse response) {
        response.setStatus(403);
    }

这里我们只重写preHandle方法就可以,依赖于hutool的部分工具,这里主要签名get请求和post的部分请求类型,post请求的application/json类型只处理了map格式的json,list格式的json暂时未处理
整体的逻辑同上面所说,客户端先md5签名,这里我们约定好将签名后的md5值添加到请求头sign里面,在处理的时候为了不再修改拦截器的接口拦截规则,以及为了签名的扩展性,添加了注解@Signature用于区分此接口是否需要签名校验,也可以在添加配置实现签名接口的白名单

/**
 * 是否需要签名标识
 * @author liufuqiang
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Signature {
}

签名工具类

public class SignUtils {
    public SignUtils () {
    }

    /**
     * 针对json字符串格式解析为map数据格式
     *
     * @param jsonString json字符串
     * @return
     */
    public static Map<String, String> signParseMap(String jsonString) {
        Map<String, String> parameterMap = new LinkedHashMap<>(16);
        JSONObject jsonObject = JSONUtil.parseObj(jsonString, false, true);
        Map<String, Object> map = JSONUtil.toBean(jsonObject, new TypeReference<LinkedHashMap<String, Object>>() {
        }.getType(), false);
        Map<String, String> finalParameterMap = parameterMap;
        map.forEach((key, value) -> {
            if (value instanceof JSONArray) {
                JSONArray array = (JSONArray) value;
                finalParameterMap.put(key,
                        array.stream().map(String::valueOf).collect(Collectors.joining(StrUtil.COMMA)));
            } else {
                finalParameterMap.put(key, value + "");
            }
        });
        return finalParameterMap;
    }

    /**
     * 数据格式转换
     * <pre>k1=v1&k2=v2</pre>
     *
     * @param map
     * @return
     */
    public static String mapConvertString(Map<String, String> map) {
        if (CollUtil.isEmpty(map)) {
            return StrUtil.EMPTY;
        }
        // 排序字典并拼接,字典序 a=a&b=b
        List<String> paramsTest = new ArrayList<>();
        map.entrySet().stream().sorted(Map.Entry.comparingByKey())
                .forEachOrdered(x -> paramsTest.add(x.getKey() + "=" + x.getValue()));
        return String.join("&", paramsTest);
    }
}

如果是post请求内容,涉及请求内容重写问题,所以复制了一份出来

public class RequestBodyCopyFilter implements Filter {
    @Override
    public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if(servletRequest instanceof HttpServletRequest) {
            if (ServletUtil.isPostMethod((HttpServletRequest) servletRequest)) {
                requestWrapper = new RequestBodyWrapper((HttpServletRequest) servletRequest);
            }
        }
        if(requestWrapper == null) {
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            filterChain.doFilter(requestWrapper, servletResponse);
        }
    }

    public static class RequestBodyWrapper extends HttpServletRequestWrapper {

        private @Nullable byte[] body;

        public RequestBodyWrapper(HttpServletRequest request) throws IOException {
            super(request);
            if (ServletUtil.isMultipart(request)) {
                return;
            }
            if (!StrUtil.equals(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
                return;
            }
            if (ServletUtil.isPostMethod(request)) {
                StringBuilder sb = new StringBuilder();
                String line;
                BufferedReader reader = request.getReader();
                while ((line = reader.readLine()) != null) {
                    sb.append(line);
                }
                String body = sb.toString();
                this.body = body.getBytes(StandardCharsets.UTF_8);
                System.out.println(1);
            }
        }

        public String getBody() {
            return new String(this.body , StandardCharsets.UTF_8) ;
        }

        @Override
        public ServletInputStream getInputStream() {
            final ByteArrayInputStream bais = new ByteArrayInputStream(body);
            return new ServletInputStream() {
                @Override
                public boolean isFinished() {
                    return false;
                }
                @Override
                public boolean isReady() {
                    return false;
                }
                @Override
                public void setReadListener(ReadListener readListener) {
                }
                @Override
                public int read(){
                    return bais.read();
                }
            };
        }

        @Override
        public BufferedReader getReader() {
            return new BufferedReader(new InputStreamReader(this.getInputStream()));
        }
    }
}

拦截器写完,我们将这个拦截器注册到WebMvcConfigurer里面

@Override
	public void addInterceptors(InterceptorRegistry registry) {
		PhpSignatureInterceptor phpSignatureInterceptor = new PhpSignatureInterceptor();
		registry.addInterceptor(phpSignatureInterceptor).addPathPatterns("/test/**");
	}


	@Bean
	public RequestBodyCopyFilter getBodyWrapperFilter() {
		return new RequestBodyCopyFilter();
	}

	@Bean("bodyCopyFilter")
	public FilterRegistrationBean<RequestBodyCopyFilter> bodyCopyFilter(RequestBodyCopyFilter bodyWrapperFilter) {
		FilterRegistrationBean<RequestBodyCopyFilter> registrationBean = new FilterRegistrationBean();
		registrationBean.setFilter(bodyWrapperFilter);
		registrationBean.addUrlPatterns("/test/*");
		registrationBean.setOrder(1);
		registrationBean.setAsyncSupported(true);
		return registrationBean;
	}


这里拦截的接口为/test开头的接口
测试代码如下:

@GetMapping("/test/testGet")
    @Signature
    public R testGetSign(@RequestParam String username) {
        log.info("params :{}", username);
        return R.ok("success");
    }

如果这里不添加请求头sign,或者sign不正确,则显示http状态码403

我们将签名结果添加到请求头

curl --location 'http://127.0.0.1:8080/test/testGet?username=liufuqiang' \
--header 'sign: 24ec455a315fb5ea3e73909b0114b851'

标签:return,String,request,校验,api,签名,new,public,MD5
From: https://www.cnblogs.com/LiuFqiang/p/17189115.html

相关文章

  • Nginx配置referer校验,实现简单的防盗链
    1、背景对增删修改类请求接口的referer做白名单校验或在参数中增加随机,预防CSRF漏洞2.1、NginxReferer模块ngx_http_referer_module模块用于阻止"Refer"头字段中具......
  • DeviceMotionEvent API All In One
    DeviceMotionEventAPIAllInOne设备运动事件/设备动作事件https://caniuse.com/?search=DeviceMotionEventWarning:Currently,FirefoxandChromedonothan......
  • 【APIO2015】Palembang Bridges
    容易想到先排除不用过桥的再把过桥的1加上,剩下只需要考虑河边走的距离。首先考虑k=1的情况,容易发现相当于是一个直线上2n个点选一个点到所有点距离和最小,经典的结论选在中......
  • 【APIO2015】Bali Sculptures
    发现不是很好dp,考虑从大到小枚举位转而判断能不能让这一位为0。设计dp状态:\(dp[i][j]\)表示前i个分了j组是否能满足当前条件,显然有一个\(O(n^3logA)\)的简单dp。判断是否满......
  • 【APIO2015】Jakarta Skyscrapers
    很容易的想到根号分治,我们先考虑暴力做法。用dp[i][j]表示从开始状态到第i个点有一个跳跃能力为j的doge的最少跳跃次数,暴力也是O(n^2)的。我们考虑稍微优化优化。考虑根......
  • [转]C# 获得窗体句柄并发送消息(利用windows API可在不同进程中获取)
     C#使用WindowsAPI获取窗口句柄控制其他程序窗口编写程序模拟鼠标和键盘操作可以方便的实现你需要的功能,而不需要对方程序为你开放接口。比如,操作飞信定时发送短信等......
  • 告别数据开发中的人工审核!火山引擎 DataLeap 落地“自动校验开发规范”能力
    更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群近期,火山引擎DataLeap智能市场上线“数仓建表规范”功能,该功能通过规范数仓场景......
  • 函数编程:强大的 Stream API
    函数编程:强大的StreamAPI每博一文案只要有人的地方,世界就不会是冰冷的,我们可以平凡,但绝对不可以平庸。——————《......
  • Apinto V0.12 发布:新增流量镜像与 Mock 插件,路由特性更丰富!
    Hello~各位开发者朋友们好呀,Eolink旗下开源网关Apinto本周又更新啦!这次的更新我们给大家带来了2个好用的插件,且目前已经支持静态资源路由了!希望新的功能能让大家的......
  • 【APIO2014】Beads and wires
    观察其实就是每个节点可以作为蓝线的中点一次,然后求蓝线的最大权值和。考虑如果是有根的话,可能是son[x]-x-fa[x]这种结构,也可能是son[x]-x-son[x]。应该可以用一个dp[i][0/......