签名验签说明
当OneAccess同步数据至企业应用时,需要企业应用对该同步事件进行识别并确认,确保事件来源的安全性和可靠性,从而保证数据在一个安全的环境中进行交互。
签名校验/加解密术语
术语 |
说明 |
---|---|
signature |
消息签名,用于验证请求是否来自OneAccess,以防攻击者伪造。签名算法为HMAC-SHA256 + Base64。 |
AESKey |
AES算法的密钥,加密算法为AES/GCM/NoPadding + Base64。 |
msg |
明文消息体,格式为JSON。 |
encrypt _msg |
明文消息msg加密处理并进行Base64编码后的密文。 |
签名校验
为了让企业应用确认事件推送来自OneAccess,OneAccess将事件推送给企业应用回调服务时,请求body体中包含请求签名并以参数signature标识,企业应用需要验证此参数的正确性后再解密,验证步骤如下:
- 计算签名。
计算签名由签名密钥、Nonce值(nonce)、时间戳(timestamp)、事件类型(eventType)、消息体(data)5部分组成,中间使用&进行连接。采用HMAC-SHA256 + Base64算法进行加密。以下为Java语言签名示例:
String message = nonce + "&" + timestamp + "&" + eventType + "&" + data; 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语言示例:
String dataStr = RandomStringUtils.random(16, true, false) + "&" + data;
- 对拼接后的明文字符串使用AESkey加密后,再进行Base64编码,获得密文encrypt_msg。以下为Java语言示例:
Cipher 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); String ecnryptStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
密文解密过程
- 对密文进行BASE64解码。
byte[] encryptStr = Base64.getDecoder().decode(data);
- 使用AESKey进行解密。
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(StandardCharsets.UTF_8), "AES"); cipher.init(2, secretKey); byte[] bytes = cipher.doFinal(encryptStr);
- 去掉rand_msg头部的16个随机字节,剩余的部分即为明文内容msg。
String dataStr = StringUtils.split(new String(bytes, StandardCharsets.UTF_8), "&")[1];
数据签名&加密解密示例
- 以下为数据签名& AES/GCM/NoPadding算法加密解密的Java语言示例:
package com.example.demo.controller; import com.alibaba.fastjson.JSONObject; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Base64; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.HttpServletRequest; 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.util.Assert; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @RequestMapping @Controller public class GcmController { private static final Logger log = LoggerFactory.getLogger(com.example.demo.controller.GcmController.class); private static final String SEPARATOR = "&"; //region 以下三个参数均由第三方随机生成的32位字符串(英文字母大小写和数字),正式环境不要使用以下示例 /** * 安全令牌 */ private static final String TOKEN = "4JV**********NCE"; /** * 签名密钥 */ private static final String SIGN_KEY = "wGt**********Rrs"; /** * 加密密钥 */ private static final String ENCRYPTION_KEY = "ZJI**********FQo"; //endregion /** * 加密算法 */ 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 int AES_KEY_SIZE = 128; private static final Charset CHARSET = StandardCharsets.UTF_8; @ResponseBody @RequestMapping({"/gcm/callback"}) public JSONObject demo(HttpServletRequest httpRequest, @RequestBody String body) { log.info("接受事件回调信息数据,原始报文:" + body); JSONObject result = new JSONObject(); result.put("code", "200"); result.put("message", "success"); String authorization = httpRequest.getHeader("Authorization"); if (!StringUtils.join((Object[]) new String[]{"Bearer ", TOKEN}).equals(authorization)) { result.put("code", "401"); result.put("message", "不合法的请求!"); printResult(result); return result; } JSONObject request = JSONObject.parseObject(body); String signature = request.getString("signature"); String eventType = request.getString("eventType"); log.info("事件类型" + eventType); String data = request.getString("data"); if (StringUtils.isNotEmpty(SIGN_KEY)) try { log.info("开始校验签名"); String message = request.getString("nonce") + "&" + request.getLong("timestamp") + "&" + eventType + "&" + data; Mac mac = Mac.getInstance(HMAC_SHA256); SecretKeySpec secretKey = new SecretKeySpec(SIGN_KEY.getBytes(CHARSET), HMAC_SHA256); mac.init(secretKey); String newSignature = Base64.getEncoder().encodeToString(mac.doFinal(message.getBytes(CHARSET))); Assert.isTrue(newSignature.equals(signature), "签名不一致"); log.info("签名校验成功"); } catch (Exception e) { log.error("签名校验失败", e); result.put("code", "401"); result.put("message", "签名校验失败"); printResult(result); return result; } if (StringUtils.isNotEmpty(ENCRYPTION_KEY)) { log.info("开始解密数据" + data); try { Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM); SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM); byte[] iv = decodeFromBase64(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)); data = new String(bytes, CHARSET); log.info("解密数据成功,解密后数据:" + data); } catch (Exception e) { log.error("解密数据失败", e); result.put("code", "401"); result.put("message", "解密数据失败"); printResult(result); return result; } } JSONObject eventData = JSONObject.parseObject(data); JSONObject returnData = new JSONObject(1); String dataStr = null; switch (eventType) { case "CREATE_USER": // 根据 username 唯一创建下游数据,如果下游数据已存在做更新处理。并返回下游id returnData.put("id", eventData.getString("username")); break; case "CREATE_ORGANIZATION": // 根据 code 唯一创建下游数据,如果下游数据已存在做更新处理。并返回下游id returnData.put("id", eventData.getString("code")); break; case "UPDATE_USER": case "UPDATE_ORGANIZATION": // 更新数据时,没有修改的字段传值为空 returnData.put("id", eventData.getString("id")); break; case "DELETE_ORGANIZATION": case "DELETE_USER": // 根据 下游业务逻辑进行删除操作。不需要返回字段 break; default: result.put("code", "400"); result.put("message", "不支持的事件类型"); printResult(result); return result; } if (dataStr == null && returnData.size() > 0) dataStr = returnData.toJSONString(); if (StringUtils.isNotEmpty(ENCRYPTION_KEY) && dataStr != null) { String random = RandomStringUtils.random(24, true, true); log.info("开始加密数据" + dataStr); try { Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM); SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM); byte[] iv = decodeFromBase64(random); cipher.init(1, secretKey, new GCMParameterSpec(AES_KEY_SIZE, iv)); byte[] bytes = dataStr.getBytes(CHARSET); dataStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes)); dataStr = random + dataStr; log.info("加密数据成功,加密后数据:" + dataStr); } catch (Exception e) { log.error("加密数据失败", e); result.put("code", "500"); result.put("message", "加密数据失败"); printResult(result); return result; } } result.put("data", dataStr); printResult(result); return result; } private static byte[] decodeFromBase64(String data) { return Base64.getDecoder().decode(data); } private void printResult(JSONObject result) { log.info("" + result.toJSONString()); } }
- 以下为数据签名& AES/ECB/PKCS5Padding算法加密解密的Java语言示例:
package com.example.demo.controller; import com.alibaba.fastjson.JSONObject; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.UUID; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.HttpServletRequest; 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.util.Assert; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @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 = "wGt**********Rrs"; /** 加密密钥 **/ private static final String ENCRYPTION_KEY = "ZJI**********FQo"; /** 安全令牌 **/ private static final String TOKEN = "4JV**********NCE"; /** 签名算法 **/ 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) { JSONObject result = new JSONObject(); result.put("code", "200"); result.put("message", "success"); log.info("接收事件回调消息数据,原始报文: " + body); String authorization = httpRequest.getHeader("Authorization"); if (!StringUtils.join("Bearer ", TOKEN).equals(authorization)) { result.put("code", "401"); result.put("message", "不合法的请求!"); printResult(result); return result; } JSONObject request = JSONObject.parseObject(body); String signature = request.getString("signature"); String eventType = request.getString("eventType"); log.info("事件类型:" + eventType); String data = request.getString("data"); if (StringUtils.isNotEmpty(SIGN_KEY)) { try { log.info("开始校验签名"); String message = request.getString("nonce") + SEPARATOR + request.getLong("timestamp") + SEPARATOR + eventType + SEPARATOR + data; Mac mac = Mac.getInstance(HMAC_SHA256); SecretKeySpec secretKey = new SecretKeySpec(SIGN_KEY.getBytes(CHARSET), HMAC_SHA256); mac.init(secretKey); String newSignature = Base64.getEncoder().encodeToString(mac.doFinal(message.getBytes(CHARSET))); Assert.isTrue(newSignature.equals(signature), "签名不一致"); log.info("签名校验成功"); } catch (Exception e) { log.info("签名校验失败"); log.error("签名校验失败", e); result.put("code", "401"); result.put("message", "签名校验失败"); printResult(result); return result; } } if (StringUtils.isNotEmpty(ENCRYPTION_KEY)) { log.info("开始解密数据: " + data); 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(data)); data = StringUtils.split(new String(bytes, CHARSET), SEPARATOR)[1]; log.info("解密数据成功,解密后数据: " + data); } catch (Exception e) { log.info("解密数据失败"); log.error("解密数据异常", e); result.put("code", "401"); result.put("message", "解密数据失败"); printResult(result); return result; } } JSONObject eventData = JSONObject.parseObject(data); JSONObject returnData = new JSONObject(1); String dataStr = null; switch (eventType) { case "CREATE_USER": returnData.put("id", eventData.getString("username")); break; case "CREATE_ORGANIZATION": returnData.put("id", eventData.getString("code")); break; case "UPDATE_USER": case "UPDATE_ORGANIZATION": returnData.put("id", eventData.getString("id")); break; case "DELETE_ORGANIZATION": case "DELETE_USER": break; case "CHECK_URL": dataStr = UUID.randomUUID().toString().replaceAll("-", ""); break; default: result.put("code", "400"); result.put("message", "不支持的事件类型"); printResult(result); return result; } if (dataStr == null && returnData.size() > 0) { dataStr = returnData.toJSONString(); } if (StringUtils.isNotEmpty(ENCRYPTION_KEY) && dataStr != null) { dataStr = RandomStringUtils.random(16, true, false) + SEPARATOR + dataStr; log.info("开始加密数据: " + 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); dataStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes)); log.info("加密数据成功,加密后数据: " + dataStr); } catch (Exception e) { log.info("加密数据失败"); log.error("加密数据异常:", e); result.put("code", "500"); result.put("message", "加密数据失败"); printResult(result); return result; } } result.put("data", dataStr); printResult(result); return result; } private void printResult(JSONObject result) { log.info("返回数据为: " + result.toJSONString()); } }