Purpose of interface signature:

  • Tamper-proof to ensure data integrity
  • Authentication, which verifies the identity of the caller and validates the request

You also need to prevent replay of requests

process

  1. Client to server register appId (client id), appSecret
  2. The client concatenates the request parameters into a string, calculates the signature value, and appends it to the request parameters
  3. The server verifies the signature

There are two signature algorithms: HAMC based on symmetric key and SHA256withRSA based on asymmetric encryption

Message Authentication Code (MAC)

  • Hmac: hash-based message authentication Code Indicates the message authentication code based on the Hash algorithm

Hmac consists of hash functions and private keys. Authentication used to verify data integrity and messages

define

  • Mac = H (key | | message)
  • H(ke || H(key || message))

  • H hash function
  • K the private key
  • M messages
  • K’ padding key, hashed key
  • ipad inner padding
  • opad outer padding

Different hashing function choices lead to different security;

Hmac-sha256, hmac-md5

public static String HMACSHA256(String strToSign, String key) throws Exception {
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
        sha256_HMAC.init(secret_key);
        byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
        System.out.println(array.length);
        return Hex.encodeHexString(array).toUpperCase();
    }
Copy the code

The length of the array is 32. 32 x 8 = 256 bits

HMACwithRsa256 (Based on asymmetric encryption)

signed = Rsa(sha256(text))

public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
    public static final String ENCODE_ALGORITHM = "SHA-256";

    public static byte[] sign(PrivateKey privateKey, String plainText){
        MessageDigest messageDigest;
        byte[] signed;
        try{
            messageDigest = MessageDigest.getInstance(ENCODE_ALGORITHM);
            messageDigest.update(plainText.getBytes());
            byte[] outputDigest = messageDigest.digest();
            Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
            signature.initSign(privateKey);
            signature.update(outputDigest);
            signed = signature.sign();
        }catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
            e.printStackTrace();
            return null;
        }
        return signed;
    }

    public static boolean verifySign(PublicKey publicKey, String plainText, byte[] signed){
        MessageDigest messageDigest;
        boolean result = false;
        try{
            messageDigest = MessageDigest.getInstance(ENCODE_ALGORITHM);
            messageDigest.update(plainText.getBytes());
            byte[] outputToVerify = messageDigest.digest();
            Signature verifySign = Signature.getInstance(SIGNATURE_ALGORITHM);
            verifySign.initVerify(publicKey);
            verifySign.update(outputToVerify);
            result = verifySign.verify(signed);
        } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
            e.printStackTrace();
        }
        return result;
    }
Copy the code

Prevent replay

  • Timestamp: request with a timestamp, the server intercepts the request after a certain time
  • Nonce: Each request is accompanied by a random number (UUID) that cannot be repeated within a period of time. The server logs the Nonce in the cache

Interface signature algorithm

strToSign = “param1=val1&param2=val2%…” + “& body = ‘… ‘” + “&” key = prvKey;

  • The request get parameters are in lexicographical order;
  • The json data in the request body is also serialized in lexicographical order.

1. Server implementation of HMAC signature

Based on the Spring Security framework, add interceptors to the interceptor chain to verify signatures

public class AppAuthenticationFilter extends GenericFilter { static final String FILTER_APPLIED = "_app_filter_applied";  private AuthenticationManager authenticationManager; public AppAuthenticationFilter(AuthenticationManager authenticationManager){ this.authenticationManager = authenticationManager; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequestWrapper request = (HttpServletRequestWrapper) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (request.getAttribute(FILTER_APPLIED) ! = null) {// The filter will execute chain.dofilter (request, response) only once; return; } request.setAttribute(FILTER_APPLIED, Boolean.TRUE); String appId = request.getParameter(" appID "); if (StringUtils.isEmpty(appId)){ chain.doFilter(request, response); return; } //1 Splicing signature String String paramStr = sortParamForApp(request); String body = inputStream2String(request.getInputStream()); StringBuilder sbToSign = new StringBuilder(); if (StringUtils.isNotEmpty(paramStr)){ sbToSign.append(paramStr); } if (StringUtils.isNotEmpty(body)){ sbToSign.append("&body=").append(body); } //2 AppAuthenticationToken token = new AppAuthenticationToken(); token.setAppId(request.getParameter("appId")); token.setNonce(request.getParameter("nonce")); token.setTimeStamp(Long.parseLong(request.getParameter("timestamp"))); token.setSignature(request.getParameter("signature")); token.setStringToSign(sbToSign.toString()); try { Authentication authentication = authenticationManager.authenticate(token); if (authentication ! Success = null) {/ / 3 certification, set up a security context SecurityContextHolder. GetContext (). SetAuthentication (authentication); chain.doFilter(request, response); } }catch (AuthenticationException e){ e.printStackTrace(); throw e; }}}Copy the code

Adding interceptors

http.addFilterBefore(new AppAuthenticationFilter(providerManager()), UsernamePasswordAuthenticationFilter.class);
Copy the code

Provide the certification

public class AppAuthenticationProvider implements AuthenticationProvider { private static final int DEFAULT_TIMEOUT = 30; // 30S private static final String PREFIX_NONCE="printer-nonce"; // key = PREFIX_NONCE:${appId}:${nonce}, value = timeStamp private AppDetailsService appDetailsService; private int timeout; // Request valid duration (now() -timestamp < timeout) @autoWired private Jedis Jedis; public AppAuthenticationProvider(AppDetailsService appDetailsService){ this.appDetailsService = appDetailsService; this.timeout = DEFAULT_TIMEOUT; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { AppAuthenticationToken token = (AppAuthenticationToken) authentication; String appId = token.getAppId(); If (stringutils.isempty (appId)){throw new AppIdNotFoundException("appId cannot be empty "); } AppDetails appDetails = appDetailsService.loadAppDetails(appId); If (appDetails == null) {throw new AppIdNotFoundException("appId not exist "); } if (! Appdetails.isenabled ()){throw new AppIdNotFoundException("app disabled "); Boolean verifySuccess = hmacUtil.verifysign (tok.getStringToSign (), tok.getSignature ()); appDetails.appKey()); if (! VerifySuccess) {throw new AppInvalidSignException("app call signature incorrect "); } //2. Verify nonce String nonceKey = PREFIX_NONCE + ":" + appdetails.appid () + ":" + token.getnonce (); boolean nonceExist = jedis.exists(nonceKey); If (nonceExist){throw new NonceExistException("nonce exists "+ token.getnonce ()); if (nonceExist){throw new NonceExistException("nonce exists" + token.getnonce ()); }else{ jedis.setex(nonceKey, DEFAULT_TIMEOUT, String.valueOf(System.currentTimeMillis())); } if (system.currentTimemillis () -token.gettimestamp () > DEFAULT_TIMEOUT*1000){throw new InvalidTimeStampException (" timestamp illegal "); } return new AppAuthenticationToken( appDetails, token, Arrays.asList(new SimpleGrantedAuthority("APP")) ); } @Override public boolean supports(Class<? > authentication) { return AppAuthenticationToken.class.isAssignableFrom(authentication); }}Copy the code

Example Query app information

public interface AppDetailsService {

    AppDetails loadAppDetails(String appId) throws AppIdNotFoundException;

}
Copy the code

HmacUtil Symmetric signature utility class

package cn.raiyee.printer.util; import org.apache.commons.codec.binary.Hex; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; public class HMACUtil { public static String generateHash(String content, String prvKey){ String encodeStr = ""; try { MessageDigest digest = MessageDigest.getInstance("sha-256"); String strToSign = content + "&key=" + prvKey; digest.update(strToSign.getBytes()); byte[] output = digest.digest(); encodeStr = Base64.getUrlEncoder().encodeToString(output); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return encodeStr; } public static boolean verifySign(String content, String hash, String prvKey) { boolean success = false; try { MessageDigest digest = MessageDigest.getInstance("sha-256"); String strToSign = content + "&key=" + prvKey; digest.update(strToSign.getBytes()); byte[] output = digest.digest(); String encodeStr = Base64.getUrlEncoder().encodeToString(output); success = encodeStr.equals(hash); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return success; } public static String HMACSHA256(String data, String key) throws Exception { Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"); sha256_HMAC.init(secret_key); byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8")); return Hex.encodeHexString(array).toUpperCase(); }}Copy the code

Client call interface:

void testAppSign() { HttpHeaders header = new HttpHeaders(); header.add("Accept", "application/json"); AgentProxyReq AgentProxyReq = AgentProxyreq.Builder ().agentid ("2").AgentPhonename (" mI ").AgentPhoneType (" mI ") .merchantId("123") .agentPubKey("222") .build(); String timestamp = String.valueOf(System.currentTimeMillis()); String nonce = UUID.randomUUID().toString(); String appId = "473632086310273024"; Map<String, String> params = new TreeMap<>(); params.put("appId", appId); params.put("timestamp", timestamp); params.put("nonce", nonce); StringBuilder sb = new StringBuilder(); for (Map.Entry<String,String> entry : params.entrySet()){ sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } if (sb.length() > 0){ sb.deleteCharAt(sb.length()-1); } sb.append("&body=").append(JSON.toJSONString(agentProxyReq)); System.out.println("stringToSign = " + sb.toString()); HttpEntity<AgentProxyReq> requestEntity = new HttpEntity(agentProxyReq, header); String signature = HMACUtil.generateHash(sb.toString(), "acb196c11e904ebeae93ff86ed9bcfbe059c837b02784e45814d9fc33fb58256"); System.out.println("signature = " + signature); RestTemplate restTemplate = new RestTemplate(); ResponseEntity < Result > resultResponseEntity = restTemplate. PostForEntity (" http://127.0.0.1:8006/printer/app/test? appId="+appId+"&signature=" +signature+"&timestamp="+timestamp+"&nonce=" + nonce , requestEntity, Result.class, params);  System.out.println(resultResponseEntity); Result result = resultResponseEntity.getBody(); System.out.println(result); }Copy the code

SHA256withRSA server implementation

The interceptor

public class AgentAuthenticationFilter extends GenericFilter { static final String FILTER_APPLIED = "_agent_filter_applied"; private AuthenticationManager authenticationManager; public AgentAuthenticationFilter(AuthenticationManager authenticationManager){ this.authenticationManager = authenticationManager; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequestWrapper request = (HttpServletRequestWrapper) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (request.getAttribute(FILTER_APPLIED) ! = null) {// The filter will execute chain.dofilter (request, response) only once; return; } request.setAttribute(FILTER_APPLIED, Boolean.TRUE); String agentId = request.getParameter("agentId"); if (StringUtils.isEmpty(agentId)){ chain.doFilter(request, response); return; } //1 Splicing signature String String paramStr = sortParamForApp(request); String body = inputStream2String(request.getInputStream()); StringBuilder sbToSign = new StringBuilder(); if (StringUtils.isNotEmpty(paramStr)){ sbToSign.append(paramStr); } if (StringUtils.isNotEmpty(body)){ sbToSign.append("&body=").append(body); } //2 AgentAuthenticationToken token = new Agentauthtoken (); token.setAgentId(agentId); token.setSignature(request.getParameter("signature")); token.setTimestamp(Long.parseLong(request.getParameter("timestamp"))); token.setStringToSign(sbToSign.toString()); try { Authentication authentication = authenticationManager.authenticate(token); if (authentication ! Success = null) {/ / 3 certification, set up a security context SecurityContextHolder. GetContext (). SetAuthentication (authentication); chain.doFilter(request, response); } }catch (AuthenticationException e){ e.printStackTrace(); throw e; }}}Copy the code

Provides authentication providers

public class AgentAuthenticationProvider implements AuthenticationProvider { private static final int DEFAULT_TIMEOUT = 30; // 30S private AgentDetailService agentDetailService; public AgentAuthenticationProvider(AgentDetailService agentDetailService){ this.agentDetailService = agentDetailService;  } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { AgentAuthenticationToken token = (AgentAuthenticationToken) authentication; String agentId = token.getAgentId(); If (stringutils. isEmpty(agentId)){throw new AgentIdNotFoundException("agentId cannot be empty "); } PrinterAgent printerAgent = agentDetailService.loadPrinterAgent(agentId); If (printerAgent == null) {throw new AgentIdNotFoundException("agentId does not exist "); } / / check the signature try {PublicKey PublicKey = loadPublicKey (printerAgent. GetAgentPubKey ()); byte[] signatureBytes = Base64.decodeBase64(token.getSignature()); boolean verifySuccess = SignUtil.verifySign(publicKey, token.getStringToSign(), signatureBytes); if (! VerifySuccess) {throw new AgentInvalidSignException (" agent signature is not correct "); } return new AgentAuthenticationToken( printerAgent, token, Collections.singletonList(new SimpleGrantedAuthority("AGENT")) ); } catch (Exception e) { e.printStackTrace(); } long timestamp = token.getTimestamp(); If (timestamp + DEFAULT_TIMEOUT * 1000 > System. CurrentTimeMillis ()) {throw new InvalidTimeStampException (" timestamp illegal "); } return null; } @Override public boolean supports(Class<? > authentication) { return AgentAuthenticationToken.class.isAssignableFrom(authentication); }}Copy the code

Signature Toolbar

Public class SignUtil {/** * Load the public key according to the public key string ** @param publicKeyStr Public key string * @return * @throws Exception */ public static RSAPublicKey loadPublicKey(String publicKeyStr) throws Exception { try { byte[] buffer = javax.xml.bind.DatatypeConverter.parseBase64Binary(publicKeyStr); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer); return (RSAPublicKey) keyFactory.generatePublic(keySpec); } catch (NoSuchAlgorithmException e) {throw new Exception(" no algorithmexception ", e); } catch (InvalidKeySpecException e) {throw new Exception(" public key illegal ", e); } catch (NullPointerException e) {throw new Exception(" public key data null ", e); }} /** * Loads the private key based on the private key string ** @param privateKeyStr Private key string * @return * @throws Exception */ public static RSAPrivateKey loadPrivateKey(String privateKeyStr) throws Exception { try { byte[] buffer = Base64.getDecoder().decode(privateKeyStr);  PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return (RSAPrivateKey) keyFactory.generatePrivate(keySpec); } catch (NoSuchAlgorithmException e) {throw new Exception(" no algorithmexception ", e); } catch (InvalidKeySpecException e) {throw new Exception(" private key illegal ", e); } catch (NullPointerException e) {throw new Exception(" private key data is null ", e); } /** * Generate a key pair without encryption * @param Algorithm * @param keySize * @return * @throws NoSuchAlgorithmException */ public static Map<String, Object> createKey(String algorithm, int keySize) throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm); keyPairGenerator.initialize(keySize); KeyPair keyPair = keyPairGenerator.generateKeyPair(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateCrtKey = (RSAPrivateKey) keyPair.getPrivate(); System.out.println(Base64.getEncoder().encodeToString(publicKey.getEncoded()).length()); System.out.println(Base64.getEncoder().encodeToString(privateCrtKey.getEncoded()).length()); Map<String, Object> keyMap = new HashMap<>(); keyMap.put("publicKey", publicKey); keyMap.put("privateKey", privateCrtKey); return keyMap; } public static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; public static final String ENCODE_ALGORITHM = "SHA-256"; public static byte[] sign(PrivateKey privateKey, String plainText){ MessageDigest messageDigest; byte[] signed; try{ messageDigest = MessageDigest.getInstance(ENCODE_ALGORITHM); messageDigest.update(plainText.getBytes()); byte[] outputDigest = messageDigest.digest(); Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); signature.initSign(privateKey); signature.update(outputDigest); signed = signature.sign(); }catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { e.printStackTrace(); return null; } return signed; } public static boolean verifySign(PublicKey publicKey, String plainText, byte[] signed){ MessageDigest messageDigest; boolean result = false; try{ messageDigest = MessageDigest.getInstance(ENCODE_ALGORITHM); messageDigest.update(plainText.getBytes()); byte[] outputToVerify = messageDigest.digest(); Signature verifySign = Signature.getInstance(SIGNATURE_ALGORITHM); verifySign.initVerify(publicKey); verifySign.update(outputToVerify); result = verifySign.verify(signed); } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { e.printStackTrace(); } return result; }}Copy the code

Calling client

void testAgentSign() throws Exception { String agentPrvKey = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDIp0gcsntXwSs9drdU4Q69ir+9EzjN8sxR9X2RDhRIFe/OLsEBheCbZ5REQsFNwqhwBNc Rnx/3hMsV+jspYc6nneBzExnW/dHxvXnKKOEf2jo75xzntBFKjLitXPqGZei2kX4CrTZTUG5jLGV/nG47s4maTFaMce9Edb2Ndi2W7u3Iua4N5RRFvTPZcV3 8ceRDwbhzkv0iEW+FJgulOGTE4WSodInlnjkgfdn3spIXCN/ztchlU9hGtL1KSg+jsb5jsLwg1Bo2oIAG/mTOKS8vFj69r49MO61SgQRduWRy8z2koCa4NjV gaREUKODqDFFaxFd7lqOE4z1lNvuXqknpAgMBAAECggEAZPMkaKuzOndJFedAXUfNbrb7uFiPX64j9agYwH3g3lOLDqSfHfEJC6aVBbLAJislKxaETa1NG+6 HbksysKMwoUvgvISDn5KbqY+2Ums2uBvG6JRiCoChomwiDbaVpEBpUFDqoNYcWtZAc0zG0+kT3J17QVHeyVIZGsxRzXYDgcdWHANKCukESqdCXpDX3wGzHkR q2Xz2U5ZuxWbCfpFd85HNdEZVl6jzDiYDGEVwOLjFPi/rcSkGNX5BOHOFwcg8c/0cfu+IPyhQWfAGPLz2UWuXcPVEk6Xt0eCM+k5/AW9h1IDWc9xu6XWCWU7 RSikZ0iz+sT4iOnMSZIeVXDOdMQKBgQD8o/O0mI0CDmurI6pwVINY3nYjDSGcAjTM0Epa9jbrGIZj/vPLPevuMMje/c7uQJ7HPOBuxWGwYDGsndwEodFW+i6 JKCbpO+Z83E1CmL9SBg/zafG37qu/37BaD4gNEszdO7JV0JP6bzQM8fnzXFKiFh+8AqW/BS7NkO0vuCgDiwKBgQDLUlqIlNfAUn8eBCuQZN5JDxXotdsw26/ FfcB0TJaa9W8Gs7jhdD807JfO+3Aa/DPjLjy5nM46a/V07zoZYF3TDYGyzNr5pYiqywjWFTbIdUD8EPbZcGzaaYq0GN9fY4Q8SrifxIxgOgAowi2aeC879dy ImomyBvo+4xZCO04G2wKBgQDzPP8Ur5ODmVK8cRhWEmhrlbP0R15GkDE5yIjuTwPNEc3CVONwmOugZsPfPkqPRRQaC1iiDdPiNptc8Je2tf2RWkqXr1rXT96 39HtGVT5OwJt25lfdmSMvFzT5YN7Ch4lKr4Eh8jGm+o4IsKjQT+EXQWnIYFwoL9tB+/kA6rNLxQKBgQCeA6nSngKzOCoMtOb6eDn9A5leWv83kHShgqKwf9l IItifl8tmhEafJgSxWt38Suc0dvnAsynfY4nG0CkSEb+5R7T1tZm1DT4Spmp+nswNrHrNq4183Y/riry+TNpEsv3RMa0clc8W9dyr0IVKmH71FZXIIHpE/oE 7oJbq8FYqowKBgGbFqMY+hMJ6xUrc61cteMOk08g4p42hWiqLp5eKmGi8ZauGFZDrwtZElJ0Xvvw/1O6d2gBU/Udd5NbF+KaawoQkObXE1lut9PHHnWSlnPl 4ZCnaFB5vbiqONzUh/C15MNZ+2p8LvNZ01n39KpzMjZ7xa5fOaRoN3he+JawkYnV4"; String agentId = "111"; HttpHeaders header = new HttpHeaders(); header.add("Accept", "application/json"); String timestamp = String.valueOf(System.currentTimeMillis()); Map<String, String> params = new TreeMap<>(); params.put("agentId", agentId); params.put("timestamp", timestamp); params.put("merchantId", "222"); StringBuilder sb = new StringBuilder(); for (Map.Entry<String,String> entry : params.entrySet()){ sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } if (sb.length() > 0){ sb.deleteCharAt(sb.length()-1); } System.out.println("stringToSign = " + sb.toString()); byte[] signatureBytes = SignUtil.sign(SignUtil.loadPrivateKey(agentPrvKey), sb.toString()); String signature = Base64.getUrlEncoder().encodeToString(signatureBytes); System.out.println("signature = " + signature); RestTemplate restTemplate = new RestTemplate(); ResponseEntity<Result> resultResponseEntity = restTemplate GetForEntity (" http://127.0.0.1:8006/printer-platform/agent/test? agentId="+agentId+"&signature=" +signature+"&timestamp="+timestamp+"&merchantId=222", Result.class, params); System.out.println(resultResponseEntity); Result result = resultResponseEntity.getBody(); System.out.println(result); }Copy the code