通過過濾器、攔截器實現公共驗證簽名以及時間戳功能


需求背景

最近我們在做一個開放平台,將公司的能力接口通過此平台暴露出去,給外部公司使用,然后收取費用。那么在對接外部公司的時候,就會涉及到接口參數簽名以及驗證時間戳。如果每個接口都手動去校驗,毫無疑問非常的繁瑣,因此優化了一下,通過filter以及interceptor來實現公共校驗。

代碼

1、過濾器

為啥直接通過攔截器無法實現,因為request的輸入流只能讀取一次因為流對應的是數據,數據放在內存中,有的是部分放在內存中。read 一次標記一次當前位置(mark position),第二次read就從標記位置繼續讀(從內存中copy)數據。 所以這就是為什么讀了一次第二次是空了。 怎么讓它不為空呢?只要inputstream 中的pos 變成0就可以重寫讀取當前內存中的數據。javaAPI中有一個方法public void reset() 這個方法就是可以重置pos為起始位置,但是不是所有的IO讀取流都可以調用該方法!ServletInputStream是不能調用reset方法,這就導致了只能調用一次getInputStream()。

因此通過過濾器里面進行一層包裝,包裝拿到了request中的請求數據,並且原來的request繼續往后傳遞,可以理解成做了一個拷貝。

HttpServletRequestReplacedFilter

package com.yzf.enterprise.open.platform.bff.security.sign.filter;

import com.yzf.enterprise.open.platform.bff.security.sign.wrapper.RequestWrapper;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component
public class HttpServletRequestReplacedFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if(request instanceof HttpServletRequest) {
            requestWrapper = new RequestWrapper((HttpServletRequest) request);
        }
        //獲取請求中的流如何,將取出來的字符串,再次轉換成流,然后把它放入到新request對象中。
        // 在chain.doFiler方法中傳遞新的request對象
        if(requestWrapper == null) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
    }

    @Override
    public void destroy() {

    }
}

RequestWrapper

package com.yzf.enterprise.open.platform.bff.security.sign.wrapper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

/**
 * 由於request數據讀取一次就無法讀取,因此通過包裝類來解決
 */
public class RequestWrapper extends HttpServletRequestWrapper {
    private final String body;

    private static final Logger LOGGER = LoggerFactory.getLogger(RequestWrapper.class);

    public RequestWrapper(HttpServletRequest request) {
        /**
         * 由於繼承了HttpServletRequestWrapper,HttpServletRequestWrapper又繼承了ServletRequestWrapper,ServletRequestWrapper
         * 中有一個private ServletRequest request;也就是將原來的request做了一個備份,具體讀到的數據放在body中
         */
        super(request);
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        InputStream inputStream = null;
        try {
            inputStream = request.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                char[] charBuffer = new char[128];
                int bytesRead = -1;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            } else {
                stringBuilder.append("");
            }
        } catch (Exception ex) {
            LOGGER.error("過濾器request請求包裝時出現異常", ex);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                }
                catch (IOException e) {
                    LOGGER.error("過濾器request請求包裝關閉流出現異常", e);
                }
            }
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                }
                catch (IOException e) {
                    LOGGER.error("過濾器request請求包裝關閉流出現異常", e);
                }
            }
        }
        body = stringBuilder.toString();
        LOGGER.info("過濾器request請求包裝結果為:" + body);
    }

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

    }

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

    public String getBody() {
        return this.body;
    }
}

FilterRegisterConfig

然后將filter注冊到容器中-----基於springboot項目

package com.yzf.enterprise.open.platform.bff.security.sign.filter;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterRegisterConfig {

    @Autowired
    private HttpServletRequestReplacedFilter httpServletRequestReplacedFilter;

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(httpServletRequestReplacedFilter);
        //攔截所有的請求,給每個請求都包裝一下,攔截器中再判斷是否需要攔截處理
        registrationBean.addUrlPatterns("/*");
        //給自定義的filter設置順序,值越小,優先級越高,建議可以稍微高一些,防止影響框架的一些filter
        registrationBean.setOrder(10);
        return registrationBean;
    }
}

2、攔截器

SignInterceptor

package com.yzf.enterprise.open.platform.bff.security.sign.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.yzf.accounting.common.base.AjaxResult;
import com.yzf.accounting.common.exception.BizRuntimeException;
import com.yzf.enterprise.open.platform.bff.security.sign.wrapper.RequestWrapper;
import com.yzf.enterprise.open.platform.client.api.OpAccessInfoClient;
import com.yzf.enterprise.open.platform.client.dto.OpAccessInfoDto;
import com.yzf.enterprise.open.platform.common.exception.BusinessErrorCode;
import com.yzf.enterprise.open.platform.common.utils.SignUtil;
import com.yzf.enterprise.open.platform.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 統一處理過濾請求:校驗簽名是否合法、校驗時間戳是否超過30s
 */
@Component
@Slf4j
public class SignInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(SignInterceptor.class);

    //目標方法執行之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        RequestWrapper requestWrapper;
        /**
         * 統一處理業務接口驗簽以及驗證時間戳操作
         */

      if(request instanceof RequestWrapper) {
        requestWrapper = (RequestWrapper) request;
      }else {
        requestWrapper = new RequestWrapper(request);  
      }


        String requestBody = requestWrapper.getBody();
        if(StringUtils.isBlank(requestBody)) {
            return true;
        }

        JSONObject jsonObject = null;
        try {
            jsonObject = JSONObject.parseObject(requestBody);
            logger.info("攔截器中請求參數格式化后為:" + jsonObject.toJSONString());
        }catch(Exception ex) {
            logger.error("簽名時間戳全局攔截器請求消息轉化出現異常", ex);
            throw new BizRuntimeException(BusinessErrorCode.ERROR_PARAM);
        }
        
        Long timestamp = jsonObject.getLong("timestamp");
        String sign = jsonObject.getString("sign");
        if (timestamp == null || StringUtils.isBlank(sign)) {
            throw new BizRuntimeException(BusinessErrorCode.ERROR_PARAM);
        }

        /**
         * 校驗時間戳
         */
        if (!SignUtil.validateTimestamp(timestamp)) {
            throw new BizRuntimeException(BusinessErrorCode.TIMESTAMP_VALID_ERROR);
        }

        /**
         * 校驗參數簽名
         */
        if (!SignUtil.validateSign(jsonObject, "參與簽名的秘鑰secret", sign)) {
            throw new BizRuntimeException(BusinessErrorCode.SIGN_VALID_ERROR);
        }

        return true;
    }
}

CustomMvcConfig

package com.yzf.enterprise.open.platform.bff.security.sign.interceptor;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class CustomMvcConfig extends WebMvcConfigurationSupport {

    @Autowired
    private SignInterceptor signInterceptor;

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/js/**").addResourceLocations("classpath:/js/");
    registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    registry.addResourceHandler("/swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
    registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(signInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/access/**",
                                     "/getToken",
                                     "/profile/**",
                                     "/swagger-ui.html",
                                     "/swagger-resources/**",
                                     "/webjars/**",
                                     "/*/api-docs",
                                     "/favicon.ico",
                                     "/actuator/**",
                                     "/error");
    }
}

SignUtil

package com.yzf.enterprise.open.platform.common.utils;

import com.alibaba.fastjson.JSON;
import com.yzf.enterprise.open.platform.common.annotation.SignIgnore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;

/**
 * 簽名工具類
 */
public class SignUtil {
    /**
     * 日志
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(SignUtil.class);
    /**
     * 編碼
     */
    private static final String CHARSET = "utf-8";

    /**
     * @param parameters
     * @param accessSecret
     * @param sign
     * @return 注:當接口入參為DTO對象時,使用此方法進行驗證簽名
     */
    public static boolean validateSign(Object parameters, String accessSecret, String sign) {
        LOGGER.info("validateSign map:{}", JSON.toJSONString(parameters));
        boolean flag = false;
        if (parameters == null) {
            return flag;
        }
        String mySign = getSignByObj(parameters, accessSecret);
        // 驗證簽名是否一致
        if (mySign.equals(sign)) {
            flag = true;
        }
        return flag;
    }

    /**
     * 校驗簽名timestamp與當前時間是否超過30s
     * 注:true未過期;false過期
     *
     * @param timestamp
     * @return
     */
    public static boolean validateTimestamp(Long timestamp) {
        if (timestamp == null) {
            return false;
        }

        Date date = new Date(timestamp);
        return !date.before(new Date(System.currentTimeMillis() - 30000));
    }

    /**
     * 簽名算法
     *
     * @param o 要參與簽名的數據對象
     * @return 簽名,使用sha256
     * @throws IllegalAccessException
     */
    public static String getSignByObj(Object o, String key) {
        String result = "";

        try {
            ArrayList<String> list = new ArrayList<>();
            Class cls = o.getClass();
            /**
             * 由於class.getDeclaredFields()無法獲取父類的字段,因此通過循環的方式獲取其所有父類,並排除掉object類字段
             */
            while(cls != null && !cls.getName().toLowerCase().equals("java.lang.object")) {
                Field[] fields = cls.getDeclaredFields();
                for (Field f : fields) {
                    f.setAccessible(true);
                    if (f.isAnnotationPresent(SignIgnore.class)) {
                        continue;
                    }

                    if(ObjectUtil.isSimpleTypeOrString(f.getType())) {
                        if (f.get(o) != null && f.get(o) != "") {
                            list.add(f.getName() + "=" + f.get(o) + "&");
                        }
                    }
                }
                cls = cls.getSuperclass();
            }

            int size = list.size();
            String[] arrayToSort = list.toArray(new String[size]);
            Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < size; i++) {
                sb.append(arrayToSort[i]);
            }

            result = sb.toString();
            result += key;
            result = SHA256Util.getSHA256StrJava(result, CHARSET);
        } catch (Exception e) {
            LOGGER.error("getSignByObject Exception", e);
        }

        return result;
    }

    /**
     * 驗證簽名 服務器端使用
     *
     * @param parametersMap 參數
     * @param secretkey     密鑰
     * @param sign          簽名
     * @return
     */
    public static boolean validateSign(Map<String, Object> parametersMap, String secretkey, String sign) {
        LOGGER.info("validateSign map:{}", JSON.toJSONString(parametersMap));
        boolean flag = false;
        if (parametersMap.isEmpty()) {
            return flag;
        }
        //去除parametersMap中的sign
        parametersMap.remove("sign");
        String mySign = signRequest(parametersMap, secretkey);
        // 驗證簽名是否一致
        if (mySign.equals(sign)) {
            flag = true;
        }
        return flag;
    }

    /**
     * 簽名加密 客戶端使用
     *
     * @param parametersMap 參數
     * @param secretkey     密鑰
     * @return
     */
    public static String signRequest(Map<String, Object> parametersMap, String secretkey) {
        ArrayList<String> list = new ArrayList<>();
        for (Map.Entry<String, Object> entry : parametersMap.entrySet()) {
            String key = entry.getKey();
            Object val = entry.getValue();
            if(ObjectUtil.isSimpleTypeOrString(val.getClass())) {
                if (null != val && !"".equals(val)) {
                    list.add(key + "=" + val + "&");
                }
            }
        }
        int size = list.size();
        String[] arrayToSort = list.toArray(new String[size]);
        Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < size; i++) {
            sb.append(arrayToSort[i]);
        }
        String result = sb.toString();
        result += secretkey;
        result = SHA256Util.getSHA256StrJava(result, CHARSET);
        return result;
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM