restful 风格接口大行其道,大家现在用的也都差不多,我们只用了 post 一种方式。
之前公司接口没有做任何的安全校验,也就是说如果别人知道了接口地址,正确的传参就能返回结果。
下载个 app ,然后开个代理,所有接口、参数列表都能抓取到。如果有人想做些坏事的话,这就太简单了。
所以考虑给接口加密。
综合考虑后,得出下面的方案。
- 请求端
- 将请求的参数和约定的秘钥做一次 HMAC-MD5 得到 sign。
-
将 sign 放到 http 请求的 header 里。
- 服务端
- 过滤器拦截请求,取出请求的参数, header 中的 sign。
- 将请求的参数加上秘钥做 HMAC-MD5 得到 sign。
- 将 header 中的 sign 和计算出来的 sign 比较,相同则正确,否则抛出 401。
看到这可能会有人有疑问为啥不用拦截器?
我一开始也是这么想的,但是发现一个坑爹的情况,就是在拦截器里读取不到 body,要不然就是报错。
那是因为 springmvc 已经读取过了,大家都知道 springmvc 里有个不错的功能就是数据绑定,有兴趣的话可以看看 springmvc HttpMessageConverter 相关代码。
具体原因是: 流对应的是数据,数据放在内存中,有的是部分放在内存中。read 一次标记一次当前位置(mark position),第二次read就从标记位置继续读(从内存中copy)数据。 所以这就是为什么读了一次第二次是空了。 怎么让它不为空呢?只要inputstream 中的pos 变成0就可以重写读取当前内存中的数据,但很遗憾,没找到有修改pos 的api。转自这个问题里面的答案
后来看到了这篇文章,这哥们也说想要对 request body 数据做些处理,但是发现无法读取两次。然后就用了 request 包装类来处理。我最终的实现办法也是基于这篇文章的。
还有关于 MultiReadHttpServletRequest 包装类缓存 request ,让 request 能够读取多次的办法来自这个 StackOverFlow
请求端
// 请求端自己封装的 httpclient
// obj 是请求的参数,最终会转成 json 串
public <T> String execPostRequestWithSign(String uri, T obj) {
HttpPost request = new HttpPost(uri);
addContentType(request, JsonContentType);
String json = "";
if(obj != null ){
json = JsonUtil.getJsonFromObject(obj);
}
// json 串加 privateCode (私钥) 得到 sign
if (json != null) {
String sign = Md5Util.getHmacMD5(json, privateCode);
request.addHeader("sign",sign);
}
//...
}
服务端
RequestWrapperFilter 过滤器
public class RequestWrapperFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// TODO Auto-generated method stub
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
MultiReadHttpServletRequest requestWrapper = new MultiReadHttpServletRequest((HttpServletRequest) request);
chain.doFilter(requestWrapper,response);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void destroy() {
// TODO Auto-generated method stub
}
}
MultiReadHttpServletRequest 包装类
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
Logger logger = LoggerFactory.getLogger(getClass());
private ByteArrayOutputStream cachedBytes;
private static final String privateCode = "YEjdO34*6)&2G";
public MultiReadHttpServletRequest(HttpServletRequest request) throws Exception {
super(request);
String bodyString = IOUtils.toString(this.getInputStream());
String requestSign = request.getHeader("sign");
if (StringUtils.isNotBlank(bodyString) && StringUtils.isNotBlank(requestSign)) {
String sign = Md5Util.getHmacMD5(bodyString, privateCode);
if (!sign.equals(requestSign)) {
logger.error("sign error,bodystring:", bodyString);
throw new AuthRequestRuntimeException("sign error");
}
}else {
throw new AuthRequestRuntimeException("sign and request body couldn't be null");
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (cachedBytes == null)
cacheInputStream();
return new CachedServletInputStream();
}
@Override
public BufferedReader getReader() throws IOException{
return new BufferedReader(new InputStreamReader(getInputStream()));
}
private void cacheInputStream() throws IOException {
/* Cache the inputstream in order to read it multiple times. For
* convenience, I use apache.commons IOUtils
*/
cachedBytes = new ByteArrayOutputStream();
IOUtils.copy(super.getInputStream(), cachedBytes);
}
/* An inputstream which reads the cached request body */
public class CachedServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
public CachedServletInputStream() {
/* create a new input stream from the cached request body */
input = new ByteArrayInputStream(cachedBytes.toByteArray());
}
@Override
public int read() throws IOException {
return input.read();
}
@Override
public boolean isFinished() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
// TODO Auto-generated method stub
}
}
}
web.xml
<filter>
<filter-name>RequestWrapperFilter</filter-name>
<filter-class>com.xxxxx.service.servlet.RequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>RequestWrapperFilter</filter-name>
<url-pattern>/v2/*</url-pattern>
</filter-mapping>
优点
大大的提升了安全性,接口再也不是裸的了。即使抓包抓到了接口抓到了请求参数,没有秘钥你也无法请求到其他数据。
实现简单,不用改太多的东西,对业务基本无影响。
缺点
sign 只绑定了请求的参数,其实最好还要把接口地址绑定上去,要不然同样参数请求不同的接口,可以用相同的 sign。
还有个缺点就是无法友好的返回异常,正常来说请求异常了要返回错误信息,但是现在这个样子没有返回结果。
到时候看看有没有更好的办法。
总结
其实加密方式有很多种,MD5、SHA1、RSA、AES 这几种目前来说是比较常见的。根据自己的要求选择就好。
考虑到平滑升级可以用 api 版本来过渡下,就是在 url 中加一个版本字段(像这样 /v2/getuser/id
)。
所有接口都升级完之后再把之前的接口停掉。
将 sign 放到 herader 中是出于处理方便考虑的,最开始想拼接到 request body 中,但是后来觉得放到 header 中会更方便些,不用再截取 request body。
如果你有更好的方法或者任何建议,欢迎在评论里留言。
There are no comments on this post.