接口安全处理
Posted 你我山巅自相逢<
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了接口安全处理相关的知识,希望对你有一定的参考价值。
一、为什么要保证接口安全
在我们日常开发中,存在一些接口是敏感且重要的,比如充值接口,如果在你调用充值接口的时候被别人抓包了,然后就可以修改充值的金额,本来充值10元可以改成充值10w,产生重大生产问题,再或者说被被人抓包了,别人可以不限制的调用该充值10元的接口,调用几万次,也是会导致重大问题,那么我们该如何保证接口安全呢?
二、接口安全的几种方式
-
数据参数合法性校验
接口数据的安全性保证,还需要我们的系统,有个数据合法性校验,简单来说就是参数校验,比如身份证长度,手机号长度,是否是数字等等
-
token授权认证方式
一般我们系统都会使用token鉴权登陆校验用户登陆状态和用户权限,访问接口前先校验token的合法性
-
数据加密,防止报文明文传输
说到数据加密,我们不难想到使用HTTPS进行传输,HTTPS使用了RSA和AES加密的方式保证了数据传输中的安全问题,具体的HTTPS的加密原理,请看HTTPS原理
数据在传输过程中被加密了,理论上,即使被抓包,数据也不会被篡改。但是https不是绝对安全的哦。还有一个点:https
加密的部分只是在外网,然后有很多服务是内网相互跳转的,签名验证也可以在这里保证不被中间人篡改,所以一般转账类安全性要求高的接口开发,都需要加签验签
-
签名验证
https虽然保证了在外网上数据不会被篡改,但是不能保证在内网中数据篡改的风险,所以需要有签名验证的环节
- 客户端把参数以特定顺序进行md5加密形成签名sign,一并同参数传递到服务端
- 服务端接收到签名和参数,也以一定的顺序对参数进行md5加密,对比传递来的sign判断是否是否被篡改
这样做的好处就是,在数据传输过程中,可以保证数据不会被篡改,如果篡改了的话sign就会不一致,验证不通过
但是这仅仅只是解决了篡改问题,那如果我拿到请求后不修改参数,原样数据多次调用,还是会产生问题,这时候就需要增加防重放功能
-
timestamp+nonce方案防止重放攻击
- timestamp是时间戳超时机制,当一个请求超过该时间后,则认定为该请求失效,需要重新发送请求,默认60s,但是如果在60s内多次调用岂不是也会导致问题?
- 通常来说,从抓包到重放的时间绝对不止60s,为了避免此类问题发生,我们可以在客户端发送请求的时候随机产生一个nonce随机数
- nonce令牌是一个随机数,每次请求后都会存入redis,过期时间60s,这样就没个请求只能请求一次,避免了多次调用的问题
-
白名单黑名单
三、防重放和防篡改拦截器
这里我们使用timestamp+nonce+sign对接口进行安全处理
1. 构建请求头
@Data
@Builder
public class RequestHeader
/**
* 签名
*/
private String sign;
/**
* 时间戳
*/
private Long timestamp;
/**
* 临时的数据
*/
private String nonce;
2. 保存请求流对象
public class SignRequestWrapper extends HttpServletRequestWrapper
//用于将流保存下来
private byte[] requestBody = null;
public SignRequestWrapper(HttpServletRequest request) throws IOException
super(request);
requestBody = StreamUtils.copyToByteArray(request.getInputStream());
@Override
public ServletInputStream getInputStream() throws IOException
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream()
@Override
public boolean isFinished()
return false;
@Override
public boolean isReady()
return false;
@Override
public void setReadListener(ReadListener readListener)
@Override
public int read() throws IOException
return bais.read();
;
@Override
public BufferedReader getReader() throws IOException
return new BufferedReader(new InputStreamReader(getInputStream()));
3. 创建请求数据处理工具
@Slf4j
public class HttpDataUtil
/**
* post请求处理:获取 Body 参数,转换为SortedMap
*
* @param request
*/
public static SortedMap<String, String> getBodyParams(final HttpServletRequest request) throws IOException
byte[] requestBody = StreamUtils.copyToByteArray(request.getInputStream());
String body = new String(requestBody);
return JsonUtils.parseObject(body, SortedMap.class);
/**
* get请求处理:将URL请求参数转换成SortedMap
*/
public static SortedMap<String, String> getUrlParams(HttpServletRequest request)
String param = "";
SortedMap<String, String> result = new TreeMap<>();
if (StringUtils.isEmpty(request.getQueryString()))
return result;
try
param = URLDecoder.decode(request.getQueryString(), "utf-8");
catch (UnsupportedEncodingException e)
e.printStackTrace();
String[] params = param.split("&");
for (String s : params)
String[] array = s.split("=");
result.put(array[0], array[1]);
return result;
4. 签名验证工具
@Slf4j
public class SignUtil
/**
* 验证签名
* 验证算法:把timestamp + JsonUtil.object2Json(SortedMap)合成字符串,然后MD5
*/
@SneakyThrows
public static boolean verifySign(SortedMap<String, String> map, RequestHeader requestHeader)
String params = requestHeader.getNonce() + requestHeader.getTimestamp() + JsonUtils.toJsonString(map);
return verifySign(params, requestHeader);
/**
* 验证签名
*/
public static boolean verifySign(String params, RequestHeader requestHeader)
log.debug("客户端签名: ", requestHeader.getSign());
if (StringUtils.isEmpty(params))
return false;
log.info("客户端上传内容: ", params);
String paramsSign = DigestUtils.md5DigestAsHex(params.getBytes()).toUpperCase();
log.info("客户端上传内容加密后的签名结果: ", paramsSign);
return requestHeader.getSign().equals(paramsSign);
5. 创建拦截器SignFilter
@Slf4j
public class SignFilter implements Filter
private static final Long signMaxTime = 60L;
private static final String NONCE_KEY = "x-nonce-";
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
log.info("过滤URL:", httpRequest.getRequestURI());
//request数据流只能读取一次,这里保存request流
HttpServletRequestWrapper requestWrapper = new SignRequestWrapper(httpRequest);
//构建请求头
String nonceHeader = httpRequest.getHeader("X-Nonce");
String timeHeader = httpRequest.getHeader("X-Time");
String signHeader = httpRequest.getHeader("X-Sign");
//验证请求头是否存在
if (StringUtils.isEmpty(nonceHeader) || ObjectUtils.isEmpty(timeHeader) || StringUtils.isEmpty(signHeader))
throw new RuntimeException("请求头不存在");
RequestHeader requestHeader = RequestHeader.builder()
.nonce(httpRequest.getHeader("X-Nonce"))
.timestamp(Long.parseLong(httpRequest.getHeader("X-Time")))
.sign(httpRequest.getHeader("X-Sign")).build();
/*
* 1.验证签名是否过期,防止重放
* 判断timestamp时间戳与当前时间是否操过60s(过期时间根据业务情况设置),如果超过了就提示签名过期。
*/
long now = System.currentTimeMillis() / 1000;
if (now - requestHeader.getTimestamp() > signMaxTime)
throw new RuntimeException("签名过期");
//2. 判断nonce,是否重复发送
boolean nonceExists = RedisUtils.hasKey(NONCE_KEY + requestHeader.getNonce());
if (nonceExists)
//请求重复
throw new RuntimeException("请求重复");
else
RedisUtils.set(NONCE_KEY + requestHeader.getNonce(), requestHeader.getNonce(), signMaxTime);
// 3. 验证签名,防止篡改
boolean accept;
SortedMap<String, String> paramMap;
switch (httpRequest.getMethod())
case "GET":
paramMap = HttpDataUtil.getUrlParams(requestWrapper);
accept = SignUtil.verifySign(paramMap, requestHeader);
break;
case "POST":
paramMap = HttpDataUtil.getBodyParams(requestWrapper);
accept = SignUtil.verifySign(paramMap, requestHeader);
break;
default:
accept = true;
break;
if (accept)
filterChain.doFilter(requestWrapper, servletResponse);
else
throw new RuntimeException("签名有误,请重新请求");
6. 配置拦截器
@Configuration
public class SignFilterConfiguration
@Bean
public FilterRegistrationBean contextFilterRegistrationBean()
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new SignFilter());
registration.addUrlPatterns("/sign/*");
registration.setName("SignFilter");
// 设置过滤器被调用的顺序
registration.setOrder(1);
return registration;
7. 测试
@RequestMapping("")
@RestController
public class SignDemoController
@PostMapping("/sign/demo1")
public R demo1(@RequestBody DemoDto demoDto)
System.out.println("===执行了demo1");
return R.ok();
@GetMapping("/demo2")
public R demo2()
System.out.println("执行了demo2====");
return R.ok();
@Data
class DemoDto
private Integer age;
private String username;
private Long id;
"age": 11,
"username": "zhangsan",
"id": 1
如何写出安全的API接口(参数加密+超时处理+私钥验证+Https)- 续(附demo)
上篇文章说到接口安全的设计思路,如果没有看到上篇博客,建议看完再来看这个。
通过园友们的讨论,以及我自己查了些资料,然后对接口安全做一个相对完善的总结,承诺给大家写个demo,今天一并放出。
对于安全也是相对的,下面我来根据安全级别分析
1.完全开放的接口
有没有这样的接口,谁都可以调用,谁都可以访问,不受时间空间限制,只要能连上互联网就能调用,毫无安全可言。
实话说,这样的接口我们天天都在接触,你查快递,你查天气预报,你查飞机,火车班次等,这些都是有公共的接口。
我把这称之为裸奔时代。代码如下:
/// <summary>
/// 接口对外公开
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("NoSecure")]
public HttpResponseMessage NoSecure(int age)
{
var result = new ResultModel<object>()
{
ReturnCode = 0,
Message = string.Empty,
Result = string.Empty
};
var dataResult = stulist.Where(T => T.Age == age).ToList();
result.Result = dataResult;
return GetHttpResponseMessage(result);
}
2.接口参数加密(基础加密)
你写个接口,你只想让特定的调用方使用,你把这些调用的人叫到一个小屋子,给他们宣布说我这里有个接口只打算给你们用,我给你们每人一把钥匙,你们用的时候拿着这把钥匙即可。
这把钥匙就是我上文说到的参数加密规则,有了这个规则就能调用。
这有安全问题啊,这里面的某个成员如果哪个不小心丢了钥匙或者被人窃取,掌握钥匙的人是不是也可以来掉用接口了呢?而且他可以复制很多钥匙给不明不白的人用。
相当于有人拿到了你的请求链接,如果业务没有对链接唯一性做判断(实际上业务逻辑通常不会把每次请求的加密签名记录下来,所以不会做唯一性判断),就会被重复调用,有一定安全漏洞,怎么破?先看这个场景的代码,然后继续往下看!
/// <summary>
/// 接口加密
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("SecureBySign")]
public HttpResponseMessage SecureBySign([FromUri]int age, long _timestamp, string appKey, string _sign)
{
var result = new ResultModel<object>()
{
ReturnCode = 0,
Message = string.Empty,
Result = string.Empty
};
#region 校验签名是否合法
var param = new SortedDictionary<string, string>(new AsciiComparer());
param.Add("age", age.ToString());
param.Add("appKey", appKey);
param.Add("_timestamp", _timestamp.ToString());
string currentSign = SignHelper.GetSign(param, appKey);
if (_sign != currentSign)
{
result.ReturnCode = -2;
result.Message = "签名不合法";
return GetHttpResponseMessage(result);
}
#endregion
var dataResult = stulist.Where(T => T.Age == age).ToList();
result.Result = dataResult;
return GetHttpResponseMessage(result);
}
3.接口参数加密+接口时效性验证(一般达到这个级别已经非常安全了)
继上一步,你发现有不明不白的人调用你的接口,你很不爽,随即把真正需要调用接口的人又叫来,告诉他们每天给他们换一把钥匙。和往常一样,有个别伙伴的钥匙被小偷偷走了,小偷煞费苦心,经过数天的踩点观察,准备在一个月黑风高的夜晚动手。拿出钥匙,捣鼓了半天也无法开启你的神圣之门,因为小偷不知道你天天都在换新钥匙。
小偷不服,经过一段时间琢磨,小偷发现了你们换钥匙的规律。在一次获得钥匙之后,不加思索,当天就动手了,因为他知道他手里的钥匙在第二天你更换钥匙后就失效了。
结果,小偷如愿。怎么破?先看这个场景的代码,然后继续往下看!
/// <summary>
/// 接口加密并根据时间戳判断有效性
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("SecureBySign/Expired")]
public HttpResponseMessage SecureBySign_Expired([FromUri]int age, long _timestamp, string appKey, string _sign)
{
var result = new ResultModel<object>()
{
ReturnCode = 0,
Message = string.Empty,
Result = string.Empty
};
#region 判断请求是否过期---假设过期时间是20秒
DateTime requestTime = GetDateTimeByTicks(_timestamp);
if (requestTime.AddSeconds(20) < DateTime.Now)
{
result.ReturnCode = -1;
result.Message = "接口过期";
return GetHttpResponseMessage(result);
}
#endregion
#region 校验签名是否合法
var param = new SortedDictionary<string, string>(new AsciiComparer());
param.Add("age", age.ToString());
param.Add("appKey", appKey);
param.Add("_timestamp", _timestamp.ToString());
string currentSign = SignHelper.GetSign(param, appKey);
if (_sign != currentSign)
{
result.ReturnCode = -2;
result.Message = "签名不合法";
return GetHttpResponseMessage(result);
}
#endregion
var dataResult = stulist.Where(T => T.Age == age).ToList();
result.Result = dataResult;
return GetHttpResponseMessage(result);
}
4.接口参数加密+时效性验证+私钥(达到这个级别安全性固若金汤)
继上一步,你发现道高一尺魔高一丈,仍然有偷盗事情发生。咋办呢?你打算下血本,给每个人配一把钥匙的基础上,再给每个人发个暗号,即使钥匙被小偷弄去了,小偷没有暗号,任然无法如愿,而且这样很容易定位是谁的暗号泄漏问题,找到问题根源,只需要给当前这个人换下钥匙就行了,不用大动干戈。
但这个并不是万无一失的,因为钥匙毕竟还有可能被小偷搞到。代码如下:
/// <summary>
/// 接口加密并根据时间戳判断有效性而且带着私有key校验
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("SecureBySign/Expired/KeySecret")]
public HttpResponseMessage SecureBySign_Expired_KeySecret([FromUri]int age, long _timestamp, string appKey, string _sign)
{
//key集合,这里随便弄两个测试数据
//如果调用方比较多,需要审核授权,根据一定的规则生成key把这些数据存放在数据库中,如果功能扩展开来,可以针对不同的调用方做不同的功能权限管理
//在调用接口时动态从库里取,每个调用方在调用时带上他的key,调用方一般把自己的key放到网站配置中
Dictionary<string, string> keySecretDic = new Dictionary<string, string>();
keySecretDic.Add("key_zhangsan", "D9U7YY5D7FF2748AED89E90HJ88881E6");//张三的key,
keySecretDic.Add("key_lisi", "I9O6ZZ3D7FF2748AED89E90ZB7732M9");//李四的key
var result = new ResultModel<object>()
{
ReturnCode = 0,
Message = string.Empty,
Result = string.Empty
};
#region 判断请求是否过期---假设过期时间是20秒
DateTime requestTime = GetDateTimeByTicks(_timestamp);
if (requestTime.AddSeconds(20) < DateTime.Now)
{
result.ReturnCode = -1;
result.Message = "接口过期";
return GetHttpResponseMessage(result);
}
#endregion
#region 根据appkey获取key值
string secret = keySecretDic.Where(T => T.Key == appKey).FirstOrDefault().Value;
#endregion
#region 校验签名是否合法
var param = new SortedDictionary<string, string>(new AsciiComparer());
param.Add("age", age.ToString());
param.Add("appKey", appKey);
param.Add("appSecret", secret);//把secret加入进行加密
param.Add("_timestamp", _timestamp.ToString());
string currentSign = SignHelper.GetSign(param, appKey);
if (_sign != currentSign)
{
result.ReturnCode = -2;
result.Message = "签名不合法";
return GetHttpResponseMessage(result);
}
#endregion
var dataResult = stulist.Where(T => T.Age == age).ToList();
result.Result = dataResult;
return GetHttpResponseMessage(result);
}
5.接口参数加密+时效性验证+私钥+Https(我把这个级别称之为金钟罩,世间最安全莫过于此)
继上一步,我们给传输机制改为Https,这下小偷彻底懵逼了。那么问题来了,Https咋玩儿呢?可以在本地搭个环境,参考此文:http://www.cnblogs.com/naniannayue/archive/2012/11/19/2776948.html
另:本文的接口是用的MVC WebAPI写的,完全基于RESTful标准。如对此不是特别了解可以参考此文:http://www.cnblogs.com/landeanfen/p/5501490.html
注:demo不能直接运行,需要把两个web项目配置到iis中,api代表接口提供方,他的主域需要配置到business的webconfig中,在浏览器地址栏分别请求business中的各个调用接口方法来实现接口调用。
1、如果想验证参数错误,需要在请求接口时打个断点把接口url取出,篡改url参数,然后在浏览器中模拟请求
2、如果想验证接口超时,需要在请求接口时打个断点把接口url取出,然后等到了超时时间,然后在浏览器中模拟请求
3、如果想验证私钥错误,需要在请求接口时打个断点把接口url取出,然后修改business的私钥配置,然后在浏览器中模拟请求
以上是关于接口安全处理的主要内容,如果未能解决你的问题,请参考以下文章
如何写出安全的API接口(参数加密+超时处理+私钥验证+Https)- 续(附demo)
如何写出安全的API接口(参数加密+超时处理+私钥验证+Https)- 续(附demo)(转)