聊聊接口安全性问题
总结:我们写过很多接口,有没有想想接口的安全性呢?jwt,openid 侧重 于 认证(就是用户是谁),OAuth2 侧重于授权(就是说这个东西是否有权限访问),接口签名呢 侧重于安全
- 请求来源(身份)是否合法?
- 请求参数被篡改?
- 请求的唯一性(不可复制)
签名介绍:
AccessKey&SecretKey (开放平台)
请求身份
为开发者分配AccessKey(开发者标识,确保唯一)和SecretKey(用于接口加密,确保不易被穷举,生成算法不易被猜测)。
防止篡改
参数签名
- 按照请求参数名的字母升序排列非空请求参数(包含AccessKey),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA;
- 在stringA最后拼接上Secretkey得到字符串stringSignTemp;
- 对stringSignTemp进行MD5运算,并将得到的字符串所有字符转换为大写,得到sign值。
请求携带参数AccessKey和Sign,只有拥有合法的身份AccessKey和正确的签名Sign才能放行。这样就解决了身份验证和参数篡改问题,即使请求参数被劫持,由于获取不到SecretKey(仅作本地加密使用,不参与网络传输),无法伪造合法的请求。
重放攻击
虽然解决了请求参数被篡改的隐患,但是还存在着重复使用请求参数伪造二次请求的隐患。
timestamp+nonce方案
nonce指唯一的随机字符串,用来标识每个被签名的请求。通过为每个请求提供一个唯一的标识符,服务器能够防止请求被多次使用(记录所有用过的nonce以阻止它们被二次使用)。
然而,对服务器来说永久存储所有接收到的nonce的代价是非常大的。可以使用timestamp来优化nonce的存储。
假设允许客户端和服务端最多能存在15分钟的时间差,同时追踪记录在服务端的nonce集合。当有新的请求进入时,首先检查携带的timestamp是否在15分钟内,如超出时间范围,则拒绝,然后查询携带的nonce,如存在已有集合,则拒绝。否则,记录该nonce,并删除集合内时间戳大于15分钟的nonce(可以使用redis的expire,新增nonce的同时设置它的超时失效时间为15分钟)。
实现
请求接口:http://api.test.com/test?name=hello&home=world&work=java
-
客户端
- 生成当前时间戳timestamp=now和唯一随机字符串nonce=random
- 按照请求参数名的字母升序排列非空请求参数(包含AccessKey)
stringA="AccessKey=access&home=world&name=hello&work=java×tamp=now&nonce=random";
- 拼接密钥SecretKey
stringSignTemp="AccessKey=access&home=world&name=hello&work=java×tamp=now&nonce=random&SecretKey=secret";
- MD5并转换为大写
sign=MD5(stringSignTemp).toUpperCase();
- 最终请求
http://api.test.com/test?name=hello&home=world&work=java×tamp=now&nonce=nonce&sign=sign;
-
服务端
Token&AppKey(APP)
在APP开放API接口的设计中,由于大多数接口涉及到用户的个人信息以及产品的敏感数据,所以要对这些接口进行身份验证,为了安全起见让用户暴露的明文密码次数越少越好,然而客户端与服务器的交互在请求之间是无状态的,也就是说,当涉及到用户状态时,每次请求都要带上身份验证信息。
Token身份验证
- 用户登录向服务器提供认证信息(如账号和密码),服务器验证成功后返回Token给客户端;
- 客户端将Token保存在本地,后续发起请求时,携带此Token;
- 服务器检查Token的有效性,有效则放行,无效(Token错误或过期)则拒绝。
安全隐患:Token被劫持,伪造请求和篡改参数。
Token+AppKey签名验证
与上面开发平台的验证方式类似,为客户端分配AppKey(密钥,用于接口加密,不参与传输),将AppKey和所有请求参数组合成源串,根据签名算法生成签名值,发送请求时将签名值一起发送给服务器验证。这样,即使Token被劫持,对方不知道AppKey和签名算法,就无法伪造请求和篡改参数。再结合上述的重发攻击解决方案,即使请求参数被劫持也无法伪造二次重复请求。
实现
登陆和登出请求
后续请求
-
客户端
和上述开放平台的客户端行为类似,把AccessKey改为token即可。 -
服务端
// 添加拦截器 | |
@Override | |
public void addInterceptors(InterceptorRegistry registry) { | |
// 接口签名认证拦截器,该签名认证比较简单,实际项目中可以使用Json Web Token或其他更好的方式替代。 | |
if (!"dev".equals(env)) { // 开发环境忽略签名认证 | |
registry.addInterceptor(new HandlerInterceptorAdapter() { | |
@Override | |
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { | |
// 验证签名 | |
boolean pass = validateSign(request); | |
if (pass) { | |
return true; | |
} else { | |
LOGGER.warn("签名认证失败,请求接口:{},请求IP:{},请求参数:{}", request.getRequestURI(), getIpAddress(request), JSON.toJSONString(request.getParameterMap())); | |
RetResult<Object> result = new RetResult<Object>(); | |
result.setCode(RetCode.UNAUTHORIZED).setMsg("签名认证失败"); | |
responseResult(response, result); | |
return false; | |
} | |
} | |
}); | |
} | |
} | |
/** | |
* @Title: responseResult | |
* @Description: 响应结果 | |
* @param response | |
* @param result | |
* @Reutrn void | |
*/ | |
private void responseResult(HttpServletResponse response, RetResult<Object> result) { | |
response.setCharacterEncoding("UTF-8"); | |
response.setHeader("Content-type", "application/json;charset=UTF-8"); | |
response.setStatus(200); | |
try { | |
response.getWriter().write(JSON.toJSONString(result)); | |
} catch (IOException ex) { | |
LOGGER.error(ex.getMessage()); | |
} | |
} | |
/** | |
* @Title: validateSign | |
* @Description: 一个简单的签名认证,规则: 1. 将请求参数按ascii码排序 2. | |
* 拼接为a=value&b=value...这样的字符串(不包含sign)3. | |
* 混合密钥(secret)进行md5获得签名,与请求的签名进行比较 | |
* @param request | |
* @Reutrn boolean | |
*/ | |
private boolean validateSign(HttpServletRequest request) { | |
String requestSign = request.getParameter("sign");// 获得请求签名,如sign=19e907700db7ad91318424a97c54ed57 | |
if (StringUtils.isEmpty(requestSign)) { | |
return false; | |
} | |
List<String> keys = new ArrayList<String>(request.getParameterMap().keySet()); | |
keys.remove("sign");// 排除sign参数 | |
Collections.sort(keys);// 排序 | |
StringBuilder sb = new StringBuilder(); | |
for (String key : keys) { | |
sb.append(key).append("=").append(request.getParameter(key)).append("&");// 拼接字符串 | |
} | |
String linkString = sb.toString(); | |
linkString = StringUtils.substring(linkString, 0, linkString.length() - 1);// 去除最后一个'&' | |
String secret = "Potato";// TODO 密钥,自己修改 | |
String sign = DigestUtils.md5Hex(linkString + secret);// 混合密钥md5 | |
return StringUtils.equals(sign, requestSign);// 比较 | |
} | |
OAuth简史: 2007年12月4日发布了OAuth Core 1.0, 此版本的协议存在严重的安全漏洞:OAuth Security Advisory: 2009.1,更详细的安全漏洞介绍可以参考:Explaining the OAuth Session Fixation Attack。2009年6月24日发布了OAuth Core 1.0 Revision A:此版本的协议修复了前一版本的安全漏洞,并成为RFC5849,我们现在使用的OAuth版本多半都是以此版本为基础。 OAuth 2.0是OAuth协议的下一版本,但不向后兼容OAuth 1.0。 OAuth 2.0关注客户端开发者的简易性,同时为Web应用,桌面应用和手机,和起居室设备提供专门的认证流程。
OAuth角色:
- Consumer:消费方
- Service Provider:服务提供者
- User:用户
- 用户访问客户端的网站,想操作用户存放在服务提供方的资源。
- 客户端向服务提供方请求一个临时令牌。
- 服务提供方验证客户端的身份后,授予一个临时令牌。
- 客户端获得临时令牌后,将用户引导至服务提供方的授权页面请求用户授权。在这个过程中将临时令牌和客户端的回调连接发送给服务提供方。
- 用户在服务提供方的网页上输入用户名和密码,然后授权该客户端访问所请求的资源。
- 授权成功后,服务提供方引导用户返回客户端的网页。
- 客户端根据临时令牌从服务提供方那里获取访问令牌。
- 服务提供方根据临时令牌和用户的授权情况授予客户端访问令牌。
- 客户端使用获取的访问令牌访问存放在服务提供方上的受保护的资源。
OAuth和OpenID的区别: OAuth关注的是authorization授权,即:“用户能做什么”; 而OpenID侧重的是authentication认证,即:“用户是谁”。 OpenID、OAuth联合使用例子:
- OpenID 用户希望访问其在example.com的账户
- example.com(在OpenID的黑话里面被称为“Relying Party”) 提示用户输入他/她/它的OpenID
- 用户给出了他的OpenID,比如说"http://user.myopenid.com"
- example.com 跳转到了用户的OpenID提供商“mypopenid.com”
- 用户在"myopenid.com"(OpenID provider)提示的界面上输入用户名密码登录
- “myopenid.com" (OpenID provider) 问用户是否要登录到example.com
- 用户同意后,"myopenid.com" (OpenID provider) 跳转回example.com
- example.com 允许用户访问其帐号
- 用户在使用example.com时希望从mycontacts.com导入他的联系人
- example.com (在OAuth的黑话里面叫“Consumer”)把用户送往mycontacts.com (黑话是“Service Provider”)
- 用户在mycontacts.com 登录(可能也可能不用了他的OpenID)
- mycontacts.com问用户是不是希望授权example.com访问他在mycontact.com的联系人
- 用户确定
- mycontacts.com 把用户送回example.com
- example.com 从mycontacts.com拿到联系人
- example.com 告诉用户导入成功
Google Connect(基于OpenID + OAuth思想的定制):
OAuth 2.0的新特性 - 6种全新流程:
- User-Agent Flow – 客户端运行于用户代理内(典型如web浏览器)。
- Web Server Flow – 客户端是web服务器程序的一部分,通过http request接入,这是OAuth 1.0提供的流程的简化版本。
- Device Flow – 适用于客户端在受限设备上执行操作,但是终端用户单独接入另一台电脑或者设备的浏览器
- Username and Password Flow – 这个流程的应用场景是,用户信任客户端处理身份凭据,但是仍然不希望客户端储存他们的用户名和密码,这个流程仅在用户高度信任客户端时才适用。
- Client Credentials Flow – 客户端使用它的身份凭据去获取access token,这个流程支持2-legged OAuth的场景。
- Assertion Flow – 客户端用assertion去换取access token,比如SAML assertion。
https://www.jianshu.com/p/c8483eee7c48