Why do you need a signature?

Imagine a scene: a friend, whom you haven’t seen for a long time, suddenly asks you “Friend, I need 200 urgent messages” in wechat. How would you react?

I think most people’s immediate reaction is: is it stolen? Is he in person?

In fact, this is a common communication behavior in our daily life. The process of calling API and transmitting data between systems is no different from wechat communication between you and your friends. All data transmission in an open environment can be intercepted or even tampered with. Therefore, data transmission is very dangerous, so it must be signed and encrypted.

Signature core solves two problems:

  1. Whether the request is legitimate: Whether it is the person I have specified

  2. Request tampered with: Whether a third party hijacked the request and tampered with the parameters

  3. Prevent repeated requests (anti-replay) : Whether to repeat requests

How to sign

Signature algorithm logic

In the first step, set all the data sent or received as set M, sort the parameters of non-empty parameter values in set M in alphabetical order according to the ASCII code of parameter names, and splice them into string stringA using the following format

key1value1key2value2...
Copy the code

Pay special attention to the following important rules:

  • Parameter names in alphabetical order (ASCII);
  • If the parameter value is null, the signature is not involved.
  • Parameter names are case-sensitive;
  • The passed sign parameter does not participate in signing;

Second, the secret key is concatenated at the end of stringA to get the stringSignTemp string

In the third step, MD5 encryption is performed on stringSignTemp to obtain signValue

Defend against replay attacks

These measures is still not the most rigorous, although counterfeiters could not easily imitate signature rules generating the same signature, in fact, if the counterfeiters to monitor and intercept the request, and then leave the signature by intercepting out imitation of formal request party cheat server for repeated requests, this can cause security issues, This attack is called a replay attack.

We can add two parameters timestamp + nonce to control the request validity and prevent replay attacks.

timestamp

Request end: Timestamp is generated by the requester, representing the time when the request is sent (both parties need to share a time counting system) together with the request parameters, and timestamp is added to sign encryption calculation as a parameter.

Server: after receiving the request, the platform server compares the current timestamp and considers the request as normal within 60 seconds; otherwise, it considers timeout and does not give feedback (due to the existence of the actual transmission time difference, it is impossible to reduce the timeout time indefinitely). But that’s still not enough, the impersonator still has 60 seconds to mimic the request for a replay attack. So further, we can add a random code (called the salt value) to sign, which we define as a nonce.

nonce

Requester: Nonce is a random number generated by the requester (sufficient random number is guaranteed to be generated within the specified time, that is, the probability of random number repetition generated within 60 seconds is 0). It is also added into the sign signature as one of the parameters.

Server: After receiving the request, the server determines whether the nonce parameter has been requested (generally put into Redis). If the nonce parameter is found to be new at the specified time, the result is normally returned. Otherwise, the server determines that it is a replay attack. As the above two parameters are also written into the signature, the attacker deliberately adds or forges timestamp and Nonce in an attempt to escape the re-release judgment, which will result in signature failure.

Front-end signature generation

Interception is generally unified at the point where AXIos sends the request

// npm install crypto-js
import MD5 from 'crypto-js/md5';

// Gets a random number of specified bits
function getRandom(num) {
  return Math.floor((Math.random() + Math.floor(Math.random() * 9 + 1)) * Math.pow(10, num - 1))}function genSign(params) {
  / / key
  const secret = 'xxxxxxxxxxxxxxxxxxxxxxx'
  // 1O bit timestamp
  const timestampStr = parseInt(new Date().getTime() / 1000).toString()
  // 20 random digits
  const nonce = getRandom(20).toString()
  
  params.timestampStr = timestampStr
  params.nonce = nonce

  / / get the key
  const sortedKeys = []
  for (const key in params) {
    // Note that the sign argument itself is removed
    if(key ! = ='sign') {
      sortedKeys.push(key)
    }
  }
  // Parameter names in alphabetical order from ASCII to largest
  sortedKeys.sort()

  // 1 Concatenation parameters
  let str = ' '
  sortedKeys.forEach(key= > {
    str += key + params[key]
  })
  // 2 Splices the key
  str += secret
  // 3 MD5 encryption
  params.sign = MD5(str).toString().toUpperCase()
}

export default genSign
Copy the code

How to solve the time difference problem

If the time on the client is inconsistent with that on the server (the time on the client is 2 minutes earlier than that on the server) and the request is valid within one minute, the signature cannot be approved.

  1. The first time the application is opened to get the local time, then the interface is asked to get the server time.
      // Get the server time
      function ajax(option){
          var xhr = null;
          if(window.XMLHttpRequest){
            xhr = new window.XMLHttpRequest();
          }else{ // ie
            xhr = new ActiveObject("Microsoft")}// Request the current file through get
          xhr.open("get"."/");
          xhr.send(null);
          // Listen for request status changes
          xhr.onreadystatechange = function(){
            var time = null,
                curDate = null;
            if(xhr.readyState===2) {// Get the timestamp in the response header
              time = xhr.getResponseHeader("Date");
              console.log(xhr.getAllResponseHeaders())
              curDate = new Date(time);
              console.log(curDate)
            }
          }
        }
    Copy the code
  2. Save the time difference to local storage
  3. Add the local time and time difference when requesting an interface.

The back-end generates signatures

import org.apache.commons.codec.binary.Hex;
import java.security.MessageDigest;

@Slf4j
public class SignUtil {
    / / key
    private static final String SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
    private static final String SIGN = "sign";
    private static final String NONCE = "nonce";
    private static final String TIMESTAMP = "timestamp";
    private static final String SIGN_KEY = "apisign_";

    /** * generates *@param params
     * @return* /
    public static String genSign(TreeMap<String, Object> params) {
        params.remove(SIGN);
        StringBuilder str = new StringBuilder();
        for (String key : params.keySet()) {
            Object val = params.get(key);
            if (ObjectUtil.isNotNull(val)) {
                // 1 Concatenation parametersstr.append(key).append(val); }}// 2 Splice the secret key
        str.append(SECRET);
        // 3 MD5 encryption
        return md5(str.toString());
    }

    public static String md5(String source) {
        String md5Result = null;
        try {
            byte[] hash = org.apache.commons.codec.binary.StringUtils.getBytesUtf8(source);
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            messageDigest.update(hash);
            hash = messageDigest.digest();
            md5Result = Hex.encodeHexString(hash);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        returnmd5Result; }}Copy the code

Verify the signature

  public static void validateSign(Map<String, Object> params) {
         // redis
        SingleRedisCacheClient cacheClient = ServiceBean.getSpringContext().getBean(SingleRedisCacheClient.class);

        String sign = (String) params.get(SIGN);
        if (StringUtils.isNotBlank(sign)) {
            if (StringUtils.isBlank(sign)) {
                throw new RuntimeException("Signature cannot be blank.");
            }

            String nonce = (String) params.get(NONCE);
            if (StringUtils.isBlank(nonce)) {
                throw new RuntimeException("Random strings cannot be empty.");
            }

            String timestampStr = (String) params.get(TIMESTAMP);
            if (StringUtils.isBlank(timestampStr)) {
                throw new RuntimeException("Time stamp cannot be empty.");
            }

            long timestamp = 0;
            try {
                timestamp = Long.parseLong(timestampStr);
            } catch (Exception e) {
                log.error("Abnormal",e);
            }
            // If the difference between the requested timestamp and the server's current timestamp is greater than 120, the requested timestamp is invalid
            if (Math.abs(timestamp - System.currentTimeMillis() / 1000) > 120) {
                throw new RuntimeException("Signature expired");
            }
            
            // If the requested random number exists in Redis, the requested nonce is invalid
            boolean nonceExists = cacheClient.hasKey(SIGN_KEY + timestampStr + nonce);
            if (nonceExists) {
                throw new RuntimeException("Random string already exists");
            }

            // Construct a signature based on the parameters passed by the request. If the signature is inconsistent with the interface signature, the request parameters are tampered with
            TreeMap<String, Object> signTreeMap = new TreeMap<>();
            signTreeMap.putAll(params);
            String currentSign = genSign(signTreeMap);
            if(! sign.equalsIgnoreCase(currentSign)) {throw new RuntimeException("Signature does not match");
            }

            / / in the redis
            cacheClient.setCacheWithExpire(SIGN_KEY + timestampStr+ nonce, nonce, 120L); }}Copy the code