接口防刷方案

1、前言

本文为描述通过Interceptor以及Redis实现接口访问防刷Demo

2、原理

  • 通过ip地址+uri拼接用以作为访问者访问接口区分

  • 通过在Interceptor中拦截请求,从Redis中统计用户访问接口次数从而达到接口防刷目的

如下图所示

3、案例工程

项目地址:

https://github.com/Tonciy/interface-brush-protection

Apifox地址:Apifox 密码:Lyh3j2Rv

其中,Interceptor处代码处理逻辑最为重要

/**  * @description: 接口防刷拦截处理  */  
@Slf4j  
public class AccessLimintInterceptor  implements HandlerInterceptor {  @Resource  private RedisTemplate<String, Object> redisTemplate;  /**  * 多长时间内  */  @Value("${interfaceAccess.second}")  private Long second = 10L;  /**  * 访问次数  */  @Value("${interfaceAccess.time}")  private Long time = 3L;  /**  * 禁用时长--单位/秒  */  @Value("${interfaceAccess.lockTime}")  private Long lockTime = 60L;  /**  * 锁住时的key前缀  */  public static final String LOCK_PREFIX = "LOCK";  /**  * 统计次数时的key前缀  */  public static final String COUNT_PREFIX = "COUNT";  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  String uri = request.getRequestURI();  String ip = request.getRemoteAddr(); // 这里忽略代理软件方式访问,默认直接访问,也就是获取得到的就是访问者真实ip地址  String lockKey = LOCK_PREFIX + ip + uri;  Object isLock = redisTemplate.opsForValue().get(lockKey);  if(Objects.isNull(isLock)){  // 还未被禁用  String countKey = COUNT_PREFIX + ip + uri;  Object count = redisTemplate.opsForValue().get(countKey);  if(Objects.isNull(count)){  // 首次访问  log.info("首次访问");  redisTemplate.opsForValue().set(countKey,1,second, TimeUnit.SECONDS);  }else{  // 此用户前一点时间就访问过该接口  if((Integer)count < time){  // 放行,访问次数 + 1  redisTemplate.opsForValue().increment(countKey);  }else{  log.info("{}禁用访问{}",ip, uri);  // 禁用  redisTemplate.opsForValue().set(lockKey, 1,lockTime, TimeUnit.SECONDS);  // 删除统计  redisTemplate.delete(countKey);  throw new CommonException(ResultCode.ACCESS_FREQUENT);  }  }  }else{  // 此用户访问此接口已被禁用  throw new CommonException(ResultCode.ACCESS_FREQUENT);  }  return true;  }  
}  

在多长时间内访问接口多少次,以及禁用的时长,则是通过与配置文件配合动态设置

当处于禁用时直接抛异常则是通过在ControllerAdvice处统一处理 (这里代码写的有点丑陋)

下面是一些测试(可以把项目通过Git还原到“【初始化】”状态进行测试)

  • 正常访问时

  • 访问次数过于频繁时

4、自我提问

上述实现就好像就已经达到了我们的接口防刷目的了

但是,还不够

为方便后续描述,项目中新增补充Controller,如下所示

简单来说就是

  • PassCotrollerRefuseController

  • 每个Controller分别有对应的get,post,put,delete类型的方法,其映射路径与方法名称一致

接口问题

  • 对于上述实现,不知道你们有没有发现一个问题

  • 对于上述实现问题比如现在我们的接口防刷处理,针对是所有的接口(项目案例中我只是写的接口比较少)

  • 而在实际开发中,说对于所有的接口都要做防刷处理,感觉上也不太可能(写此文时目前大四,实际工作经验较少,这里不敢肯定)

  • 那么问题有了,该如何解决呢?目前来说想到两个解决方案

拦截器映射规则

项目通过Git还原到"【Interceptor设置映射规则实现接口自由】"版本即可得到此案例实现

我们都知道拦截器是可以设置拦截规则的,从而达到拦截处理目的

1.这个AccessInterfaceInterceptor是专门用来进行防刷处理的,那么实际上我们可以通过设置它的映射规则去匹配需要进行【接口防刷】的接口即可

2.比如说下面的映射配置

3.这样就初步达到了我们的目的,通过映射规则的配置,只针对那些需要进行【接口防刷】的接口才会进行处理

4.至于为啥说是初步呢?下面我就说说目前我想到的使用这种方式进行【接口防刷】的不足点:

所有要进行防刷处理的接口统一都是配置成了 x 秒内 y 次访问次数,禁用时长为 z 秒

  • 要知道就是要进行防刷处理的接口,其 x, y, z的值也是并不一定会统一的

  • 某些防刷接口处理比较消耗性能的,我就把x, y, z设置的紧一点

  • 而某些防刷接口处理相对来说比较快,我就把x, y, z 设置的松一点

  • 这没问题吧

  • 但是现在呢?x, y, z值全都一致了,这就不行了

  • 这就是其中一个不足点

  • 当然,其实针对当前这种情况也有解决方案

  • 那就是弄多个拦截器

  • 每个拦截器的【接口防刷】处理逻辑跟上述一致,并去映射对应要处理的防刷接口

  • 唯一不同的就是在每个拦截器内部,去修改对应防刷接口需要的x, y, z值

  • 这样就是感觉会比较麻烦

防刷接口映射路径修改后维护问题

  • 虽然说防刷接口的映射路径基本上定下来后就不会改变

  • 但实际上前后端联调开发项目时,不会有那么严谨的Api文档给我们用(这个在实习中倒是碰到过,公司不是很大,开发起来也就不那么严谨,啥都要自己搞,功能能实现就好)

  • 也就是说还是会有那种要修改接口的映射路径需求

  • 当防刷接口数量特别多,后面的接手人员就很痛苦了

  • 就算是项目是自己从0到1实现的,其实有时候项目开发到后面,自己也会忘记自己前面是如何设计的

  • 而使用当前这种方式的话,谁维护谁蛋疼

自定义注解 + 反射

咋说呢

  • 就是通过自定义注解中定义 x 秒内 y 次访问次数,禁用时长为 z 秒

  • 自定义注解 + 在需要进行防刷处理的各个接口方法上

  • 在拦截器中通过反射获取到各个接口中的x, y, z值即可达到我们想要的接口自由目的

下面做个实现

声明自定义注解

Controlller中方法中使用

Interceptor处逻辑修改(最重要是通过反射判断此接口是否需要进行防刷处理,以及获取到x, y, z的值)

/**   * @description: 接口防刷拦截处理  */  
@Slf4j  
public class AccessLimintInterceptor  implements HandlerInterceptor {  @Resource  private RedisTemplate<String, Object> redisTemplate;  /**  * 锁住时的key前缀  */  public static final String LOCK_PREFIX = "LOCK";  /**  * 统计次数时的key前缀  */  public static final String COUNT_PREFIX = "COUNT";  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
//        自定义注解 + 反射 实现  // 判断访问的是否是接口方法  if(handler instanceof HandlerMethod){  // 访问的是接口方法,转化为待访问的目标方法对象  HandlerMethod targetMethod = (HandlerMethod) handler;  // 取出目标方法中的 AccessLimit 注解  AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);  // 判断此方法接口是否要进行防刷处理(方法上没有对应注解就代表不需要,不需要的话进行放行)  if(!Objects.isNull(accessLimit)){  // 需要进行防刷处理,接下来是处理逻辑  String ip = request.getRemoteAddr();  String uri = request.getRequestURI();  String lockKey = LOCK_PREFIX + ip + uri;  Object isLock = redisTemplate.opsForValue().get(lockKey);  // 判断此ip用户访问此接口是否已经被禁用  if (Objects.isNull(isLock)) {  // 还未被禁用  String countKey = COUNT_PREFIX + ip + uri;  Object count = redisTemplate.opsForValue().get(countKey);  long second = accessLimit.second();  long maxTime = accessLimit.maxTime();  if (Objects.isNull(count)) {  // 首次访问  log.info("首次访问");  redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);  } else {  // 此用户前一点时间就访问过该接口,且频率没超过设置  if ((Integer) count < maxTime) {  redisTemplate.opsForValue().increment(countKey);  } else {  log.info("{}禁用访问{}", ip, uri);  long forbiddenTime = accessLimit.forbiddenTime();  // 禁用  redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);  // 删除统计--已经禁用了就没必要存在了  redisTemplate.delete(countKey);  throw new CommonException(ResultCode.ACCESS_FREQUENT);  }  }  } else {  // 此用户访问此接口已被禁用  throw new CommonException(ResultCode.ACCESS_FREQUENT);  }  }  }  return  true;  }  
}  

由于不好演示效果,这里就不贴测试结果图片了

项目通过Git还原到"【自定义主键+反射实现接口自由"版本即可得到此案例实现,后面自己可以针对接口做下测试看看是否如同我所说的那样实现自定义x, y, z 的效果

嗯,现在看起来,可以针对每个要进行防刷处理的接口进行针对性自定义多长时间内的最大访问次数,以及禁用时长,哪个接口需要,就直接+在那个接口方法出即可

感觉还不错的样子,现在网上挺多资料也都是这样实现的

但是还是可以有改善的地方

先举一个例子,以我们的PassController为例,如下是其实现

下图是其映射路径关系

同一个Controller的所有接口方法映射路径的前缀都包含了/pass

我们在类上通过注解@ReqeustMapping标记映射路径/pass,这样所有的接口方法前缀都包含了/pass,并且以致于后面要修改映射路径前缀时只需改这一块地方即可

这也是我们使用SpringMVC最常见的用法

那么,我们的自定义注解也可不可以这样做呢?先无中生有个需求

假设PassController中所有接口都是要进行防刷处理的,并且他们的x, y, z值就一样

如果我们的自定义注解还是只能加载方法上的话,一个一个接口加,那么无疑这是一种很呆的做法

要改的话,其实也很简单,首先是修改自定义注解,让其可以作用在类上

接着就是修改AccessLimitInterceptor的处理逻辑

AccessLimitInterceptor中代码修改的有点多,主要逻辑如下

与之前实现比较,不同点在于x, y, z的值要首先尝试在目标类中获取

其次,一旦类中标有此注解,即代表此类下所有接口方法都要进行防刷处理

如果其接口方法同样也标有此注解,根据就近优先原则,以接口方法中的注解标明的值为准

/**  * @description: 接口防刷拦截处理  */  
@Slf4j  
public class AccessLimintInterceptor implements HandlerInterceptor {  @Resource  private RedisTemplate<String, Object> redisTemplate;  /**  * 锁住时的key前缀  */  public static final String LOCK_PREFIX = "LOCK";  /**  * 统计次数时的key前缀  */  public static final String COUNT_PREFIX = "COUNT";  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  //      自定义注解 + 反射 实现, 版本 2.0  if (handler instanceof HandlerMethod) {  // 访问的是接口方法,转化为待访问的目标方法对象  HandlerMethod targetMethod = (HandlerMethod) handler;  // 获取目标接口方法所在类的注解@AccessLimit  AccessLimit targetClassAnnotation = targetMethod.getMethod().getDeclaringClass().getAnnotation(AccessLimit.class);  // 特别注意不能采用下面这条语句来获取,因为 Spring 采用的代理方式来代理目标方法  //  也就是说targetMethod.getClass()获得是class org.springframework.web.method.HandlerMethod ,而不知我们真正想要的 Controller  
//            AccessLimit targetClassAnnotation = targetMethod.getClass().getAnnotation(AccessLimit.class);  // 定义标记位,标记此类是否加了@AccessLimit注解  boolean isBrushForAllInterface = false;  String ip = request.getRemoteAddr();  String uri = request.getRequestURI();  long second = 0L;  long maxTime = 0L;  long forbiddenTime = 0L;  if (!Objects.isNull(targetClassAnnotation)) {  log.info("目标接口方法所在类上有@AccessLimit注解");  isBrushForAllInterface = true;  second = targetClassAnnotation.second();  maxTime = targetClassAnnotation.maxTime();  forbiddenTime = targetClassAnnotation.forbiddenTime();  }  // 取出目标方法中的 AccessLimit 注解  AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);  // 判断此方法接口是否要进行防刷处理  if (!Objects.isNull(accessLimit)) {  // 需要进行防刷处理,接下来是处理逻辑  second = accessLimit.second();  maxTime = accessLimit.maxTime();  forbiddenTime = accessLimit.forbiddenTime();  if (isForbindden(second, maxTime, forbiddenTime, ip, uri)) {  throw new CommonException(ResultCode.ACCESS_FREQUENT);  }  } else {  // 目标接口方法处无@AccessLimit注解,但还要看看其类上是否加了(类上有加,代表针对此类下所有接口方法都要进行防刷处理)  if (isBrushForAllInterface && isForbindden(second, maxTime, forbiddenTime, ip, uri)) {  throw new CommonException(ResultCode.ACCESS_FREQUENT);  }  }  }  return true;  }  /**  * 判断某用户访问某接口是否已经被禁用/是否需要禁用  *  * @param second        多长时间  单位/秒  * @param maxTime       最大访问次数  * @param forbiddenTime 禁用时长 单位/秒  * @param ip            访问者ip地址  * @param uri           访问的uri  * @return ture为需要禁用  */  private boolean isForbindden(long second, long maxTime, long forbiddenTime, String ip, String uri) {  String lockKey = LOCK_PREFIX + ip + uri; //如果此ip访问此uri被禁用时的存在Redis中的 key  Object isLock = redisTemplate.opsForValue().get(lockKey);  // 判断此ip用户访问此接口是否已经被禁用  if (Objects.isNull(isLock)) {  // 还未被禁用  String countKey = COUNT_PREFIX + ip + uri;  Object count = redisTemplate.opsForValue().get(countKey);  if (Objects.isNull(count)) {  // 首次访问  log.info("首次访问");  redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);  } else {  // 此用户前一点时间就访问过该接口,且频率没超过设置  if ((Integer) count < maxTime) {  redisTemplate.opsForValue().increment(countKey);  } else {  log.info("{}禁用访问{}", ip, uri);  // 禁用  redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);  // 删除统计--已经禁用了就没必要存在了  redisTemplate.delete(countKey);  return true;  }  }  } else {  // 此用户访问此接口已被禁用  return true;  }  return false;  }  
}  

好了,这样就达到我们想要的效果了

项目通过Git还原到"【自定义注解+反射实现接口自由-版本2.0】"版本即可得到此案例实现,自己可以测试万一下

这是目前来说比较理想的做法,至于其他做法,暂时没啥了解到

5、时间逻辑漏洞

这是我一开始都有留意到的问题

也是一直搞不懂,就是我们现在的所有做法其实感觉都不是严格意义上的x秒内y次访问次数

特别注意这个x秒,它是连续,任意的(代表这个x秒时间片段其实是可以发生在任意一个时间轴上)

我下面尝试表达我的意思,但是我不知道能不能表达清楚

假设我们固定某个接口5秒内只能访问3次,以下面例子为例

底下的小圆圈代表此刻请求访问接口

按照我们之前所有做法的逻辑走

  1. 第2秒请求到,为首次访问,Redis中统计次数为1(过期时间为5秒)

  2. 第7秒,此时有两个动作,一是请求到,二是刚刚第二秒Redis存的值现在过期

  3. 我们先假设这一刻,请求处理完后,Redis存的值才过期

  4. 按照这样的逻辑走

  5. 第七秒请求到,Redis存在对应key,且不大于3, 次数+1

  6. 接着这个key立马过期

  7. 再继续往后走,第8秒又当做新的一个起始,就不往下说了,反正就是不会出现禁用的情况

按照上述逻辑走,实际上也就是说当出现首次访问时,当做这5秒时间片段的起始

第2秒是,第8秒也是

但是有没有想过,实际上这个5秒时间片段实际上是可以放置在时间轴上任意区域的

上述情况我们是根据请求的到来情况人为的把它放在【2-7】,【8-13】上

而实际上这5秒时间片段是可以放在任意区域的

那么,这样的话,【7-12】也可以放置

而【7-12】这段时间有4次请求,就达到了我们禁用的条件了

是不是感觉怪怪的

想过其他做法,但是好像严格意义上真的做不到我所说的那样(至少目前来说想不到)

之前我们的做法,正常来说也够用,至少说有达到防刷的作用

后面有机会的话再看看,不知道我是不是钻牛角尖了

6、路径参数问题

假设现在PassController中有如下接口方法

图片

也就是我们在接口方法中常用的在请求路径中获取参数的套路

但是使用路径参数的话,就会发生问题

那就是同一个ip地址访问此接口时,我携带的参数值不同

按照我们之前那种前缀+ip+uri拼接的形式作为key的话,其实是区分不了的

下图是访问此接口,携带不同参数值时获取的uri状况

这样的话在我们之前拦截器的处理逻辑中,会认为是此ip用户访问的是不同的接口方法,而实际上访问的是同一个接口方法

也就导致了【接口防刷】失效

接下来就是解决它,目前来说有两种

  1. 不要使用路径参数

这算是比较理想的做法,相当于没这个问题

但有一定局限性,有时候接手别的项目,或者自己根本没这个权限说不能使用路径参数

  1. 替换uri

  • 我们获取uri的目的,其实就是为了区别访问接口

  • 而把uri替换成另一种可以区分访问接口方法的标识即可

  • 最容易想到的就是通过反射获取到接口方法名称,使用接口方法名称替换成uri即可

  • 当然,其实不同的Controller中,其接口方法名称也有可能是相同的

  • 实际上可以再获取接口方法所在类类名,使用类名 + 方法名称替换uri即可

  • 实际解决方案有很多,看个人需求吧

  • 获取代码都是通过request.getRemoteAddr()获取的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/627744.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

localStorage、sessionStorage、vuex区别和使用感悟

一、介绍及区别 localStorage的生命周期是永久&#xff1b;不手动在浏览器提供的UI上清除localStorage信息&#xff0c;否则这些信息将永远存在。 sessionStorage的生命周期为当前窗口或标签页&#xff0c;一旦窗口或标签页被永久关闭&#xff0c;那么所有通过sessionStorage存…

AI红娘开启约会新时代;网易云音乐Agent实践探索;微软生成式AI课程要点笔记;ComfyUI新手教程;图解RAG进阶技术 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f440; Perplexity 官宣 7360 万美元B轮融资&#xff0c;打造世界上最快最准确的答案平台 https://blog.perplexity.ai/blog/perplexity-rais…

uniapp中uview组件库Toast 消息提示 的使用方法

目录 #基本使用 #配置toast主题 #toast结束跳转URL #API #Props #Params #Methods 此组件表现形式类似uni的uni.showToastAPI&#xff0c;但也有不同的地方&#xff0c;具体表现在&#xff1a; uView的toast有5种主题可选可以配置toast结束后&#xff0c;跳转相应URL目…

Linux系统——yum仓库及NFS共享

目录 一、yum仓库 1.yum简介 2.yum实现过程 3.如何实现安装服务 4.yum配置文件及命令 4.1yum配置文件 4.1.1主配置文件 4.1.2仓库设置文件 4.1.3日志文件 4.2yum命令详解 4.2.1查询 4.2.2yum安装升级 4.2.3软件卸载 4.2.4操作安装历史记录 5.搭建本地yum仓库 5…

【分布式技术】分布式存储ceph部署

目录 一、存储的介绍 单机存储设备 单机存储的问题 商业存储 分布式存储 二、分布式存储 什么是分布式存储 分布式存储的类型 三、ceph简介 四、ceph的优点 五、ceph的架构 六、ceph的核心组件 七、OSD存储后端 八、Ceph 数据的存储过程 九、Ceph 版本发行生命周…

NFS的共享与挂载

一、NFS网络文件服务 1.1简介 NFS&#xff08;Network File System 网络文件服务&#xff09; 文件系统&#xff08;软件&#xff09;文件的权限 NFS 是一种基于 TCP/IP 传输的网络文件系统协议&#xff0c;最初由 Sun 公司开发。 通过使用 NFS 协议&#xff0c;客户机可以像访…

【数据库8.0备份还原】之Percona XtraBackup

目录 Percona XtraBackup备份数据库1、Percona XtraBackup的介绍2、Percona XtraBackup安装3、Percona XtraBackup8.0的使用1.全库备份和还原2.增量备份和还原3.差异备份和还原4.差异备份和增量备份的区别5.压缩备份和还原 Percona XtraBackup备份数据库 yum源安装&#xff1a…

Spring基础属性一览:注释、对象装配、作用域、生命周期

在Spring中想要更简单的存储和读取对象的核心是使用注解&#xff0c;也就是我们接下来要学的Spring中相关注解。 之前我们存储Bean时&#xff0c;需要在自己添加的配置文件中添加一行bean才行&#xff1a; 而现在我们只需要一个注解就可以替代之前要写的一行配置的繁琐了。 …

消息队列的作用与使用场景?

一、消息队列的作用 队列的主要作用是消除高并发访问高峰&#xff0c;加快网站的响应速度。 在不使用消息队列的情况下&#xff0c;用户的请求数据直接写入数据库&#xff0c;在高并发的情况下&#xff0c;会对数据库造成巨大的压力&#xff0c;同时也使得系统响应延迟加剧。 …

HDFS WebHDFS 读写文件分析及HTTP Chunk Transfer Encoding相关问题探究

文章目录 前言需要回答的首要问题DataNode端基于Netty的WebHDFS Service的实现基于重定向的文件写入流程写入一个大文件时WebHDFS和Hadoop Native的块分布差异 基于重定向的数据读取流程尝试读取一个小文件尝试读取一个大文件 读写过程中的Chunk Transfer-Encoding支持写文件使…

postman 简单测试(二)

接着上一节 https://blog.csdn.net/myy2012/article/details/135616719 1.Tests的简单使用&#xff08;后置处理器&#xff09; 具体的截图是每一步操作后得来的&#xff0c;记录方便自己以后查阅&#xff0c;也希望能帮助到有缘人。 1.1 把返回值存入到环境变量中&#xff…

protobuf学习日记 | 初识protobuf

目录 前言 一、序列化与反序列化 二、protobuf是什么 三、protobuf的使用特点 四、快速上手 1、proto文件编写 2、编译proto文件 3、序列化与反序列化的使用 前言 这是小编新开的一个栏目&#xff0c;为了记录自己在学习ProtoBuf的历程&#xff0c;也希望能帮助大家&am…

亚马逊店飞飞ERP系统,跟卖+铺货+物流发货模式综合一体的ERP系统

跨境电商亚马逊&#xff0c;目前为止电商行业比较靠前的电商平台&#xff01;那么有人做电商&#xff0c;就会有人出单&#xff0c;有人出单就会有中转仓需求&#xff0c;代打包&#xff0c;代贴单&#xff01;那么这一切都是有一套逻辑完善的ERP来完成&#xff01;前端通过授权…

将.NET应用转换成Window服务

写在前面 本文介绍了将.NET8.0应用程序转换成Windows服务。 需要在NuGet中获取并安装&#xff1a;Microsoft.Extensions.Hosting.WindowsServices 包 代码实现 using System.Runtime.InteropServices; using WorkerService1;public class Program {public static void Main…

Kafka 简介

目录 1、概念介绍 Kafka 由来 ZooKeeper Kafka 特性 Kafka 使用场景 Kafka 复制备份 2、Kafka 架构 Broker Topic Producer Partition Consumers Consumer Group Distribution 1、概念介绍 Kafka 由来 Kafka 是最初由 Linkedin 公司开发&#xff0c;是一个分布…

aigc修复美颜学习笔记

目录 GFPGAN进行图像人脸修复 美颜 修复畸形手势 GFPGAN进行图像人脸修复 原文&#xff1a;本地使用GFPGAN进行图像人脸修复_人相修复处理网页 csdn-CSDN博客 人脸修复 1.下载项目和权重文件 2.部署环境 3.下载权重文件 4.运行代码 5.网页端体验 首先来看一下效果图 1.下…

uni-app的项目创建和环境搭建

uni-app 是一个使用 Vue.js 开发所有前端应用的框架&#xff0c;开发者编写一套代码&#xff0c;可发布到iOS、Android、Web&#xff08;响应式&#xff09;、以及各种小程序&#xff08;微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝&#xff09;、快应用等多个平台。 第一步…

clip安装使用教程

1.配置环境 安装依赖 pip install transformers pip install torch 看缺失什么包自己先安装好 2.安装clip 进入https://github.com/openai/CLIP&#xff0c;先将CLIP文件夹下载到本地&#xff0c;随便什么位置。即点击下图中的Download ZIP&#xff0c;下载到本地后进行解压…

HNU-编译原理-实验2-Bison

编译原理实验2Bison 计科210X 甘晴void 202108010XXX 实验要求 详细的实验项目文档为 https://gitee.com/coderwym/cminus_compiler-2023-fall/tree/master/Documentations/lab2 实验步骤 本次实验需要在 Lab1 已完成的 flex 词法分析器的基础上&#xff0c;进一步使用 b…