签名验签说明
同步数据至企业应用时,需要企业应用对该同步事件进行识别并确认,确保事件来源的安全性和可靠性,从而保证数据在一个安全的环境中进行交互。
签名校验/加解密术语
| 术语 | 说明 |
|---|---|
| signature | 消息签名,用于验证请求是否来自IDaaS,以防攻击者伪造。签名算法为HMAC-SHA256 + Base64。 |
| AESKey | AES算法的密钥,加密算法支持AES/GCM/NoPadding和AES/ECB/PKCS5Padding,推荐使用AES/GCM/NoPadding |
| msg | 明文消息体,格式为JSON。 |
| encrypt_msg | 明文消息msg加密处理并进行Base64编码后的密文。 |
签名校验
为了让企业应用确认事件推送来自IDaaS,IDaaS将事件推送给企业应用回调服务时,请求包体中包含请求签名并以参数signature标识,企业应用需要验证此参数的正确性后再解密,验证步骤如下:
计算签名:由签名密钥、Nonce值、时间戳、事件类型、加密消息体5部分组成,中间使用&进行连接。采用HMAC-SHA256 + Base64算法进行加密。以下为Java语言签名示例:
javaString message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData; Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKey = new SecretKeySpec(签名秘钥.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); mac.init(secretKey); String newSignature = Base64.getEncoder().encodeToString(mac.doFinal(message.getBytes(StandardCharsets.UTF_8)));比较计算的签名cal_signature与请求参数signature是否相等,相等则表示验证通过。
企业应用按照要求返回响应消息格式。
明文加密过程
拼接明文字符串。明文字符串由16个字节的随机字符串、明文msg拼接组成,中间使用&进行连接。以下为Java语言示例:
javaString dataStr = RandomStringUtils.random(16, true, false) + "&" + data;对拼接后的明文字符串使用AESkey加密后,再进行Base64编码,获得密文encrypt_msg。以下为Java语言示例,分别演示两种不同的加密算法:
AES/ECB/PKCS5Padding:
javaCipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(StandardCharsets.UTF_8), "AES"); cipher.init(1, secretKey); byte[] bytes = dataStr.getBytes(StandardCharsets.UTF_8); String ecnryptStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));AES/GCM/NoPadding:
javaCipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(StandardCharsets.UTF_8), "AES"); cipher.init(1, secretKey); byte[] bytes = dataStr.getBytes(StandardCharsets.UTF_8); //生成长度24的字符串 String repIvStr = generate(24); byte[] iv = Base64.getDecoder().decode(repIvStr); cipher.init(1, secretKey , new GCMParameterSpec(128, iv)); String ecnryptStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
密文解密过程
对密文进行BASE64解码。
javabyte[] encryptStr = Base64.getDecoder().decode(data);使用AESKey进行解密。
AES/ECB/PKCS5Padding:
javaCipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(StandardCharsets.UTF_8), "AES"); cipher.init(2, secretKey); byte[] bytes = cipher.doFinal(encryptStr);AES/GCM/NoPadding:
javaCipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM); byte[] iv = Base64.getDecoder().decode(data.substring(0, 24)); data = data.substring(24); cipher.init(2, secretKey, new GCMParameterSpec(AES_KEY_SIZE, iv)); byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(data));
获取明文内容。
AES/ECB/PKCS5Padding: 需去掉rand_msg头部的16个随机字节,剩余的部分即为明文内容msg
javaString dataStr = new String(bytes, CHARSET).substring(17);AES/GCM/NoPadding:
javaString dataStr = new String(bytes, CHARSET);
数据签名&加密解密示例
以下为数据签名&加密解密的Java语言示例,分别演示两种算法:
- AES/ECB/PKCS5Padding:
javapackage BOOT-INF.classes.com.example.demo.controller; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.HttpServletRequest; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.UUID; @RequestMapping @Controller public class DemoController { private static final Logger log = LoggerFactory.getLogger(DemoController.class); private static final String SEPARATOR = "&"; private static final String SIGN_KEY = "wGt9VxV2qqLbgRrs"; private static final String ENCRYPTION_KEY = "ZJIXSHUdo8WK7FQo"; private static final String TOKEN = "4JVImwu3GdM3zNCE4JVImwu3GdM3zNCE"; private static final String HMAC_SHA256 = "HmacSHA256"; private static final String ENCRYPT_ALGORITHM = "AES/ECB/PKCS5Padding"; private static final String ENCRYPT_KEY_ALGORITHM = "AES"; private static final Charset CHARSET = StandardCharsets.UTF_8; @ResponseBody @RequestMapping({"/callback"}) public JSONObject demo(HttpServletRequest httpRequest, @RequestBody String body) { try { // 验证授权信息 // 获取 Authorization 头部信息 String authorization = httpRequest.getHeader("Authorization"); assertAuthorizationMessage(authorization); //注:若需判断接收的事件回调消息是否重复,可通过解密后的数据的前16位字符串进行验证。(即&前的字符串用于校验消息是否重复,&后的字符串为对象属性数据) JSONObject messageBody = JSON.parseObject(body); //校验消息体来源是否合法 assertMessageSourceValidity(messageBody); //解析加密消息体 String encryptMessage = messageBody.getString("data"); String plaintMessage = parseEncryptedMessage(encryptMessage); // 处理数据 JSONObject eventData = JSON.parseObject(plaintMessage); String eventType = messageBody.getString("eventType"); JSONObject processResultData = processEventData(eventData, eventType); String processResultDataStr = RandomStringUtils.random(16, true, false)+ "&" + processResultData.toJSONString(); ; String encryptResultData =encryptResultData(processResultDataStr); JSONObject result = new JSONObject(); result.put("code", "200"); result.put("message", "success"); result.put("data", encryptResultData); return result; } catch (Exception e) { JSONObject result = new JSONObject(); result.put("code", "400"); String message = e.getMessage(); result.put("message", StringUtils.isNotEmpty(message) ? message : e); return result; } } private String encryptResultData(String dataStr) { try { Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM); SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM); cipher.init(1, secretKey); byte[] bytes = dataStr.getBytes(CHARSET); return Base64.getEncoder().encodeToString(cipher.doFinal(bytes)); } catch (Exception e) { throw new RuntimeException("error encrypt Result Data", e); } } private void assertAuthorizationMessage(String authorization) { if (!"Bearer ".concat(TOKEN).equals(authorization)) { throw new RuntimeException("error authorization."); } } private void assertMessageSourceValidity(JSONObject messageBody) { try { String nonce = messageBody.getString("nonce"); String timestamp = messageBody.getString("timestamp"); String eventType = messageBody.getString("eventType"); String encryptData = messageBody.getString("data"); String signature = messageBody.getString("signature"); String message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData; Mac mac = Mac.getInstance(HMAC_SHA256); SecretKeySpec secretKey = new SecretKeySpec(SIGN_KEY.getBytes(StandardCharsets.UTF_8), HMAC_SHA256); mac.init(secretKey); byte[] bytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8)); String cal_signature = Base64.getEncoder().encodeToString(bytes); if (!cal_signature.equals(signature)) { throw new RuntimeException("signature validation failed."); } } catch (Exception e) { throw new RuntimeException("error validating message signature", e); } } private String parseEncryptedMessage(String encryptMessage) { try { Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM); SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM); cipher.init(2, secretKey); byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(encryptMessage)); return new String(bytes, CHARSET).substring(17); } catch (Exception e) { throw new RuntimeException("error parsing encrypted message", e); } } private JSONObject processEventData(JSONObject eventData, String eventType) { if (eventType.equals("CREATE_USER")) { return processCreateUser(eventData); } else if (eventType.equals("CREATE_ORGANIZATION")) { return processCreateOrganization(eventData); } else if (eventType.equals("UPDATE_USER")) { return processUpdateUser(eventData); } else if (eventType.equals("UPDATE_ORGANIZATION")) { return processUpdateOrganization(eventData); } else if (eventType.equals("DELETE_ORGANIZATION")) { // No data to return processDeleteUser(eventData); return new JSONObject(); } else if (eventType.equals("DELETE_USER")) { // No data to return processDeleteOrganization(eventData); return new JSONObject(); } else if (eventType.equals("CHECK_URL")) { return processCheckUrl(); } else { throw new RuntimeException("the event type is not supported"); } } private JSONObject processCreateUser(JSONObject eventData) { //TODO 业务方处理逻辑 JSONObject returnData = new JSONObject(1); returnData.put("id", eventData.getString("username")); return returnData; } private JSONObject processCreateOrganization(JSONObject eventData) { //TODO 业务方处理逻辑 JSONObject returnData = new JSONObject(1); returnData.put("id", eventData.getString("code")); return returnData; } private JSONObject processUpdateUser(JSONObject eventData) { //TODO 业务方处理逻辑 JSONObject returnData = new JSONObject(1); returnData.put("id", eventData.getString("id")); return returnData; } private JSONObject processUpdateOrganization(JSONObject eventData) { //TODO 业务方处理逻辑 JSONObject returnData = new JSONObject(1); returnData.put("id", eventData.getString("id")); return returnData; } private void processDeleteUser(JSONObject eventData) { //TODO 业务方处理逻辑 } private void processDeleteOrganization(JSONObject eventData) { //TODO 业务方处理逻辑 } private JSONObject processCheckUrl() { //TODO 业务方处理逻辑 JSONObject returnData = new JSONObject(1); String randomStr = UUID.randomUUID().toString().replaceAll("-", ""); returnData.put("randomStr", randomStr); return returnData; } }- AES/GCM/NoPadding:
javaimport com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.HttpServletRequest; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.UUID; @RequestMapping @Controller public class DemoController { private static final Logger log = LoggerFactory.getLogger(com.example.demo.controller.DemoController.class); private static final String SEPARATOR = "&"; private static final String SIGN_KEY = "wGt9VxV2qqLbgRrs"; private static final String ENCRYPTION_KEY = "ZJIXSHUdo8WK7FQo"; private static final String TOKEN = "4JVImwu3GdM3zNCE"; private static final String HMAC_SHA256 = "HmacSHA256"; private static final String ENCRYPT_ALGORITHM = "AES/GCM/NoPadding"; private static final String ENCRYPT_KEY_ALGORITHM = "AES"; private static final Charset CHARSET = StandardCharsets.UTF_8; @ResponseBody @RequestMapping({"/callback"}) public JSONObject demo(HttpServletRequest httpRequest, @RequestBody String body) { try { // 验证授权信息 // 获取 Authorization 头部信息 String authorization = httpRequest.getHeader("Authorization"); assertAuthorizationMessage(authorization); //注:若需判断接收的事件回调消息是否重复,可通过解密后的数据的前16位字符串进行验证。(即&前的字符串用于校验消息是否重复,&后的字符串为对象属性数据) JSONObject messageBody = JSON.parseObject(body); //校验消息体来源是否合法 assertMessageSourceValidity(messageBody); //解析加密消息体 String encryptMessage = messageBody.getString("data"); String plaintMessage = parseEncryptedMessage(encryptMessage); // 处理数据 JSONObject eventData = JSON.parseObject(plaintMessage); String eventType = messageBody.getString("eventType"); JSONObject processResultData = processEventData(eventData, eventType); String encryptResultData = encryptResultData(processResultData); JSONObject result = new JSONObject(); result.put("code", "200"); result.put("message", "success"); result.put("data", encryptResultData); return result; } catch (Exception e) { JSONObject result = new JSONObject(); result.put("code", "400"); String message = e.getMessage(); result.put("message", StringUtils.isNotEmpty(message) ? message : e); return result; } } private String encryptResultData(JSONObject processResultData) { try { String dataStr = processResultData.toJSONString(); Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM); SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM); String ivRandom = RandomStringUtils.random(24, true, true); byte[] iv = Base64.getDecoder().decode(ivRandom); cipher.init(1, secretKey, new GCMParameterSpec(128, iv)); byte[] bytes = dataStr.getBytes(CHARSET); dataStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes)); return ivRandom + dataStr; } catch (Exception e) { throw new RuntimeException("error encrypt Result Data", e); } } private void assertAuthorizationMessage(String authorization) { if (!"Bearer ".concat(TOKEN).equals(authorization)) { throw new RuntimeException("error authorization."); } } private void assertMessageSourceValidity(JSONObject messageBody) { try { String nonce = messageBody.getString("nonce"); String timestamp = messageBody.getString("timestamp"); String eventType = messageBody.getString("eventType"); String encryptData = messageBody.getString("data"); String signature = messageBody.getString("signature"); String message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData; Mac mac = Mac.getInstance(HMAC_SHA256); SecretKeySpec secretKey = new SecretKeySpec(SIGN_KEY.getBytes(StandardCharsets.UTF_8), HMAC_SHA256); mac.init(secretKey); byte[] bytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8)); String cal_signature = Base64.getEncoder().encodeToString(bytes); if (!cal_signature.equals(signature)) { throw new RuntimeException("signature validation failed."); } } catch (Exception e) { throw new RuntimeException("error validating message signature", e); } } private String parseEncryptedMessage(String encryptMessage) { try { Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM); SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM); byte[] iv = Base64.getDecoder().decode(encryptMessage.substring(0, 24)); String encryptMsg = encryptMessage.substring(24); cipher.init(2, secretKey, new GCMParameterSpec(128, iv)); byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(encryptMsg)); return new String(bytes, CHARSET); } catch (Exception e) { throw new RuntimeException("error parsing encrypted message", e); } } private JSONObject processEventData(JSONObject eventData, String eventType) { if (eventType.equals("CREATE_USER")) { return processCreateUser(eventData); } else if (eventType.equals("CREATE_ORGANIZATION")) { return processCreateOrganization(eventData); } else if (eventType.equals("UPDATE_USER")) { return processUpdateUser(eventData); } else if (eventType.equals("UPDATE_ORGANIZATION")) { return processUpdateOrganization(eventData); } else if (eventType.equals("DELETE_ORGANIZATION")) { // No data to return processDeleteUser(eventData); return new JSONObject(); } else if (eventType.equals("DELETE_USER")) { // No data to return processDeleteOrganization(eventData); return new JSONObject(); } else if (eventType.equals("CHECK_URL")) { return processCheckUrl(); } else { throw new RuntimeException("the event type is not supported"); } } private JSONObject processCreateUser(JSONObject eventData) { //TODO 业务方处理逻辑 JSONObject returnData = new JSONObject(1); returnData.put("id", eventData.getString("username")); return returnData; } private JSONObject processCreateOrganization(JSONObject eventData) { //TODO 业务方处理逻辑 JSONObject returnData = new JSONObject(1); returnData.put("id", eventData.getString("code")); return returnData; } private JSONObject processUpdateUser(JSONObject eventData) { //TODO 业务方处理逻辑 JSONObject returnData = new JSONObject(1); returnData.put("id", eventData.getString("id")); return returnData; } private JSONObject processUpdateOrganization(JSONObject eventData) { //TODO 业务方处理逻辑 JSONObject returnData = new JSONObject(1); returnData.put("id", eventData.getString("id")); return returnData; } private void processDeleteUser(JSONObject eventData) { //TODO 业务方处理逻辑 } private void processDeleteOrganization(JSONObject eventData) { //TODO 业务方处理逻辑 } private JSONObject processCheckUrl() { //TODO 业务方处理逻辑 JSONObject returnData = new JSONObject(1); String randomStr = UUID.randomUUID().toString().replaceAll("-", ""); returnData.put("randomStr", randomStr); return returnData; } }