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) {} } } }
文档内容是否对您有帮助?