2000年,Roy Thomas Fielding在他的博士论文中提出的概念。 Fielding将他对互联网软件的架构原则,定名为REST,即Representational State Transfer的缩写。这个词组的翻译是"表现层状态转化"。
REST API一般用来将某种资源和允许的对资源的操作暴露给外界,使调用者能够以正确的方式操作资源。Rest API需要清晰定义哪些操作能够公开访问,哪些操作需要授权访问。一般而言,如果对RestAPI的安全性要求比较高,那么,所有的API的所有操作均需得到授权。也就是我们要思考如何设计用户认证的体系(Authentication)。
在HTTP协议之上处理授权有很多方法,如HTTP BASIC Auth,OAuth,HMAC Auth等,其核心思想都是验证某个请求是由一个合法的请求者发起。
basic auth是一种基于用户名密码的认证,每次访问都需带上用户的用户名密码,用户的密码暴露在网络之中,这种方案存在安全问题。 OAuth是一种比较通用的,安全的认证方式,不需要用户名密码,只需要用户授权;OAuth的核心部分与HMAC Auth差不多,只不过多了很多与token分发相关的内容。
这里我们主要介绍下HMAC Auth。
HMAC主要在请求头中使用两个字段:Authorization和Date(或X-Auth-Timestamp)。Authorization字段的内容由":"分隔成两部分,":"前是access-key,":"后是HTTP请求的HMAC值。在API授权的时候一般会为调用者生成access-key和access-secret,前者可以暴露在网络中,后者必须安全保存。当客户端调用API时,用自己的access-secret按照要求对request的headers/body计算HMAC,然后把自己的access-key和HMAC填入Authorization头中。服务器拿到这个头,从数据库(或者缓存)中取出access-key对应的secret,按照相同的方式计算HMAC,如果其与Authorization header中的一致,则请求是合法的,且未被修改过的;否则不合法。
GET /photos/puppy.jpg HTTP/1.1
Host: johnsmith.s3.amazonaws.com
Date: Mon, 26 Mar 2007 19:37:58 +0000
Authorization: AWS AKIAIOSFODNN7EXAMPLE:frJIUN8DYpKDtOLCwo//yllqDzg=
可能的实现:
1、网站为调用者生成一个access-key和access-secret, 其中access-key用表示访问身份,access-secret,用于加密签名字符串和服务端验证签名字符串的密钥。
2、调用者发起请求,首先使用请求参数构造规范化的请求字符串。
3、将上一步构造的规范化字符串按照一定的规则构造成待签名的字符串。
4、计算待签名字符串StringToSign的HMAC值。
6、按照某种编码规则(比如Base64)把上面的HMAC值编码成字符串,即可得到签名值(signature)
7、将得到的签名值最为signature参数添加到请求参数中,发送给服务端。
8、服务端请求参数中获取signature,从数据库(或者缓存)中取出access-key对应的secret,按照相同的方式生成signature,如果其与参数中的signature一致,则请求是合法的,且未被修改过的;否则不合法。
流程大致如下图:
java代码示例:
SignatureUtil.isSignatureValid
import org.apache.commons.lang.StringUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class SignatureUtil {
/**
* 校验签名是否有效
*
* @param paramMap
* @param key
* @param signature
* @return
*/
public static boolean isSignatureValid(Map<String, String> paramMap, String key,
String signature) throws SignatureException {
String mySignature = null;
try {
mySignature = createSignature(paramMap, key);
} catch (SignatureException e) {
e.printStackTrace();// should not happen
throw e;
}
if (mySignature.equals(signature)) {
return true;
}
return false;
}
/**
* 创建签名字符串
*
* @param paramMap
* @param key
* @return
* @throws SignatureException
*/
public static String createSignature(Map<String, String> paramMap, String key)
throws SignatureException {
String data = getParamString(paramMap);
try {
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(data.getBytes("UTF-8"));
return DatatypeConverter.printBase64Binary(rawHmac);
// Base64 only valid in Java8
//return new String(Base64.getEncoder().encode(rawHmac));
} catch (NoSuchAlgorithmException e) {
throw new SignatureException("no such algorithm.", e);
} catch (InvalidKeyException e) {
throw new SignatureException("invalid key.", e);
} catch (UnsupportedEncodingException e) {
throw new SignatureException("unsupported encoding.", e);
}
}
public static String getParamString(Map<String, String> paramMap) {
if (paramMap == null || paramMap.isEmpty()) {
return null;
}
StringBuilder buffer = new StringBuilder();
List<String> keys = new ArrayList<String>(paramMap.keySet());
Collections.sort(keys);
for (String key : keys) {
String value = paramMap.get(key);
if (StringUtils.isNotBlank(value)) {
try {
value = URLEncoder.encode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
value = null;
}
}
buffer.append("&").append(key).append("=").append(value);
}
return buffer.substring(1);
}
}
python代码示例:
#!/usr/bin/env python
# coding=utf-8
import hashlib
import hmac
import urllib
import base64
import datetime
class SignatureUtil(object):
@staticmethod
def get_time_stamp():
return datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
@staticmethod
def get_param_string(params):
if not isinstance(params, dict):
return ""
params = SignatureUtil.convert_param_value_to_string(params)
param_list = sorted(params.keys(), reverse=False)
return "&".join(["%s=%s" % (param, urllib.quote_plus(params[param]).replace("%2A", "*")) for param in param_list])
@staticmethod
def convert_param_value_to_string(params):
for key in params:
value = params[key]
# print value
if isinstance(key, unicode):
params.pop(key, "")
key = key.encode('utf-8')
params[key] = value
if isinstance(value, unicode):
params[key] = value.encode('utf-8')
elif not isinstance(value, str):
value = str(value)
params[key] = str(value)
return params
@staticmethod
def create_signature(params, key):
data = SignatureUtil.get_param_string(params)
hashed = hmac.new(str(key), data, hashlib.sha1)
return base64.b64encode(hashed.digest())
注:以上签名计算需要满足:
对所有参数(signature除外)按照参数名的字母顺序排序,并按&连接,所有参数在计算signature之前都需要进行utf-8编码。
HMAC Auth会保证一致性:请求的数据在传输过程中未被修改,因此可以安全地用于验证请求的合法性。