在一些商业合作的场景下,合作方有自己的软件系统并且具备开发能力,需要访问我们的数据资源(比如:账号、产品、统计等),一般的技术方案是提供HTTP API给合作方调用。此时为了保证数据的安全性以及对数据访问范围的控制,就必须验证API调用方的身份,然后结合调用方的权限返回对应的资源,对于无法识别身份的调用方,服务端会进行拦截。
这是一种用于给消息签名的技术,我们怕消息在传递的过程中被人修改,所以,我们需要用对消息进行一个MAC算法,得到一个摘要字串,然后,接收方得到消息后,进行同样的计算,然后比较这个MAC字符串,如果一致,则表明没有被修改过(整个过程参看下图)。而HMAC – Hash-based Authenticsation Code,指的是利用Hash技术完成这一工作,比如:SHA-256算法。
以SHA-256算法示例,签名流程:
OAuth 是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版。OAuth 2.0依赖于TLS/SSL的链路加密技术(HTTPS),完全放弃了签名的方式,认证服务器再也不返回什么token secret的密钥了。
Authorization Code 是最常使用的OAuth 2.0的授权许可类型,它适用于用户给第三方应用授权访问自己信息的场景。其流程图如下:
授权流程:
客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。
授权流程:
client_id
和 client_secret
向 Authorization Server 请求 Access Token。微信支付采用 App Secret Key + HMAC 签名,首先介绍一下微信支付的大致原理:
微信是支付系统的开发方,掌管整个支付系统,负责记账。
商家想要接入微信支付收银,需要向微信支付部门申请商户号。
普通用户通过微信点击商家的付款链接,进行付款。
微信后台记录一笔用户和商家之间的交易流水,然后通知商家系统支付成功。
好了,现在可以知道,交易过程其实就是商家系统和微信后台的接口互相调用,而且只需要单向的关注商家调用微信后台。
JSAPI支付-开发文档,签名算法:
假设传递的参数如下:
appid: wxd930ea5d5a258f4f
mch_id: 10000100
device_info: 1000
body: test
nonce_str: ibuaiVcKdpRxkhJA
第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。
stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。
stringSignTemp=stringA+"&key=192006250b4c09247ec02edce69f6a2d" //注:key为商户平台设置的密钥key
sign=MD5(stringSignTemp).toUpperCase()="9A0A8659F005D6984697E2CA0A9CF3B7" //注:MD5签名方式
sign=hash_hmac("sha256",stringSignTemp,key).toUpperCase()="6A9AE1657590FD6257D693A078E1C3E4BB6BA4DC30B23E0EE2496E54170DACD6" //注:HMAC-SHA256签名方式,部分语言的hmac方法生成结果二进制结果,需要调对应函数转化为十六进制字符串。
最终发送的数据:
<xml>
<appid>wxd930ea5d5a258f4f</appid>
<mch_id>10000100</mch_id>
<device_info>1000</device_info>
<body>test</body>
<nonce_str>ibuaiVcKdpRxkhJA</nonce_str>
<sign>9A0A8659F005D6984697E2CA0A9CF3B7</sign>
</xml>
access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。
接口调用请求说明
GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
参数说明:
返回情况
正常情况下,微信会返回下述JSON数据包给公众号:
{"access_token":"ACCESS_TOKEN","expires_in":7200}
参数说明:
如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。
网页授权AccessToken的流程
前面介绍几种常用的API鉴权技术,在产品调研环节分别可以找到其落地场景。
首先拿 微信支付 来看,一笔交易的下单在一个接口中完成,请求的参数包含金额、商户号等,都是非常关键的参数,必须要求严格校验,防止被攻击篡改,同时参数还有时效限制(含时间戳)。此时自然不适合使用OAuth 2.0的鉴权方式,AccessToken的请求不对参数进行校验。
然后看下 微信公众号 AccessToken的场景,可以看到使用AccessToken调用接口(管理公众号菜单、管理账号)都属于一个企业范围内的数据,可以这么理解,这部分信息属于微信授权给企业的一份独立资产,公众号对应的企业有权限管理这份资产。此时使用AccessToken可以很好的控制访问范围。这里不是不能用 App Secret Key + HMAC 的鉴权方式,而是觉得这部分信息安全要求没有支付高。另一方面,不对参数加密,通信也会更加高效(加密有耗时,比如文件上传也不太适合进行加密)。
最后看下 微信网页授权,同理类推,用户的信息属于每个独立的用户,获取的AccessToken的访问范围也只能是当前用户的信息。
上面提到的两种鉴权方式,无论是作为服务方还是调用方,我都在工作中都有使用到。个人觉得 App Secret Key + HMAC 实践起来相对容易,客户端对服务端的调用比较直接,鉴权不通过时可以通过接口的响应及时获得反馈。
另一种,OAuth 2.0的AccessToken的方式,服务端需要维护AccessToken,并且还要控制AccessToken的失效,拿微信公众号来看,新的AccessToken生成后,旧的AccessToken在5分钟之内有效;客户端需要维护一份AccessToken并及时刷新保持有效。再看下业务的交互上,比起 App Secret Key + HMAC 明显多一些环节,环节多了就容易犯错。
最后,具体选择使用哪一种鉴权方式,我想还是需要结合对应的业务场景来看。比如业务发展的初期,需要快速开发推向市场,这时就没必要纠结,直接选择一种相对而言简单且不容易犯错的 App Secret Key + HMAC 签名鉴权。等到后续用户量大了,业务成熟了,可以参考 微信公众号、AWS s4签名,精细划分每一个AccessToken的访问范围。
实践案例使用 App Secret Key + HMAC 的鉴权方式,下面会详细介绍 客户端签名 和 服务端验签 的过程。
在签名之前首先需要分配 AppId 和 AppSecret,落实到业务场景中,这个就是我们作为资源方分配给合作方的租户配置。关于 AppId 和 AppSecret 的生成没有标准规范,每家的生成算法都不一样,也都不会公布出来。本次案例,我们使用32位的uuid作为AppId,以64位的hash串作为AppSecret:
// 生成AppId
private static String generateAppId() {
UUID uuid = UUID.randomUUID();
return uuid.toString().replaceAll("-", "");
}
// 生成AppSecret
private static String generateAppSecret() {
UUID uuid = UUID.randomUUID();
return DigestUtils.sha256Hex(uuid.toString());
}
计算得出:
APPID = "ivv49q404zfp8075ivbcwye4ardqafha"
APP_SECRET = "ut338c829x2yzfnklvy8lezyu3ndsss68dyzo9opt3icbin7lv7p2j4b0i2cvjz8"
假设传递的参数如下:
private static final String APPID = "ivv49q404zfp8075ivbcwye4ardqafha";
/**
* 下单请求对象
*/
class PlaceOrderForm {
String appid;
Integer totalAmount;
String body;
String detail;
String nonceStr;
}
/**
* 模拟请求对象
*/
private static PlaceOrderForm mockWebForm () {
PlaceOrderForm form = new PlaceOrderForm();
form.appid = APPID;
form.body = "test";
form.detail = "test";
form.nonceStr = "123456";
form.totalAmount = 88;
return form;
}
第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。
/**
* TreeMap会根据Key排序
*/
private static Map<String, String> confirmToMap(PlaceOrderForm form) throws Exception {
Map<String, String> map = new TreeMap<>();
Field[] fields = PlaceOrderForm.class.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
Object value = field.get(form);
if (value != null && !field.getName().equals("sign")) {
if (value instanceof String) {
map.put(field.getName(), (String) value);
} else if (value instanceof Integer) {
map.put(field.getName(), String.valueOf(value));
}
}
}
return map;
}
第二步,在stringA最后拼接上appsecret得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。
private static final String APP_SECRET = "ut338c829x2yzfnklvy8lezyu3ndsss68dyzo9opt3icbin7lv7p2j4b0i2cvjz8";
/**
* 生成签名
*/
private static String sign(Map<String, String> params) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
sb.append(entry.getKey());
sb.append("=");
sb.append(entry.getValue());
sb.append("&");
}
sb.append("appsecret=");
sb.append(APP_SECRET);
return DigestUtils.md5Hex(sb.toString()).toUpperCase();
}
最后计算得到摘要
public static void main(String[] args) {
PlaceOrderForm form = mockWebForm();
try {
Map<String, String> stringStringMap = confirmToMap(form);
String sign = sign(stringStringMap);
System.out.println(sign);
} catch (Exception e) {
e.printStackTrace();
}
}
服务端接收请求的参数,使用同样的签名算法计算出摘要 sign 进行比较,如果一致,则说明请求没有被修改。
原文:https://www.cnblogs.com/maxzuo/p/14017929.html