因爲以前寫過的一片文章 (java接口簽名(Signature)實現方案 )收穫了不少好評,這次來講一下另外一種簡單粗暴的簽名方案。相對於以前的簽名方案,對body、paramenter、path variable的獲取都作了簡化的處理。也就是說這種方式針全部數據進行了簽名,並不能指定某些數據進行簽名。html
一、線下分配appid和appsecret,針對不一樣的調用方分配不一樣的appid和appsecretjava
二、加入timestamp(時間戳),10分鐘內數據有效web
三、加入流水號nonce(防止重複提交),至少爲10位。針對查詢接口,流水號只用於日誌落地,便於後期日誌覈查。 針對辦理類接口需校驗流水號在有效期內的惟一性,以免重複請求。算法
四、加入signature,全部數據的簽名信息。spring
以上紅色字段放在請求頭中。apache
signature 字段生成規則以下。json
Path Variable:按照path中的字典順序將全部value進行拼接app
Parameter:按照key=values(多個value按照字典順序拼接)字典順序進行拼接curl
Body:從request inputstream中獲取保存爲String形式ide
若是存在多種數據形式,則按照body、parameter、path variable的順序進行再拼接,獲得全部數據的拼接值。
上述拼接的值記做 Y。
X=」appid=xxxnonce=xxxtimestamp=xxx」
最終拼接值=XY
最後將最終拼接值按照以下方法進行加密獲得簽名。
signature=org.apache.commons.codec.digest.HmacUtils.AesEncodeUtil(app secret, 拼接的值);
注:省去了X=」appid=xxxnonce=xxxtimestamp=xxx」這部分。
爲何要自定義request對象,由於咱們要獲取request inputstream(默認只能獲取一次)。
public class BufferedHttpServletRequest extends HttpServletRequestWrapper { private ByteBuf buffer; private final AtomicBoolean isCached = new AtomicBoolean(); public BufferedHttpServletRequest(HttpServletRequest request, int initialCapacity) { super(request); int contentLength = request.getContentLength(); int min = Math.min(initialCapacity, contentLength); if (min < 0) { buffer = Unpooled.buffer(0); } else { buffer = Unpooled.buffer(min, contentLength); } } @Override public ServletInputStream getInputStream() throws IOException { //Only returning data from buffer if it is readonly, which means the underlying stream is EOF or closed. if (isCached.get()) { return new NettyServletInputStream(buffer); } return new ContentCachingInputStream(super.getInputStream()); } public void release() { buffer.release(); } private class ContentCachingInputStream extends ServletInputStream { private final ServletInputStream is; public ContentCachingInputStream(ServletInputStream is) { this.is = is; } @Override public int read() throws IOException { int ch = this.is.read(); if (ch != -1) { //Stream is EOF, set this buffer to readonly state buffer.writeByte(ch); } else { isCached.compareAndSet(false, true); } return ch; } @Override public void close() throws IOException { //Stream is closed, set this buffer to readonly state try { is.close(); } finally { isCached.compareAndSet(false, true); } } @Override public boolean isFinished() { throw new UnsupportedOperationException("Not yet implemented!"); } @Override public boolean isReady() { throw new UnsupportedOperationException("Not yet implemented!"); } @Override public void setReadListener(ReadListener readListener) { throw new UnsupportedOperationException("Not yet implemented!"); } } }
替換默認的request對象
@Configuration public class FilterConfig { @Bean public RequestCachingFilter requestCachingFilter() { return new RequestCachingFilter(); } @Bean public FilterRegistrationBean requestCachingFilterRegistration( RequestCachingFilter requestCachingFilter) { FilterRegistrationBean bean = new FilterRegistrationBean(requestCachingFilter); bean.setOrder(1); return bean; } }
public class RequestCachingFilter extends OncePerRequestFilter { private static Logger LOGGER = LoggerFactory.getLogger(RequestCachingFilter.class); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { boolean isFirstRequest = !isAsyncDispatch(request); HttpServletRequest requestToUse = request; if (isFirstRequest && !(request instanceof BufferedHttpServletRequest)) { requestToUse = new BufferedHttpServletRequest(request, 1024); } try { filterChain.doFilter(requestToUse, response); } catch (Exception e) { LOGGER.error("RequestCachingFilter>>>>>>>>>", e); } finally { this.printRequest(requestToUse); if (requestToUse instanceof BufferedHttpServletRequest) { ((BufferedHttpServletRequest) requestToUse).release(); } } } private void printRequest(HttpServletRequest request) { String body = StringUtils.EMPTY; try { if (request instanceof BufferedHttpServletRequest) { body = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); } } catch (IOException e) { LOGGER.error("printRequest 獲取body異常...", e); } JSONObject requestJ = new JSONObject(); JSONObject headers = new JSONObject(); Collections.list(request.getHeaderNames()) .stream() .forEach(name -> headers.put(name, request.getHeader(name))); requestJ.put("headers", headers); requestJ.put("parameters", request.getParameterMap()); requestJ.put("body", body); requestJ.put("remote-user", request.getRemoteUser()); requestJ.put("remote-addr", request.getRemoteAddr()); requestJ.put("remote-host", request.getRemoteHost()); requestJ.put("remote-port", request.getRemotePort()); requestJ.put("uri", request.getRequestURI()); requestJ.put("url", request.getRequestURL()); requestJ.put("servlet-path", request.getServletPath()); requestJ.put("method", request.getMethod()); requestJ.put("query", request.getQueryString()); requestJ.put("path-info", request.getPathInfo()); requestJ.put("context-path", request.getContextPath()); LOGGER.info("Request-Info: " + JSON.toJSONString(requestJ, SerializerFeature.PrettyFormat)); } }
@Aspect @Component public class SignatureAspect { private static final Logger LOGGER = LoggerFactory.getLogger(StringUtils.class); @Around("execution(* com..controller..*.*(..)) " + "&& (@annotation(org.springframework.web.bind.annotation.RequestMapping)" + "|| @annotation(org.springframework.web.bind.annotation.GetMapping)" + "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" + "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)" + "|| @annotation(org.springframework.web.bind.annotation.PatchMapping))" ) public Object doAround(ProceedingJoinPoint pjp) throws Throwable { try { this.checkSign(); return pjp.proceed(); } catch (Throwable e) { LOGGER.error("SignatureAspect>>>>>>>>", e); throw e; } } private void checkSign() throws Exception { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); String oldSign = request.getHeader("X-SIGN"); if (StringUtils.isBlank(oldSign)) { throw new RuntimeException("取消簽名Header[X-SIGN]信息"); } //獲取body(對應@RequestBody) String body = null; if (request instanceof BufferedHttpServletRequest) { body = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); } //獲取parameters(對應@RequestParam) Map<String, String[]> params = null; if (!CollectionUtils.isEmpty(request.getParameterMap())) { params = request.getParameterMap(); } //獲取path variable(對應@PathVariable) String[] paths = null; ServletWebRequest webRequest = new ServletWebRequest(request, null); Map<String, String> uriTemplateVars = (Map<String, String>) webRequest.getAttribute( HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); if (!CollectionUtils.isEmpty(uriTemplateVars)) { paths = uriTemplateVars.values().toArray(new String[]{}); } try { String newSign = SignUtil.sign(body, params, paths); if (!newSign.equals(oldSign)) { throw new RuntimeException("簽名不一致..."); } } catch (Exception e) { throw new RuntimeException("驗簽出錯...", e); } } }
分別獲取了request inputstream中的body信息、parameter信息、path variable信息。
public class SignUtil { private static final String DEFAULT_SECRET = "1qaz@WSX#$%&"; public static String sign(String body, Map<String, String[]> params, String[] paths) { StringBuilder sb = new StringBuilder(); if (StringUtils.isNotBlank(body)) { sb.append(body).append('#'); } if (!CollectionUtils.isEmpty(params)) { params.entrySet() .stream() .sorted(Map.Entry.comparingByKey()) .forEach(paramEntry -> { String paramValue = String.join(",", Arrays.stream(paramEntry.getValue()).sorted().toArray(String[]::new)); sb.append(paramEntry.getKey()).append("=").append(paramValue).append('#'); }); } if (ArrayUtils.isNotEmpty(paths)) { String pathValues = String.join(",", Arrays.stream(paths).sorted().toArray(String[]::new)); sb.append(pathValues); } String createSign = HmacUtils.hmacSha256Hex(DEFAULT_SECRET, sb.toString()); return createSign; } public static void main(String[] args) { String body = "{\n" + "\t\"name\": \"hjzgg\",\n" + "\t\"age\": 26\n" + "}"; Map<String, String[]> params = new HashMap<>(); params.put("var3", new String[]{"3"}); params.put("var4", new String[]{"4"}); String[] paths = new String[]{"1", "2"}; System.out.println(sign(body, params, paths)); } }
簡單寫了一個包含body參數,parameter參數,path variable參數的controller
@RestController @RequestMapping("example") public class ExampleController { @PostMapping(value = "test/{var1}/{var2}", produces = MediaType.ALL_VALUE) public String myController(@PathVariable String var1 , @PathVariable String var2 , @RequestParam String var3 , @RequestParam String var4 , @RequestBody User user) { return String.join(",", var1, var2, var3, var4, user.toString()); } private static class User { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return new ToStringBuilder(this) .append("name", name) .append("age", age) .toString(); } } }
經過 簽名核心工具類SignUtil 的main方法生成一個簽名,經過以下命令驗證
curl -X POST \ 'http://localhost:8080/example/test/1/2?var3=3&var4=4' \ -H 'Content-Type: application/json' \ -H 'X-SIGN: 4955125a3aa2782ab3def51dc958a34ca46e5dbb345d8808590fb53e81cc2687' \ -d '{ "name": "hjzgg", "age": 26 }'
請關注訂閱號,回覆:signature, 即可查看。
就先分享這麼多了,更多分享請關注咱們的技術公衆號!!!