HTTP任务签名认证

更新时间:

为确保HTTP任务的服务接收方能安全地处理分布式任务调度平台SchedulerX发起的调度请求,调度端会在HTTP请求头中默认采用SHA1-RSA签名算法生成schedulerx-signature字段签名串,用于服务端做认证处理。本文介绍如何进行HTTP任务签名认证。

签名验签流程

1626857968216-b3e05058-8d62-4daa-9f1e-8b34da20e5ba..jpeg

验证方案(JAVA)

您需要下载签名证书来进行请求的签名认证处理,具体验证方法如下所示。

  1. 初始化构建AppKey与GroupId的映射配置,用于生成待签数据。

  2. 获取签名时间戳进行有效时间校验。例如,60秒内有效。

  3. 获取签名算法版本,校验版本是否一致。

  4. 获取待签字符串,规则如下所示。

    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) {}
    
            }
        }
    }