需要思考的问题
- 对于外部系统进行对接得时候为什么要进行鉴权
- 鉴权需要哪些准备工作
- 如何进行统一校验不用写重复得代码
概述
在为第三方系统提供接口的时候,肯定是要考虑接口数据安全问题
比如:
- 接口中数据是否有篡改,如果有大佬在你的接口数据中增加或者修改数据,那岂不是对业务系统造成不可避免的损失
- 请求是否已经超时。
- 请求是否重复提交等问题。
如何使做到统一校验:
- 使用spring web 拦截器
- 使用spring aop
本次就使用spring web 拦截器进行统一校验
设计思路
如何防止上述问题的出现呢 需要向数据发送方进行约定传入参数进行校验
因此在做接口主要解决如下几个问题:
- 请求发起必须是在规定的时间
- 请求发起方必须是授权的对象
- 请求不能重复发起
- 请求发送时参数不能被篡改
接口的设计
1. 第三方请求的合法性
- 需要向对方约定在headers中添加以下几个参数
- sign值:sign值说明在最后
- timestamp: 发送时间戳
- sn:未唯一请求值,对方系统可取随机数,不能重复
- appid:业务系统标识 线下提供appid 分配的 sate 值
- 接受方系统会校验次几项参数是否正确填写
2.单次请求失效性
接受方系统会校验发送方timestamp时间戳不得超过当前时间 360秒
//进行限制 如果当前时间 - 发送时间 > 3600 则认定超时
int time = 360;
long now = System.currentTimeMillis() / 1000;
if ((now - Long.valueOf(timeStamp)) > time) {
map.put("code", "ERROR");
map.put("message", "请求发起时间超过服务器规定得时间");
return map;
}
3.校验APPID 是否正确
接受方系统需要校验发送的APPID是否正确顺便取出需要得sate值
private static final Map<String, String> appSate = new HashMap<>();
static {
appSate.put("123", "321");
appSate.put("789", "987");
}
//目前先写死appid 正常应该从数据库中进行配置
String sate = appSate.get(appid);
if (StringUtils.isEmpty(sate)) {
map.put("code", "ERROR");
map.put("message", "appid填写不正确,请填写正确得appid或联系管理员进行处理!");
return map;
}
4.请求判重
接受方系统需要需要根据发送方的sn值来判断是否是重复请求
- 1.使用Redis(推荐)
- 2.使用Redisson(推荐)
- 3.使用ConcurrentHasMap(也可以)
在此我是用的是ConcurrentHashMap使用其中putIfAbsent(Redist同样的方法)
如果所指定的 key 已经在 map中存在,返回和这个 key 值对应的 value, 如果所指定的 key 不在 map中存在,则返回 null。
注意:使用redis时加上key的过期时间
//判断是否 是重复请求 再次使用ConcurrentHashMap有条件同学或者正式环境可以使用Redis或者Redisson
{
//首先先从map中或者redis中获取是否存在该sn
//原则上sn不允许重复 也可以设置规则 sn 在第一天中不能重复
String s = toKenMap.putIfAbsent(toKenMapPrefix + sn, sn);
if (s != null) {
map.put("code", "ERROR");
map.put("message", "sn->重复,检测为重复请求!");
return map;
}
}
5.sign校验及说明
sign值说明
- sign = body中的参数需要根据key进行排序组合成 key = value & 的格式 最后增加 sate = value 的
- 然后进行md5加密
示例:
json字符串为:{"name":"张三","age": "15"}
需要组合成:age=15&name=张三&sate=123
最后进行md5加密:fcae857780607eb61ff81b0f70e43d1
接受方系统需要需要根据发送方的sign值来判断是否篡改
TreeMap body = getBody(request);
String body1 = getBody(body, sate);
String s = signDataParam(body1);
if (!sign.equals(s)) {
map.put("code", "ERROR");
map.put("message", "无效的Sign值");
return map;
}
String getBody(SortedMap<String, String> map, String sate) {
StringBuffer sb = new StringBuffer();
if (map != null) {
for (String key : map.keySet()) {
sb.append(key + "=" + map.get(key) + "&");
}
}
sb.append("sate" + "=" + sate);
return sb.toString();
}
TreeMap getBody(HttpServletRequest request) {
String bodyString = HttpHelper.getBodyString(request);
TreeMap o = JSONObject.parseObject(bodyString, TreeMap.class);
return o;
}
完整示例代码
public class WebInterceptors implements HandlerInterceptor {
private final ConcurrentHashMap<String, String> toKenMap = new ConcurrentHashMap<>();
private final String toKenMapPrefix = "TOKENCHECK";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//实现校验逻辑
Map<String, String> map = check(request);
if ("SUCCESS".equals(map.get("code"))) {
System.out.println("成功");
return true;
} else {
//校验失败
System.out.println("失败!");
return error(response, map.get("message"));
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
private static final Map<String, String> appSate = new HashMap<>();
static {
appSate.put("123", "321");
appSate.put("789", "987");
}
/***
* 校验请求头
* 1.先看所有的参数是否为空
* 2.timeStamp 时间戳是否所起
* 3.校验appid 是否存在
* 4.校验是否重复请求
* @param request
* @return
*/
public Map<String, String> check(HttpServletRequest request) {
Map<String, String> map = new HashMap<>();
String sign = request.getHeader("sign");
String timeStamp = request.getHeader("timeStamp");
String sn = request.getHeader("sn");
String appid = request.getHeader("appid");
if (StringUtils.isEmpty(timeStamp) || StringUtils.isEmpty(sn) || StringUtils.isEmpty(sign) || StringUtils.isEmpty(appid)) {
map.put("code", "ERROR");
map.put("message", "请求参数填写不规范!");
return map;
}
//进行限制 如果当前时间 - 发送时间 > 360 则认定超时
int time = 360;
long now = System.currentTimeMillis() / 1000;
if ((now - Long.valueOf(timeStamp)) > time) {
map.put("code", "ERROR");
map.put("message", "请求发起时间超过服务器规定得时间");
return map;
}
//目前先写死appid 正常应该从数据库中进行配置
String sate = appSate.get(appid);
if (StringUtils.isEmpty(sate)) {
map.put("code", "ERROR");
map.put("message", "appid填写不正确,请填写正确得appid或联系管理员进行处理!");
return map;
}
//判断是否 是重复请求 再次使用ConcurrentHashMap有条件或者正式环境可以使用Redis或者Redisson
{
//首先先从map中或者redis中获取是否存在该sn
//原则上sn不允许重复 也可以设置规则 sn 在第一天中不能重复
String s = toKenMap.putIfAbsent(toKenMapPrefix + sn, sn);
if (s != null) {
map.put("code", "ERROR");
map.put("message", "sn->重复,检测为重复请求!");
return map;
}
}
TreeMap body = getBody(request);
String body1 = getBody(body, sate);
String s = signDataParam(body1);
if (!sign.equals(s)) {
map.put("code", "ERROR");
map.put("message", "无效的Sign值");
return map;
}
return map;
}
private String signDataParam(String params) {
if (Objects.isNull(params)) {
return "";
}
return MD5Utils.string2MD5(params);
}
String getBody(SortedMap<String, String> map, String sate) {
StringBuffer sb = new StringBuffer();
if (map != null) {
for (String key : map.keySet()) {
sb.append(key + "=" + map.get(key) + "&");
}
}
sb.append("sate" + "=" + sate);
return sb.toString();
}
TreeMap getBody(HttpServletRequest request) {
String bodyString = HttpHelper.getBodyString(request);
TreeMap o = JSONObject.parseObject(bodyString, TreeMap.class);
return o;
}
private boolean error(HttpServletResponse httpServletResponse, String errorMsg) throws IOException {
ObjectMapper om = new ObjectMapper();
Map<String, Object> map = new HashMap<String, Object>();
map.put("code", 5000);
map.put("msg", errorMsg);
httpServletResponse.setHeader("Content-type", "text/html;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
om.writeValue(httpServletResponse.getOutputStream(), map);
return false;
}