美文网首页
接口签名

接口签名

作者: 川流不息attitude | 来源:发表于2022-07-08 18:59 被阅读0次

    数据签名,主要就是为了防止 数据被 篡改

    避免body 只读一次

    
    @Component
    @WebFilter(filterName = "httpServletRequestWrapperFilter", urlPatterns = {"/*"})
    public class RepeatReadFilter implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            // 防止流读取一次后就没有了, 所以需要将流继续写出去
            String contentType = request.getContentType();
            if(StringUtil.isNotEmpty(contentType) && contentType.contains(MediaType.APPLICATION_JSON_VALUE)){
                ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(httpServletRequest);
                chain.doFilter(requestWrapper, response);
            }else {
                chain.doFilter(request,response);
            }
    
    
        }
    
        @Override
        public void destroy() {
    
        }
    }
    
    
    public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
    
        private final byte[] body;
    
        public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) {
            super(request);
            String sessionStream = getBodyString(request);
            body = sessionStream.getBytes(Charset.forName("UTF-8"));
        }
    
        /**
         * 获取请求Body
         *
         * @param request
         * @return
         */
        public String getBodyString(final ServletRequest request) {
            StringBuilder sb = new StringBuilder();
            try (InputStream inputStream = cloneInputStream(request.getInputStream());
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    sb.append(line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return sb.toString();
        }
    
        /**
         * Description: 复制输入流</br>
         *
         * @param inputStream
         * @return</br>
         */
        public InputStream cloneInputStream(ServletInputStream inputStream) {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            try {
                while ((len = inputStream.read(buffer)) > -1) {
                    byteArrayOutputStream.write(buffer, 0, len);
                }
                byteArrayOutputStream.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        }
    
        @Override
        public BufferedReader getReader() {
    
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }
    
        @Override
        public ServletInputStream getInputStream() {
    
            final ByteArrayInputStream bais = new ByteArrayInputStream(body);
            return new ServletInputStream() {
    
                @Override
                public int read() {
    
                    return bais.read();
                }
    
                @Override
                public boolean isFinished() {
    
                    return false;
                }
    
                @Override
                public boolean isReady() {
    
                    return false;
                }
    
                @Override
                public void setReadListener(ReadListener readListener) {
    
                }
            };
        }
    }
    

    拦截器

    @Slf4j
    @Component
    public class SignAuthInterceptor implements HandlerInterceptor {
    
        /**
         * 时间戳
         */
        private static final String TIMESTAMP = "timeStamp";
    
        /**
         * 应用 appKey
         */
        private static final String APP_KEY = "appKey";
    
        /**
         * 签名  sign
         */
        private static final String SIGN = "sign";
    
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 1. 非控制器请求直接跳出
            if (!(handler instanceof HandlerMethod)) {
                return true;
            }
    
            String contentType = request.getContentType();
            SortedMap<String, String> result = new TreeMap<>();
            if(StringUtil.isNotEmpty(contentType) && contentType.contains(MediaType.APPLICATION_JSON_VALUE)){
                HttpUtils.getBodyParam(request,result);
            }else {
                HttpUtils.getFormParams(request,result);
            }
            // 校验时间戳
            String timestamp = result.get(TIMESTAMP);
            String appKey = result.get(APP_KEY);
            String sign = result.get(SIGN);
            if(StringUtil.isEmpty(timestamp)){
                throw new BusinessException("参数错误 缺失[timeStamp]");
            }
            if(StringUtil.isEmpty(appKey)){
                throw new BusinessException("参数错误 缺失[appKey]");
            }
            if(StringUtil.isEmpty(sign)){
                throw new BusinessException("参数错误 缺失[sign]");
            }
           
            boolean validateTimeStamp = validateTimeStamp(Long.valueOf(timestamp));
            if(!validateTimeStamp){
                throw new BusinessException("当前请求参数已过期,不允许访问");
            }
            //校验签名
            boolean verifySign = SignUtils.verifySign(result,appSecret);
    
            if(!verifySign){
                log.info("签名 app_secret:{}",appSecret);
                throw new BusinessException("签名不正确,不允许访问");
            }
            return true;
        }
    
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
           
        }
        /**
         * @description: 判断客户端的请求是否超过3分钟
    
         */
        private boolean validateTimeStamp(long timestamp) {
            Long tims = (System.currentTimeMillis()-timestamp) / (1000 * 60);
            //验证时间戳是否超过3分钟
            if (Math.abs(tims) > 3) {
                return false;
            } else {
                return true;
            }
        }
    }
    

    获取参数方法

    public class HttpUtils {
    
        /**
         *
         * @param request
         * @return
         * @throws IOException
         */
        public static SortedMap<String, String> getAllParams(HttpServletRequest request) throws IOException {
            SortedMap<String, String> result = new TreeMap<>();
            // 获取URL上的参数
            getUrlParams(request, result);
            // 获取body参数
            getBodyParam(request, result);
            return result;
        }
    
        /**
         * 获取 Body 参数
         *
         */
        public static void getBodyParam(final HttpServletRequest request, SortedMap<String, String> result)
                throws IOException {
            BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
            String str = "";
            StringBuilder wholeStr = new StringBuilder();
            // 一行一行的读取body体里面的内容;
            while ((str = reader.readLine()) != null) {
                wholeStr.append(str);
            }
            wholeStr.trimToSize();
            String s = wholeStr.toString();
            if (!StringUtil.isEmpty(s)) {
                // 转化成json对象
                Map<String, String> allRequestParam = JSONObject.parseObject(s, Map.class);
                // 将URL的参数和body参数进行合并
                for (Map.Entry entry : allRequestParam.entrySet()) {
                    result.put((String)entry.getKey(), (String)entry.getValue());
                }
            }
        }
    
        /**
         * 获取url参数
         */
        public static void getUrlParams(HttpServletRequest request, SortedMap<String, String> result) {
            String param = "";
            try {
                String urlParam = request.getQueryString();
                if (urlParam != null) {
                    param = URLDecoder.decode(urlParam, "utf-8");
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            String[] params = param.split("&");
            for (String s : params) {
                int index = s.indexOf("=");
                if (index != -1) {
                    result.put(s.substring(0, index), s.substring(index + 1));
                }
            }
        }
    
        /**
         * 获取表单数据
         */
        public static void getFormParams(HttpServletRequest request, SortedMap<String, String> result) {
            Map<String, String[]> parameterMap = request.getParameterMap();
            for (Map.Entry<String,String[]> entry:parameterMap.entrySet()){
                result.put(entry.getKey(), Arrays.stream(entry.getValue()).collect(Collectors.joining(",")));
            }
        }
    }
    

    签名工具

    算法 参考微信 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3

    @Slf4j
    public class SignUtils {
    
        /**
         * @param params
         *  所有的请求参数都会在这里进行排序加密
         * @return 验证签名结果
         */
        public static boolean verifySign(SortedMap<String, String> params,String key) {
            String urlSign = params.get("sign");
            log.info("Url Sign : {}", urlSign);
            if (StringUtil.isEmpty(urlSign)) {
                return false;
            }
            // 把参数加密
            String paramsSign = getParamsSign(params,key);
            log.info("Param Sign : {}", paramsSign);
            return !StringUtil.isEmpty(paramsSign) && urlSign.equals(paramsSign);
        }
    
        /**
         * @param params
         *
         * @return 得到签名
         */
        public static String getParamsSign(SortedMap<String, String> params,String key) {
            // 要先去掉 Url 里的 Sign
            params.remove("sign");
            StringBuffer sb = new StringBuffer();
            params.forEach((k,v)->{
                sb.append(k).append("=").append(v).append("&");
            });
            sb.append("key").append("=").append(key);
    
            try {
                String sign = new String(sb.toString().getBytes(), "utf-8");
                return SecureUtil.md5(sign).toUpperCase();
            } catch (UnsupportedEncodingException e) {
                throw new BusinessException("签名错误不支持utf-8编码");
            }
    
        }
    
    
    }
    
    

    彩蛋 也可以在网关 做,附上网关 获取参数 的类。

    定义 filter 存到上下文

    package com.xiaominfo.swagger.service.doc.config.test02;
    import io.netty.buffer.ByteBufAllocator;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.core.io.ByteArrayResource;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.core.io.buffer.DataBufferUtils;
    import org.springframework.core.io.buffer.NettyDataBufferFactory;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.http.codec.HttpMessageReader;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
    import org.springframework.http.server.reactive.ServerHttpResponse;
    import org.springframework.stereotype.Component;
    import org.springframework.util.MultiValueMap;
    import org.springframework.web.reactive.function.server.HandlerStrategies;
    import org.springframework.web.reactive.function.server.ServerRequest;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    
    import java.io.UnsupportedEncodingException;
    import java.net.URLEncoder;
    import java.nio.charset.Charset;
    import java.nio.charset.StandardCharsets;
    import java.util.List;
    import java.util.Map;
    
    
    
    /**
     * @Author 
     * @Date 2022/6/20 11:56
     */
    @Component
    @Slf4j
    public class RequestCoverFilter implements GlobalFilter, Ordered {
    
        /**
         * default HttpMessageReader
         */
        private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
    
        /**
         * ReadFormData
         *
         * @param exchange
         * @param chain
         * @return
         */
        private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain,
                                        GatewayContext gatewayContext) {
            final ServerHttpRequest request = exchange.getRequest();
            HttpHeaders headers = request.getHeaders();
    
            return exchange.getFormData().doOnNext(multiValueMap -> {
                gatewayContext.setFormData(multiValueMap);
                log.debug("[GatewayContext]Read FormData:{}", multiValueMap);
            }).then(Mono.defer(() -> {
                Charset charset = headers.getContentType().getCharset();
                charset = charset == null ? StandardCharsets.UTF_8 : charset;
                String charsetName = charset.name();
                MultiValueMap<String, String> formData = gatewayContext.getFormData();
                /**
                 * formData is empty just return
                 */
                if (null == formData || formData.isEmpty()) {
                    return chain.filter(exchange);
                }
                StringBuilder formDataBodyBuilder = new StringBuilder();
                String entryKey;
                List<String> entryValue;
                try {
                    /**
                     * repackage form data
                     */
                    for (Map.Entry<String, List<String>> entry : formData.entrySet()) {
                        entryKey = entry.getKey();
                        entryValue = entry.getValue();
                        if (entryValue.size() > 1) {
                            for (String value : entryValue) {
                                formDataBodyBuilder.append(entryKey).append("=")
                                        .append(URLEncoder.encode(value, charsetName)).append("&");
                            }
                        } else {
                            formDataBodyBuilder.append(entryKey).append("=")
                                    .append(URLEncoder.encode(entryValue.get(0), charsetName)).append("&");
                        }
                    }
                } catch (UnsupportedEncodingException e) {
                    // ignore URLEncode Exception
                }
                /**
                 * substring with the last char '&'
                 */
                String formDataBodyString = "";
                if (formDataBodyBuilder.length() > 0) {
                    formDataBodyString = formDataBodyBuilder.substring(0, formDataBodyBuilder.length() - 1);
                }
                /**
                 * get data bytes
                 */
                byte[] bodyBytes = formDataBodyString.getBytes(charset);
                int contentLength = bodyBytes.length;
                ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) {
                    /**
                     * change content-length
                     *
                     * @return
                     */
                    @Override
                    public HttpHeaders getHeaders() {
                        HttpHeaders httpHeaders = new HttpHeaders();
                        httpHeaders.putAll(super.getHeaders());
                        if (contentLength > 0) {
                            httpHeaders.setContentLength(contentLength);
                        } else {
                            httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                        }
                        return httpHeaders;
                    }
    
                    /**
                     * read bytes to Flux<Databuffer>
                     *
                     * @return
                     */
                    @Override
                    public Flux<DataBuffer> getBody() {
                        return DataBufferUtils.read(new ByteArrayResource(bodyBytes),
                                new NettyDataBufferFactory(ByteBufAllocator.DEFAULT), contentLength);
                    }
                };
                ServerWebExchange mutateExchange = exchange.mutate().request(decorator).build();
                log.info("[GatewayContext]Rewrite Form Data :{}", formDataBodyString);
    
                return chain.filter(mutateExchange);
            }));
        }
    
        /**
         * ReadJsonBody
         *
         * @param exchange
         * @param chain
         * @return
         */
        private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) {
            /**
             * join the body
             */
            return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
                /*
                 * read the body Flux<DataBuffer>, and release the buffer
                 * //TODO when SpringCloudGateway Version Release To G.SR2,this can be update with the new version's feature
                 * see PR https://github.com/spring-cloud/spring-cloud-gateway/pull/1095
                 */
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);
                Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
                    DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                    DataBufferUtils.retain(buffer);
                    return Mono.just(buffer);
                });
                /**
                 * repackage ServerHttpRequest
                 */
                ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                    @Override
                    public Flux<DataBuffer> getBody() {
                        return cachedFlux;
                    }
                };
                /**
                 * mutate exchage with new ServerHttpRequest
                 */
                ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
                /**
                 * read body string with default messageReaders
                 */
                return ServerRequest.create(mutatedExchange, messageReaders).bodyToMono(String.class)
                        .doOnNext(objectValue -> {
                            gatewayContext.setCacheBody(objectValue);
                            log.debug("[GatewayContext]Read JsonBody:{}", objectValue);
                        }).then(chain.filter(mutatedExchange));
            });
        }
    
        @Override
        public int getOrder() {
            return HIGHEST_PRECEDENCE;
        }
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            /**
             * save request path and serviceId into gateway context
             */
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();
    
            GatewayContext gatewayContext = new GatewayContext();
            String path = request.getPath().pathWithinApplication().value();
            gatewayContext.setPath(path);
            gatewayContext.getFormData().addAll(request.getQueryParams());
            gatewayContext.setIpAddress(String.valueOf(request.getRemoteAddress()));
            HttpHeaders headers = request.getHeaders();
            gatewayContext.setHeaders(headers);
            log.debug("HttpMethod:{},Url:{}", request.getMethod(), request.getURI().getRawPath());
    
            /// 注意,因为webflux的响应式编程 不能再采取原先的编码方式 即应该先将gatewayContext放入exchange中,否则其他地方可能取不到
            /**
             * save gateway context into exchange
             */
            exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT, gatewayContext);
    
            // 处理参数
            MediaType contentType = headers.getContentType();
            long contentLength = headers.getContentLength();
            if (contentLength > 0) {
                if (MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType)) {
                    return readBody(exchange, chain, gatewayContext);
                }
                if (MediaType.APPLICATION_FORM_URLENCODED.equals(contentType)) {
                    return readFormData(exchange, chain, gatewayContext);
                }
            }
            // TODO 多版本划区域控制后期实现
    
            log.debug("[GatewayContext]ContentType:{},Gateway context is set with {}", contentType, gatewayContext);
            return chain.filter(exchange);
        }
    
    }
    
    
    package com.xiaominfo.swagger.service.doc.config.test02;
    
    import lombok.Data;
    import org.springframework.http.HttpHeaders;
    import org.springframework.util.LinkedMultiValueMap;
    import org.springframework.util.MultiValueMap;
    
    /**
     * @Author 
     * @Date 2022/6/20 11:55
     */
    @Data
    public class GatewayContext {
        public static final String CACHE_GATEWAY_CONTEXT = "cacheGatewayContext";
    
        /**
         * cache headers
         */
        private HttpHeaders headers;
    
        /**
         * baseHeader
         */
        //private BaseHeader baseHeader;
    
        /**
         * cache json body
         */
        private String cacheBody;
        /**
         * cache formdata
         */
        private MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
    
        /**
         * ipAddress
         */
        private String  ipAddress;
    
        /**
         * path
         */
        private String path;
    
    }
    
    
    
    // 其他 filter 从上下文获取
    GatewayContext gatewayContext = exchange.getAttribute(GatewayContext.CACHE_GATEWAY_CONTEXT);
    

    相关文章

      网友评论

          本文标题:接口签名

          本文链接:https://www.haomeiwen.com/subject/ixvvbrtx.html