HTTP任務簽名認證
更新時間:
為確保HTTP任務的服務接收方能安全地處理分布式任務調度平臺SchedulerX發起的調度請求,調度端會在HTTP請求頭中默認采用SHA1-RSA簽名算法生成schedulerx-signature字段簽名串,用于服務端做認證處理。本文介紹如何進行HTTP任務簽名認證。
簽名驗簽流程
驗證方案(JAVA)
您需要下載簽名證書來進行請求的簽名認證處理,具體驗證方法如下所示。
初始化構建AppKey與GroupId的映射配置,用于生成待簽數據。
獲取簽名時間戳進行有效時間校驗。例如,60秒內有效。
獲取簽名算法版本,校驗版本是否一致。
獲取待簽字符串,規則如下所示。
METHOD + "\n" + HTTP-URL & QUERY-STRING + "\n" + APP-KEY + "\n" + COOKIE + "\n" + CanonicalizedSCXHeaders + "\n" + POST-BODY
METHOD
:HTTP的請求方法。HTTP-URL & QUERY-STRING
:請求地址以及請求參數字符串。APP-KEY
:根據GroupID需要獲取對應的Appkey。COOKIE
:分布式任務調度平臺中配置的Cookie信息。CanonicalizedSCXHeaders
:HTTP請求頭中schedulerx-
開頭的字段組合,按字符順序排列。POST-BODY
:針對POST請求攜帶的body內容。
POST http://localhost:18080/hello?key=value AjS6+IQ4Czx/**********== cookie: schedulerx-attempt:0 schedulerx-datatimestamp:1626851714550 schedulerx-groupid:local.test schedulerx-jobid:12 schedulerx-jobname:httptest schedulerx-maxattempt:0 schedulerx-scheduletimestamp:1626851714550 schedulerx-signature-method:SHA1withRSA schedulerx-signature-timestamp:1626851714555 schedulerx-signature-version:1.0 schedulerx-user:%E5%8D%83x%28330965%29 test=test
簽名驗證過濾器參考代碼如下所示。
public class SignatureVerificationFilter implements Filter { private final String HTTP_JOB_HEADER_PREFIX = "schedulerx-"; private final String HTTP_SIGNATURE_VERSION = "1.0"; private final Map<String, String> appKeyMap = new HashMap<>(); @Override public void init(FilterConfig filterConfig) throws ServletException { //TODO 此處需要自行根據對接的應用構建對接的應用AppKey列表 appKeyMap.put("groupId", "APPKEY*******"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try { // 可獲取schedulerx-signature-timestamp進行失效驗證 Long signatureTimestamp = Long.parseLong(((HttpServletRequest) servletRequest).getHeader("schedulerx-signature-timestamp")); if(System.currentTimeMillis() - signatureTimestamp > 60*1000){ ((HttpServletResponse)servletResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED, "簽名已超時."); return; } // 判斷當前簽名驗證算法版本 String signatureVersion = ((HttpServletRequest) servletRequest).getHeader("schedulerx-signature-version"); if(!HTTP_SIGNATURE_VERSION.equals(signatureVersion)){ ((HttpServletResponse)servletResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED, "簽名算法版本已變更."); return; } // 獲取待簽名數據 String content = getSignContent((HttpServletRequest)servletRequest, ""); // 獲取請求頭中簽名信息 String signature = ((HttpServletRequest) servletRequest).getHeader("schedulerx-signature"); // 獲取證書 // 進行簽名驗證 boolean res = verify("/Users/yaohui/certificate.crt", content, signature); System.out.println("驗證結果:"+res); if(res) { filterChain.doFilter(servletRequest, servletResponse); }else { ((HttpServletResponse)servletResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED, "簽名驗證未通過."); } } catch (SignatureException e) { ((HttpServletResponse)servletResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED, "簽名驗證異常:" + e.getMessage()); } } /** * 對文本進行驗簽 * @param publicKeyPath * @param message * @param signature * @return * @throws SignatureException */ public static boolean verify(String publicKeyPath, String message, String signature) throws SignatureException{ try { Signature sign = Signature.getInstance("SHA1withRSA"); byte[] keyBytes = Files.readAllBytes(Paths.get(publicKeyPath)); X509Certificate cert = X509Certificate.getInstance(keyBytes); PublicKey publicKey = cert.getPublicKey(); sign.initVerify(publicKey); sign.update(message.getBytes("UTF-8")); return sign.verify(Base64.decodeBase64(signature.getBytes("UTF-8"))); } catch (Exception ex) { throw new SignatureException(ex); } } /** * 獲取加簽內容信息 * @param request * @param appKey * @return * @throws IOException */ private String getSignContent(HttpServletRequest request, String appKey) throws IOException { StringBuilder sb = new StringBuilder(); // 請求方式Method sb.append(request.getMethod()); sb.append("\n"); // http請求地址 String fullUrl = request.getRequestURL()+ (StringUtils.isEmpty(request.getQueryString())?"":"?"+URLDecoder.decode(request.getQueryString(), "UTF-8")); sb.append(fullUrl); sb.append("\n"); // 當前請求對應的AppKey sb.append(appKeyMap.get(request.getHeader("schedulerx-groupid"))); sb.append("\n"); // cookie信息 sb.append("cookie" + ":" + request.getHeader("cookie")); sb.append("\n"); List<String> schedulerXHeaders = new ArrayList(); //獲取請求頭信息 Enumeration headerNames = request.getHeaderNames(); //使用循環遍歷請求頭,并通過getHeader()方法獲取一個指定名稱的頭字段 while (headerNames.hasMoreElements()){ String headerName = (String) headerNames.nextElement(); // 過濾簽名頭 if (headerName.startsWith(HTTP_JOB_HEADER_PREFIX) && !"schedulerx-signature".equals(headerName)) { schedulerXHeaders.add(headerName + ":" + request.getHeader(headerName)); } } // 對SchedulerX相關的請求頭排序拼接 Collections.sort(schedulerXHeaders); for (String kv : schedulerXHeaders) { sb.append(kv); sb.append("\n"); } if (request.getMethod().equals("POST")) { // 對于POST將其請求內容作為加簽內容的一部分,對于該部分內容需要配套 InputStream is = request.getInputStream(); byte[] content = new byte[request.getContentLength()]; is.read(content); ContentType contentType = ContentType.parse(request.getContentType()); sb.append(new String(content, contentType.getCharset())); } return sb.toString(); } @Override public void destroy() {} }
說明CacheRequestInputStreamFilter
用于對Request請求中的inputStream
進行緩存處理,以便后續在針對POST請求驗簽時,獲取待簽數據內容。@Order(Ordered.HIGHEST_PRECEDENCE) class CacheRequestInputStreamFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ContentCachingRequestWrapper requestToUse = new ContentCachingRequestWrapper((HttpServletRequest) request); chain.doFilter(requestToUse, response); } @Override public void destroy() {} static class ContentCachingRequestWrapper extends HttpServletRequestWrapper { private byte[] body; private BufferedReader reader; private ServletInputStream inputStream; ContentCachingRequestWrapper(HttpServletRequest request) throws IOException{ super(request); InputStream is = request.getInputStream(); body = new byte[request.getContentLength()]; is.read(body); inputStream = new RequestCachingInputStream(body); } public byte[] getBody() { return body; } @Override public ServletInputStream getInputStream() throws IOException { if (inputStream != null) { return inputStream; } return super.getInputStream(); } @Override public BufferedReader getReader() throws IOException { if (reader == null) { reader = new BufferedReader(new InputStreamReader(inputStream, getCharacterEncoding())); } return reader; } private static class RequestCachingInputStream extends ServletInputStream { private final ByteArrayInputStream inputStream; public RequestCachingInputStream(byte[] bytes) { inputStream = new ByteArrayInputStream(bytes); } @Override public int read() throws IOException { return inputStream.read(); } @Override public boolean isFinished() { return inputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readlistener) {} } } }
文檔內容是否對您有幫助?