接口安全优化

发布于 2022-03-10  1.6k 次阅读


接口的安全优化注意包括三个方面:

  1. 接口地址的隐藏
  2. 接口验证码机制
  3. 接口限流

一,接口地址的隐藏

接口地址隐藏的作用:防止恶意的脚本请求,如果直接暴露接口地址,脚本的请求破坏即损坏了用户体验又增加了服务器压力

隐藏接口地址的实现过程:

  1. 创建两个接口,一个为对外暴露的接口,一为隐藏接口
  2. 暴露接口产生随机地址码,隐藏接口进行判断并调用业务方法
  3. 数据解耦通过redis
例:代码演示

① 前端

//暴露接口
function interface() {
    var id = $("#seckid").val();
    var code = $("#captcha").val()
    $.ajax({
        url: "/tSeckillOrder/Seckillfalse/"+id+"/"+code,
        type: "GET",
        dataType: "json",
        success : function (datas) {
            if(datas.code==200){
            interfaceaddr(datas.obj.path);
        }

}
    })

}

//调用隐藏接口
function interfaceaddr(path) {
    var id = $("#seckid").val();
    var datas= {
        goodsid:id

}
    $.ajax({
        url: path,
        type: "POST",
        dataType: "json",
        data: datas,
        success : function (datas) {
            if(datas.code==200){
                alert("成功")
            }else {
                alert("失败")
            }

}

② 地址码

@Service
public class Interfacehidden {

    @Autowired
    RedisTemplate redisTemplate;


    //创建隐藏接口的地址并保存
    public String createinterfaceaddr(String username,int goodid){

        String substring = Md5Hash.fromBase64String(UUID.randomUUID() + "123456").toHex().toString().substring(0, 15);

        redisTemplate.opsForValue().set(username+":"+goodid,substring,60, TimeUnit.SECONDS);

        return substring;
    }

    //效验
    public Boolean  checkoutinterfaceaddr(String username,int goodid,String md5random){


        String random= (String) redisTemplate.opsForValue().get(username + ":" + goodid);


        if(md5random.equals(random)){
            return true;
        }
        return false;
    }

③ 暴露的接口

//暴露的接口
@GetMapping("/Seckillfalse/{id}")
@ResponseBody
public ResposeBeanCode seckaddre(HttpSession httpSession, @PathVariable("id") int goodsid) {

    //获取用户名
    String user = (String)httpSession.getAttribute("user");

    String createinterfaceaddr = interfacehidden.createinterfaceaddr(user, goodsid);

    String path = "/tSeckillOrder/" +createinterfaceaddr+"/seckillorder";

    HashMap<String,String> map = new HashMap();
    map.put("path",path);

    return  ResposeBeanCode.addresssuccess(map) ;
}

④ 隐藏的接口

//隐藏的接口
@PostMapping("/{ran}/seckillorder")
@ResponseBody
public ResposeBeanCode seckillorder(@RequestParam("goodsid") int goodsid,@PathVariable("ran") String ran,HttpSession httpSession){

    String user = (String) httpSession.getAttribute("user");

    Boolean checkoutinterfaceaddr = interfacehidden.checkoutinterfaceaddr(user, goodsid, ran);

    if(!checkoutinterfaceaddr){
        System.out.println("checkoutinterfaceaddr:"+checkoutinterfaceaddr);
        return ResposeBeanCode.error();
    }

    boolean seckillserver = seckillOrderService.seckillserver(goodsid, user);

    if(!seckillserver){
        return ResposeBeanCode.error();
    }
    return ResposeBeanCode.success();
}

二,接口验证码机制

验证码使用git上的easy-captcha开源项目:

<dependency>
   <groupId>com.github.whvcse</groupId>
   <artifactId>easy-captcha</artifactId>
   <version>1.6.2</version>
</dependency>

接口验证码的作用:

  • 为接口加一层安全防护,更进一步防止脚本刷链接
  • 再一定程度上可以削峰,输入效验码的过程可以对流量进行分流

接口验证机制实现的过程:和接口隐藏差不多,只是中间加入了效验码

例:代码演示,再接口地址隐藏的基础上改进

①效验码

@RequestMapping("/seckill")
public class CheckCode {

    @Autowired
    RedisTemplate redisTemplate;

    @RequestMapping("/captcha/{id}")
    public void captcha(@PathVariable("id") String id, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 设置请求头为输出图片类型
        response.setContentType("image/gif");
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);


        // 三个参数分别为宽、高、位数
        SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 5);
        // 设置字体
        specCaptcha.setFont(new Font("Verdana", Font.PLAIN, 32));  // 有默认字体,可以不用设置
        // 设置类型,纯数字、纯字母、字母数字混合
        specCaptcha.setCharType(Captcha.TYPE_ONLY_NUMBER);
        //获取id和用户
        String user_id = request.getSession().getAttribute("user")+"_fq:"+id;
        //以用户名和id键,值为验证码保存到redis中
        redisTemplate.opsForValue().set(user_id,specCaptcha.text(),300, TimeUnit.SECONDS);
        // 输出图片流
        specCaptcha.out(response.getOutputStream());
    }
}

②前端

<img id="captchImg" height="32" width="130" onclick="refresshCaptcha()"/>
function interface() {
    var id = $("#seckid").val();

    var code = $("#captcha").val();

    if(code!=""){
    $.ajax({
        url: "/tSeckillOrder/Seckillfalse/"+id+"/"+code,
        type: "GET",
        dataType: "json",
        success : function (datas) {
            if(datas.code==200){
            interfaceaddr(datas.obj.path);
            }
}else {
                alert("验证码错误")}
        }}
    })}else{
        alert("验证码不能为空")
    }
}

③接口

@GetMapping("/Seckillfalse/{id}/{verificat}")
@ResponseBody
public ResposeBeanCode seckaddre(HttpSession httpSession, @PathVariable("id") int goodsid,@PathVariable("verificat") String verificat) {

    //获取用户名
    String user = (String)httpSession.getAttribute("user");

    //验证码判断
    boolean b = verificationCode.VerificationC(user, goodsid, verificat);

    if(!b){
        return ResposeBeanCode.verificationerror();
    }

    String createinterfaceaddr = interfacehidden.createinterfaceaddr(user, goodsid);

    String path = "/tSeckillOrder/" +createinterfaceaddr+"/seckillorder";

    HashMap<String,String> map = new HashMap();
    map.put("path",path);

    return  ResposeBeanCode.addresssuccess(map) ;
}

三,接口限流机制

接口限流是高并发系统的一个常见的处理方法,对访问流量进行限制,让系统处于一个最安全的处理环境

接口限流主要有两种:

  • 控制并发访问的数量:最简单粗暴的方法,设置最大访问数量超过访问数量就直接拒绝请求
  • 控制并发访问的速率:这个是企业开发最常用的方式,它主要有两种算法,漏桶算法和令牌桶算法

自定义控制访问数量来实现限流:

  1. 依赖SpringMvc的拦截器,对指定接口进行拦截,判断控制并发量
  2. 使用Redis保存数量,判断是否超过限制标准
  3. 加入没有超过就返回true放行,假如为flase就拦截

例:

① 自定义限流注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentAnnotation {

    int second();//redis中的过期时间
    int maxcount();//最大流量限制数量
}

② 编写拦截器

@Component
public class MyHandlerInterceptor implements HandlerInterceptor {

    @Autowired
    RedisTemplate redisTemplate;

    @Autowired
    UserContext userContext;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {


       if(handler instanceof HandlerMethod){


           HandlerMethod handlerMethod = (HandlerMethod) handler;
           //反射获取CurrentAnnotation注解,handlerMethod在方法上寻找标注CurrentAnnotation的方法
           CurrentAnnotation annotation = handlerMethod.getMethodAnnotation(CurrentAnnotation.class);
            if(annotation==null){
                return true;
            }
            int second = annotation.second();
            int maxcount = annotation.maxcount();
            String key =  request.getRequestURI();
            String name = (String) request.getSession().getAttribute("user");
           Object count =  redisTemplate.opsForValue().get(name + ":" + key);
           if(count.toString().equals("")){
               redisTemplate.opsForValue().set(name + ":" + key,1,second, TimeUnit.SECONDS);
           }else if((Integer)count>=maxcount){
               userContext.render(response, ResposeBeanCode.currentlimit());
               return false;
           }else{
               redisTemplate.opsForValue().increment(name + ":" + key);
               return true;
           }
       }
        return true;
    }
}

③ 将拦截器添加到WebMvcConfigurer组件定制化中

@Configuration
public class MyWebMvcCofig implements WebMvcConfigurer { //组件定制化

    @Autowired
    MyHandlerInterceptor myHandlerInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(myHandlerInterceptor);
    }
}

④ 将注解标注再接口上

@PostMapping("/{ran}/seckillorder")
@CurrentAnnotation(second = 100,maxcount = 5)
@ResponseBody
public ResposeBeanCode seckillorder(@RequestParam("goodsid") int goodsid,@PathVariable("ran") String ran,HttpSession httpSession){

    String user = (String) httpSession.getAttribute("user");

    Boolean checkoutinterfaceaddr = interfacehidden.checkoutinterfaceaddr(user, goodsid, ran);

    if(!checkoutinterfaceaddr){
        System.out.println("checkoutinterfaceaddr:"+checkoutinterfaceaddr);
        return ResposeBeanCode.error();
    }

    boolean seckillserver = seckillOrderService.seckillserver(goodsid, user);

    if(!seckillserver){
        return ResposeBeanCode.error();
    }
    return ResposeBeanCode.success();
}

路漫漫其修远兮,吾将上下而求索