Signature authentication for HTTP tasks

更新时间:
复制 MD 格式

To ensure that the service that receives an HTTP task can securely process scheduling requests from Distributed Task Scheduling Platform (SchedulerX), the scheduler generates a signature string. By default, the scheduler uses the SHA1-RSA signature algorithm and adds this string to the `schedulerx-signature` field in the HTTP request header. The server uses this signature for authentication. This topic describes how to perform signature authentication for HTTP tasks.

Signature verification flow

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

Validation solution (Java)

You can download the signing certificate to authenticate the request signature. The validation method is as follows.

  1. Initialize the mapping configuration between the AppKey and GroupId. This mapping is used to generate the data to be signed.

  2. Retrieve the signature timestamp and check its validity period. For example, the signature is valid for 60 seconds.

  3. Retrieve the signature algorithm version and verify that the version is correct.

  4. Retrieve the string to be signed. The rules are as follows.

    METHOD + "\n"
    + HTTP-URL & QUERY-STRING + "\n"
    + APP-KEY + "\n"
    + COOKIE + "\n"
    + CanonicalizedSCXHeaders + "\n"
    + POST-BODY
    • METHOD: The HTTP request method.

    • HTTP-URL & QUERY-STRING: The request URL and the request parameter string.

    • APP-KEY: The AppKey that corresponds to the GroupID.

    • COOKIE: The cookie information configured in SchedulerX.

    • CanonicalizedSCXHeaders: A combination of fields in the HTTP request header that start with `schedulerx-`. The fields are sorted in alphabetical order.

    • POST-BODY: The body content of a POST request.

    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

    The following code provides an example of a signature verification filter.

    Expand to view the code

    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: Build a list of AppKeys for your applications.
            appKeyMap.put("groupId", "APPKEY*******");
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            try {
                // Obtain schedulerx-signature-timestamp to check for expiration.
                Long signatureTimestamp = Long.parseLong(((HttpServletRequest) servletRequest).getHeader("schedulerx-signature-timestamp"));
                if(System.currentTimeMillis() - signatureTimestamp > 60*1000){
                    ((HttpServletResponse)servletResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Signature has timed out.");
                    return;
                }
                
                // Check the current version of the signature verification algorithm.
                String signatureVersion = ((HttpServletRequest) servletRequest).getHeader("schedulerx-signature-version");
                if(!HTTP_SIGNATURE_VERSION.equals(signatureVersion)){
                    ((HttpServletResponse)servletResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED, "The signature algorithm version has changed.");
                    return;
                }
             	// Get the data to be signed.
                String content = getSignContent((HttpServletRequest)servletRequest, "");
                
                // Get the signature from the request header.
                String signature = ((HttpServletRequest) servletRequest).getHeader("schedulerx-signature");
                
                // Get the certificate.
                // Perform signature verification.
                boolean res = verify("/Users/yaohui/certificate.crt", content, signature);
                System.out.println("Verification result: "+res);
                if(res) {
                    filterChain.doFilter(servletRequest, servletResponse);
                }else {
                    ((HttpServletResponse)servletResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Signature verification failed.");
                }
            } catch (SignatureException e) {
                ((HttpServletResponse)servletResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Signature verification error: " + e.getMessage());
            }
        }
    
        /**
         * Verifies the signature of the text.
         * @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);
            }
        }
    
        /**
         * Gets the content to be signed.
         * @param request
         * @param appKey
         * @return
         * @throws IOException
         */
        private String getSignContent(HttpServletRequest request, String appKey) throws IOException {
            StringBuilder sb = new StringBuilder();
            // Request method
            sb.append(request.getMethod());
            sb.append("\n");
            // HTTP request URL
            String fullUrl = request.getRequestURL()+ (StringUtils.isEmpty(request.getQueryString())?"":"?"+URLDecoder.decode(request.getQueryString(), "UTF-8"));
            sb.append(fullUrl);
            sb.append("\n");
            // The AppKey corresponding to the current request
            sb.append(appKeyMap.get(request.getHeader("schedulerx-groupid")));
            sb.append("\n");
    		// Cookie information
            sb.append("cookie" + ":" + request.getHeader("cookie"));
            sb.append("\n");
    
            List<String> schedulerXHeaders = new ArrayList();
            // Get request header information.
            Enumeration headerNames = request.getHeaderNames();
            // Traverse the request headers in a loop and use the getHeader() method to get the value of a specified header.
            while (headerNames.hasMoreElements()){
                String headerName = (String) headerNames.nextElement();
                // Filter the signature header.
                if (headerName.startsWith(HTTP_JOB_HEADER_PREFIX) && !"schedulerx-signature".equals(headerName)) {
                    schedulerXHeaders.add(headerName + ":" + request.getHeader(headerName));
                }
            }
            // Sort and concatenate the SchedulerX-related request headers.
            Collections.sort(schedulerXHeaders);
            for (String kv : schedulerXHeaders) {
                sb.append(kv);
                sb.append("\n");
            }
            if (request.getMethod().equals("POST")) {
                // For a POST request, its content is part of the data to be signed.
                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() {}
    }
    Note

    The CacheRequestInputStreamFilter caches the inputStream from the request. This lets you retrieve the data to be signed when you verify the signature for a POST request.

    Expand to view the code

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