JavaWeb项目对接第三方接口的鉴权

需要思考的问题

  1. 对于外部系统进行对接得时候为什么要进行鉴权
  2. 鉴权需要哪些准备工作
  3. 如何进行统一校验不用写重复得代码

概述

在为第三方系统提供接口的时候,肯定是要考虑接口数据安全问题

比如

  • 接口中数据是否有篡改,如果有大佬在你的接口数据中增加或者修改数据,那岂不是对业务系统造成不可避免的损失 

  • 请求是否已经超时。

  • 请求是否重复提交等问题。

如何使做到统一校验

  • 使用spring web 拦截器
  • 使用spring aop

本次就使用spring web 拦截器进行统一校验

设计思路

如何防止上述问题的出现呢 需要向数据发送方进行约定传入参数进行校验

因此在做接口主要解决如下几个问题:

  1. 请求发起必须是在规定的时间
  2. 请求发起方必须是授权的对象
  3. 请求不能重复发起
  4. 请求发送时参数不能被篡改

接口的设计

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;
}
  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值