• 接口的安全设计的几大要素:ticket,签名,时间戳


    概述

    与前端对接的API接口,如果被第三方抓包并进行恶意篡改参数,可能会导致数据泄露,甚至会被篡改数据,我主要围绕时间戳,token,签名三个部分来保证API接口的安全性

    968e187655d70e807a6aaca1266153df.jpeg

    1.用户成功登陆站点后,服务器会返回一个token,用户的任何操作都必须带了这个参数,可以将这个参数直接放到header里。

    2.客户端用需要发送的参数和token生成一个签名sign,作为参数一起发送给服务端,服务端在用同样的方法生成sign进行检查是否被篡改。

    3.但这依然存在问题,可能会被进行恶意无限制访问,这时我们需要引入一个时间戳参数,如果超时即是无效的。

    4.服务端需要对token,签名,时间戳进行验证,只有token有效,时间戳未超时,签名有效才能被放行。

    开放接口

    没有进行任何限制,简单粗暴的访问方式,这样的接口方式一般在开放的应用平台,查天气,查快递,只要你输入正确对应的参数调用,即可获取到自己需要的信息,我们可以任意修改参数值。

    /*
     * Description: 开放的接口
     * @author huangweicheng
     * @date 2020/12/21
    */
    @RestController
    @RequestMapping( "/token")
    public  class TokenSignController {

         @Autowired
         private TokenSignService tokenSignService;

         @RequestMapping(value =  "openDemo",method = RequestMethod.GET)
         public List openDemo(int personId){
             return tokenSignService.getPersonList(personId);
        }
    }

    Token认证获取

    用户登录成功后,会获取一个ticket值,接下去任何接口的访问都需要这个参数。我们把它放置在redis内,有效期为10分钟,在ticket即将超时,无感知续命。延长使用时间,如果用户在一段时间内没进行任何操作,就需要重新登录系统。扩展:

    @RequestMapping(value =  "login",method = RequestMethod.POST)
         public JSONObject login(@NotNull String username, @NotNull String password){
             return tokenSignService.login(username,password);
        }

    登录操作,查看是否有这个用户,用户名和密码匹配即可成功登录。

    /** 
         * 
         * Description:验证登录,ticket成功后放置缓存中,
         * @param
         * @author huangweicheng
         * @date 2020/12/31   
        */ 
         public JSONObject login(String username,String password){
            JSONObject result =  new JSONObject();
            PersonEntity personEntity = personDao.findByLoginName(username);
             if (personEntity ==  null || (personEntity !=  null && !personEntity.getPassword().equals(password))){
                result.put( "success", false);
                result.put( "ticket", "");
                result.put( "code", "999");
                result.put( "message", "用户名和密码不匹配");
                 return result;
            }
             if (personEntity.getLoginName().equals(username) && personEntity.getPassword().equals(password)){
                String ticket = UUID.randomUUID().toString();
                ticket = ticket.replace( "-", "");
                redisTemplate.opsForValue().set(ticket,personEntity.getLoginName(), 10L, TimeUnit.MINUTES);
                result.put( "success", true);
                result.put( "ticket",ticket);
                result.put( "code", 200);
                result.put( "message", "登录成功");
                 return result;
            }
            result.put( "success", false);
            result.put( "ticket", "");
            result.put( "code", "1000");
            result.put( "message", "未知异常,请重试");
             return result;
        }

    Sign签名

    把所有的参数拼接一起,在加入系统秘钥,进行MD5计算生成一个sign签名,防止参数被人恶意篡改,后台按同样的方法生成秘钥,进行签名对比。

    /**
         * @param request
         * @return
         */
         public static Boolean checkSign(HttpServletRequest request,String sign){
            Boolean flag=  false;
             //检查sigin是否过期
            Enumeration pNames =  request.getParameterNames();
            Map params =  new HashMap();
             while (pNames.hasMoreElements()) {
                String pName = (String) pNames.nextElement();
                 if( "sign".equals(pName))  continue;
                String pValue = (String)request.getParameter(pName);
                params.put(pName, pValue);
            }
            System.out.println( "现在的sign-->>" + sign);
            System.out.println( "验证的sign-->>" + getSign(params,secretKeyOfWxh));
             if(sign.equals(getSign(params, secretKeyOfWxh))){
                flag =  true;
            }
             return flag;
        }

    重复访问

    引入一个时间戳参数,保证接口仅在一分钟内有效,需要和客户端时间保持一致。

    public static long getTimestamp(){
             long timestampLong = System.currentTimeMillis();

             long timestampsStr = timestampLong /  1000;

             return timestampsStr;
        }

    需要跟当前服务器时间进行对比,如果超过一分钟,就拒绝本次请求,节省服务器查询数据的消耗

    拦截器

    每次请求都带有这三个参数,我们都需要进行验证,只有在三个参数都满足我们的要求,才允许数据返回或被操作。

    public  class LoginInterceptor implements HandlerInterceptor {

         @Autowired
         private RedisTemplate redisTemplate;

         @Override
         public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws IOException {
            JSONObject jsonObject =  new JSONObject();
            String ticket = request.getParameter( "ticket");
            String sign = request.getParameter( "sign");
            String ts = request.getParameter( "ts");
             if (StringUtils.isEmpty(ticket) || StringUtils.isEmpty(sign) || StringUtils.isEmpty(ts)){
                jsonObject.put( "success", false);
                jsonObject.put( "message", "args is isEmpty");
                jsonObject.put( "code", "1001");
                PrintWriter printWriter = response.getWriter();
                printWriter.write(jsonObject.toJSONString());
                 return  false;
            }
             //如果redis存在ticket就认为是合法的请求
             if (redisTemplate.hasKey(ticket)){
                System.out.println(redisTemplate.opsForValue().getOperations().getExpire(ticket));
                String values = (String) redisTemplate.opsForValue().get(ticket);
                 //判断ticket是否即将过期,进行续命操作
                 if (redisTemplate.opsForValue().getOperations().getExpire(ticket) != - 2 && redisTemplate.opsForValue().getOperations().getExpire(ticket) <  20){
                    redisTemplate.opsForValue().set(ticket,values, 10L, TimeUnit.MINUTES);
                }
                System.out.println(SignUtils.getTimestamp());
                 //判断是否重复访问,存在重放攻击的时间窗口期
                 if (SignUtils.getTimestamp() - Long.valueOf(ts) >  600){
                    jsonObject.put( "success", false);
                    jsonObject.put( "message", "Overtime to connect to server");
                    jsonObject.put( "code", "1002");
                    PrintWriter printWriter = response.getWriter();
                    printWriter.write(jsonObject.toJSONString());
                     return  false;
                }
                 //验证签名
                 if (!SignUtils.checkSign(request,sign)){
                    jsonObject.put( "success", false);
                    jsonObject.put( "message", "sign is invalid");
                    jsonObject.put( "code", "1003");
                    PrintWriter printWriter = response.getWriter();
                    printWriter.write(jsonObject.toJSONString());
                     return  false;
                }
                 return  true;
            } else {
                jsonObject.put( "success", false);
                jsonObject.put( "message", "ticket is invalid,Relogin.");
                jsonObject.put( "code", "1004");
                PrintWriter printWriter = response.getWriter();
                printWriter.write(jsonObject.toJSONString());
            }
             return  false;
        }
    }

    访问

    先登录系统,获取合法的ticket

    515bf4eff0c22399d88a991ce53568e3.jpeg

    生成一个合法的sign验证,获取测试ts,访问openDemo,即可正常访问。还可以将参数加密,将http换成https,就不一 一展开了。

    6214136c43276d0bb431ec96c6a7a645.jpeg

    demo代码

    https://github.com/hwc4110/spring-demo1221

  • 相关阅读:
    深入JVM:探索Java虚拟机
    使用RANKX计算层级排名
    Java并发总结
    CCF CSP认证 历年题目自练Day36
    【SSL 1590】旅游(线段树优化DP)
    【中兴】web训练营~一文带你走进前端 | 百图制作
    PZ-3B-70E2A-10、PZ-6B-220E2A-20电控比例压力流量驱动泵放大器
    模型推荐丨电子商务实战案例模型分享
    FFmpeg开发笔记(三十八)APP如何访问SRS推流的RTMP直播地址
    Java刷题面试系列习题(十一)
  • 原文地址:https://blog.csdn.net/chenxuyuana/article/details/126190347