在爲第三方系統提供接口的時候,確定要考慮接口數據的安全問題,好比數據是否被篡改,數據是否已通過時,數據是否能夠重複提交等問題。其中我認爲最終要的仍是數據是否被篡改。在此分享一下個人關於接口簽名的實踐方案。html
Json: 按照key字典序排序,將全部key=value進行拼接(例如{"a":"a","c":"c","b":{"e":"e"}} => a=a^_^b=e=e^_^c=c) String: 整個字符串做爲一個拼接
若是存在多種數據形式,則按照path、query、form、body的順序進行再拼接,獲得全部數據的拼接值。
上述拼接的值記做 Y。java
X="appid=xxxnonce=xxxtimestamp=xxx"redis
最終拼接值=XY
最後將最終拼接值按照以下方法進行加密獲得簽名(signature)。算法
signature=org.apache.commons.codec.digest.HmacUtils.hmacSha256Hex(app secret, 拼接的值);
@Target({TYPE, METHOD}) @Retention(RUNTIME) @Documented public @interface Signature { String ORDER_SORT = "ORDER_SORT";//按照order值排序 String ALPHA_SORT = "ALPHA_SORT";//字典序排序 boolean resubmit() default true;//容許重複請求 String sort() default Signature.ALPHA_SORT; }
@Target({FIELD}) @Retention(RUNTIME) @Documented public @interface SignatureField { //簽名順序 int order() default 0; //字段name自定義值 String customName() default ""; //字段value自定義值 String customValue() default ""; }
public static String toSplice(Object object) { if (Objects.isNull(object)) { return StringUtils.EMPTY; } if (isAnnotated(object.getClass(), Signature.class)) { Signature sg = findAnnotation(object.getClass(), Signature.class); switch (sg.sort()) { case Signature.ALPHA_SORT: return alphaSignature(object); case Signature.ORDER_SORT: return orderSignature(object); default: return alphaSignature(object); } } return toString(object); } private static String alphaSignature(Object object) { StringBuilder result = new StringBuilder(); Map<String, String> map = new TreeMap<>(); for (Field field : getAllFields(object.getClass())) { if (field.isAnnotationPresent(SignatureField.class)) { field.setAccessible(true); try { if (isAnnotated(field.getType(), Signature.class)) { if (!Objects.isNull(field.get(object))) { map.put(field.getName(), toSplice(field.get(object))); } } else { SignatureField sgf = field.getAnnotation(SignatureField.class); if (StringUtils.isNotEmpty(sgf.customValue()) || !Objects.isNull(field.get(object))) { map.put(StringUtils.isNotBlank(sgf.customName()) ? sgf.customName() : field.getName() , StringUtils.isNotEmpty(sgf.customValue()) ? sgf.customValue() : toString(field.get(object))); } } } catch (Exception e) { LOGGER.error("簽名拼接(alphaSignature)異常", e); } } } for (Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator(); iterator.hasNext(); ) { Map.Entry<String, String> entry = iterator.next(); result.append(entry.getKey()).append("=").append(entry.getValue()); if (iterator.hasNext()) { result.append(DELIMETER); } } return result.toString(); } private static String toString(Object object) { Class<?> type = object.getClass(); if (BeanUtils.isSimpleProperty(type)) { return object.toString(); } if (type.isArray()) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < Array.getLength(object); ++i) { sb.append(toSplice(Array.get(object, i))); } return sb.toString(); } if (ClassUtils.isAssignable(Collection.class, type)) { StringBuilder sb = new StringBuilder(); for (Iterator<?> iterator = ((Collection<?>) object).iterator(); iterator.hasNext(); ) { sb.append(toSplice(iterator.next())); if (iterator.hasNext()) { sb.append(DELIMETER); } } return sb.toString(); } if (ClassUtils.isAssignable(Map.class, type)) { StringBuilder sb = new StringBuilder(); for (Iterator<? extends Map.Entry<String, ?>> iterator = ((Map<String, ?>) object).entrySet().iterator(); iterator.hasNext(); ) { Map.Entry<String, ?> entry = iterator.next(); if (Objects.isNull(entry.getValue())) { continue; } sb.append(entry.getKey()).append("=").append(toSplice(entry.getValue())); if (iterator.hasNext()) { sb.append(DELIMETER); } } return sb.toString(); } return NOT_FOUND; }
@ConfigurationProperties(prefix = "wmhopenapi.validate", exceptionIfInvalid = false) @Signature public class SignatureHeaders { public static final String SIGNATURE_HEADERS_PREFIX = "wmhopenapi-validate"; public static final Set<String> HEADER_NAME_SET = Sets.newHashSet(); private static final String HEADER_APPID = SIGNATURE_HEADERS_PREFIX + "-appid"; private static final String HEADER_TIMESTAMP = SIGNATURE_HEADERS_PREFIX + "-timestamp"; private static final String HEADER_NONCE = SIGNATURE_HEADERS_PREFIX + "-nonce"; private static final String HEADER_SIGNATURE = SIGNATURE_HEADERS_PREFIX + "-signature"; static { HEADER_NAME_SET.add(HEADER_APPID); HEADER_NAME_SET.add(HEADER_TIMESTAMP); HEADER_NAME_SET.add(HEADER_NONCE); HEADER_NAME_SET.add(HEADER_SIGNATURE); } /** * 線下分配的值 * 客戶端和服務端各自保存appId對應的appSecret */ @NotBlank(message = "Header中缺乏" + HEADER_APPID) @SignatureField private String appid; /** * 線下分配的值 * 客戶端和服務端各自保存,與appId對應 */ @SignatureField private String appsecret; /** * 時間戳,單位: ms */ @NotBlank(message = "Header中缺乏" + HEADER_TIMESTAMP) @SignatureField private String timestamp; /** * 流水號【防止重複提交】; (備註:針對查詢接口,流水號只用於日誌落地,便於後期日誌覈查; 針對辦理類接口需校驗流水號在有效期內的惟一性,以免重複請求) */ @NotBlank(message = "Header中缺乏" + HEADER_NONCE) @SignatureField private String nonce; /** * 簽名 */ @NotBlank(message = "Header中缺乏" + HEADER_SIGNATURE) private String signature; }
private SignatureHeaders generateSignatureHeaders(Signature signature, HttpServletRequest request) throws Exception { //處理header name Map<String, Object> headerMap = Collections.list(request.getHeaderNames()) .stream() .filter(headerName -> SignatureHeaders.HEADER_NAME_SET.contains(headerName)) .collect(Collectors.toMap(headerName -> headerName.replaceAll("-", "."), headerName -> request.getHeader(headerName))); //將header信息:name=value轉換成PropertySource PropertySource propertySource = new MapPropertySource("signatureHeaders", headerMap); //將header信息綁定到SignatureHeaders對象 SignatureHeaders signatureHeaders = RelaxedConfigurationBinder.with(SignatureHeaders.class) .setPropertySources(propertySource) .doBind(); Optional<String> result = ValidatorUtils.validateResultProcess(signatureHeaders); if (result.isPresent()) { throw new ServiceException("WMH5000", result.get()); } //從配置中拿到appid對應的appsecret String appSecret = limitConstants.getSignatureLimit().get(signatureHeaders.getAppid()); if (StringUtils.isBlank(appSecret)) { LOGGER.error("未找到appId對應的appSecret, appId=" + signatureHeaders.getAppid()); throw new ServiceException("WMH5002"); } //其餘合法性校驗 Long now = System.currentTimeMillis(); Long requestTimestamp = Long.parseLong(signatureHeaders.getTimestamp()); if ((now - requestTimestamp) > EXPIRE_TIME) { String errMsg = "請求時間超過規定範圍時間10分鐘, signature=" + signatureHeaders.getSignature(); LOGGER.error(errMsg); throw new ServiceException("WMH5000", errMsg); } String nonce = signatureHeaders.getNonce(); if (nonce.length() < 10) { String errMsg = "隨機串nonce長度最少爲10位, nonce=" + nonce; LOGGER.error(errMsg); throw new ServiceException("WMH5000", errMsg); } if (!signature.resubmit()) { String existNonce = redisCacheService.getString(nonce); if (StringUtils.isBlank(existNonce)) { redisCacheService.setex(nonce, nonce, (int) TimeUnit.MILLISECONDS.toSeconds(RESUBMIT_DURATION)); } else { String errMsg = "不容許重複請求, nonce=" + nonce; LOGGER.error(errMsg); throw new ServiceException("WMH5000", errMsg); } } //設置appsecret signatureHeaders.setAppsecret(appSecret); return signatureHeaders; }
生成簽名前須要以下幾個校驗步驟。spring
String headersToSplice = SignatureUtils.toSplice(signatureHeaders);
private List<String> generateAllSplice(Method method, Object[] args, String headersToSplice) { List<String> pathVariables = Lists.newArrayList(), requestParams = Lists.newArrayList(); String beanParams = StringUtils.EMPTY; for (int i = 0; i < method.getParameterCount(); ++i) { MethodParameter mp = new MethodParameter(method, i); boolean findSignature = false; for (Annotation anno : mp.getParameterAnnotations()) { if (anno instanceof PathVariable) { if (!Objects.isNull(args[i])) { pathVariables.add(args[i].toString()); } findSignature = true; } else if (anno instanceof RequestParam) { RequestParam rp = (RequestParam) anno; String name = mp.getParameterName(); if (StringUtils.isNotBlank(rp.name())) { name = rp.name(); } if (!Objects.isNull(args[i])) { List<String> values = Lists.newArrayList(); if (args[i].getClass().isArray()) { //數組 for (int j = 0; j < Array.getLength(args[i]); ++j) { values.add(Array.get(args[i], j).toString()); } } else if (ClassUtils.isAssignable(Collection.class, args[i].getClass())) { //集合 for (Object o : (Collection<?>) args[i]) { values.add(o.toString()); } } else { //單個值 values.add(args[i].toString()); } values.sort(Comparator.naturalOrder()); requestParams.add(name + "=" + StringUtils.join(values)); } findSignature = true; } else if (anno instanceof RequestBody || anno instanceof ModelAttribute) { beanParams = SignatureUtils.toSplice(args[i]); findSignature = true; } if (findSignature) { break; } } if (!findSignature) { LOGGER.info(String.format("簽名未識別的註解, method=%s, parameter=%s, annotations=%s", method.getName(), mp.getParameterName(), StringUtils.join(mp.getMethodAnnotations()))); } } List<String> toSplices = Lists.newArrayList(); toSplices.add(headersToSplice); toSplices.addAll(pathVariables); requestParams.sort(Comparator.naturalOrder()); toSplices.addAll(requestParams); toSplices.add(beanParams); return toSplices; }
generateAllSplice方法是在控制層切面內執行,能夠在方法執行以前獲取到已經綁定好的入參。分別對注有@PathVariable、@RequestParam、@RequestBody、@ModelAttribute註解的參數進行參數拼接的處理。其中注@RequestParam註解的參數須要特殊處理一下,分別考慮數組、集合、原始類型這三種狀況。apache
SignatureUtils.signature(allSplice.toArray(new String[]{}), signatureHeaders.getAppsecret());
//初始化請求頭信息 SignatureHeaders signatureHeaders = new SignatureHeaders(); signatureHeaders.setAppid("111"); signatureHeaders.setAppsecret("222"); signatureHeaders.setNonce(SignatureUtils.generateNonce()); signatureHeaders.setTimestamp(String.valueOf(System.currentTimeMillis())); List<String> pathParams = new ArrayList<>(); //初始化path中的數據 pathParams.add(SignatureUtils.encode("18237172801", signatureHeaders.getAppsecret())); //調用簽名工具生成簽名 signatureHeaders.setSignature(SignatureUtils.signature(signatureHeaders, pathParams, null, null)); System.out.println("簽名數據: " + signatureHeaders); System.out.println("請求數據: " + pathParams);
拼接結果: appid=111^_^appsecret=222^_^nonce=c9e778ba668c8f6fedf35634eb493af6304d54392d990262d9e7c1960b475b67^_^timestamp=1538207443910^_^w8rAwcXDxcDKwsM=^_^ 簽名數據: SignatureHeaders{appid=111, appsecret=222, timestamp=1538207443910, nonce=c9e778ba668c8f6fedf35634eb493af6304d54392d990262d9e7c1960b475b67, signature=0a7d0b5e802eb5e52ac0cfcd6311b0faba6e2503a9a8d1e2364b38617877574d} 請求數據: [w8rAwcXDxcDKwsM=]
上述的簽名方案的實現校驗邏輯是在控制層的切面內完成的。若是項目用的是springmvc框架,能夠放在Filter或者攔截器裏嗎?很明顯是不行的(由於ServletRequest的輸入流InputStream 在默認狀況只能讀取一次)。上述方案須要獲取綁定後的參數結果,而後執行簽名校驗邏輯。在執行控制層方法以前,springmvc已經幫咱們完成了綁定的步驟,固然了,在綁定的過程當中會解析ServletRequest中參數信息(例如path參數、parameter參數、body參數)。api
其實若是咱們能在Filter或者攔截器中實現上述方案,那麼複雜度將會大大的下降。首先考慮如何讓ServletRequest的輸入流InputStream能夠屢次讀取,而後經過ServletRequest獲取path variable(對應@PathVariable)、parameters(對應@RequestParam)、body(對應@RequestBody)參數,最後總體按照規則進行拼接並生成簽名。數組
優化方案參考:https://www.cnblogs.com/hujunzheng/p/10178584.html安全