需求背景
最近我們在做一個開放平台,將公司的能力接口通過此平台暴露出去,給外部公司使用,然后收取費用。那么在對接外部公司的時候,就會涉及到接口參數簽名以及驗證時間戳。如果每個接口都手動去校驗,毫無疑問非常的繁瑣,因此優化了一下,通過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; } }