高并发系统设计 -- 登录系统设计

news2024/11/19 8:28:08

同源策略

  1. 同源策略是一种安全策略。是游览器最核心最基本的安全功能。
  2. 防止XSS,CSFR等攻击
  3. 具体表现是游览器在执行脚本之前,会判断脚本是否与打开的网页是同源的,也就是协议,域名,端口是否都相同,相同就是同源,其中一项不相同就叫做跨域访问
  4. 跨域访问会在控制台报一个CORS异常,目的是为了保护本地数据不被js代码获取回来的数据污染。因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被游览器接收。
  5. 在有明确授权的情况下,禁止页面加载或者执行与自身不同意的任何脚本。我们可以使用Nginx或者SpringCloud Gateway解决。

CSRF攻击

CSRF攻击原理

查看CSDN里面存放的相关的文章。

CSRF防御原理

检测CSRF漏洞

  1. 最简单的方法就是抓取一个正常请求的数据包,去掉Referer字段之后再重新提交,如果提交还有效,那么基本上可以确认存在CSRF漏洞。
  2. 以CSRFTester工具为例,它的测试原理如下:抓取游览器中访问过的所有链接以及所有的表单信息,然后通过CSRFTester中修改相应的表单等信息,重新提交,如果修改后的请求成功被网站服务器接收,说明存在CSRF漏洞。这个工具就是使用CSRF的攻击原理来进行检测的。

防御CSRF攻击

  1. 验证HTTP Referer字段

    简单便捷但是不完善

  2. 在请求地址中添加token并且认证

    **要抵御CSRF,关键在于请求中放入黑客所不能伪造的信息,并且该信息不存在于cookie之中。**例如,可以在HTTP请求中以参数的形式加入一个随机产生的token,如果请求中没有token或者token不存在,那么久认为是CSRF攻击而拒绝这个请求。

    这种方法比Referer要安全一些。

  3. HTTP头中自定义属性并验证

XSS攻击

XSS攻击又称为跨站脚本攻击,通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。

这些恶意网页程序通常是JavaScript,但实际上也可以包括Java、 VBScript、ActiveX、 Flash 或者甚至是普通的HTML。攻击成功后,攻击者可能得到包括但不限于更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。

跨站脚本攻击(XSS),是最普遍的Web应用安全漏洞。这类漏洞能够使得攻击者嵌入恶意脚本代码到正常用户会访问到的页面中,当正常用户访问该页面时,则可导致嵌入的恶意脚本代码的执行,从而达到恶意攻击用户的目的。

攻击者可以使用户在浏览器中执行其预定义的恶意脚本,其导致的危害可想而知,如劫持用户会话,插入恶意内容、重定向用户、使用恶意软件劫持用户浏览器、繁殖XSS蠕虫,甚至破坏网站、修改路由器配置信息等。

无状态和有状态

专业点的例子:

无状态:任意一个Web请求端提出请求时,请求本身包含了响应端为响应这一请求所需的全部信息(认证信息等)

有状态:Web请求端的请求必须被提交到保存有其相关状态信息(比如session)的服务器上,否则这些请求可能无法被理解,这也就意味着在此模式下服务器端无法对用户请求进行自由调度。

再说直白一点就是状态(公共交互)信息是由请求方还是响应方负责保存,请求方保存就是无状态,响应方保存就是有状态。

无状态应用不关心响应方是谁,需不需要同步各个响应方之间的信息,响应服务可随时被删除也不会影响别人,容错性高,分布式服务的负载均衡失效不会丢数据,无内存消耗,直接部署上线即可使用

有状态应用需要及时同步数据,可能存在数据同步不玩丢失数据,消耗内存资源保存数据等。

Token

在这里插入图片描述

那么应该怎么进行预防这种攻击那?目前主流的框架为了预防这种攻击,都是采用TOKEN机制。也就是说当用户与服务端进行交互的时候,传递一个加密字符串到服务端,服务端来检测这个字符串是否是合法的,如果不合法就有可能是黑客伪造用户信息进行请求的。

那么这个加密字符串是怎么生成的那?加密字符串是由后端程序生成,然后赋值到页面之上。一般是由当前控制器,方法,密钥,时间组合在一起加密而成。传递到服务端以后,服务端重新生成一遍,如果一致就是合法的,否则就是不合法的。

JWT

JWT = Json Web Token

通俗一点讲就是:

JWT是通过Json的形式作为Web应用中的令牌,用于在各方之间安全的将信息作为Json对象船速,在数据传输过程中还可以完成数据加密,签名等相关处理

JWT可以做什么?

  1. 授权,安全认证,这是JWT最常见的方案,一旦用户登录,每一个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一个功能,因为它的开销小,可以在不同的域中轻松使用。

在这里插入图片描述

例如我要访问淘宝购物车购买东西的页面,我要向你的服务器发起请求。当然这是肯定不可以的,因为你还没有进行登录。你进行登录之后,你可以用JWT标志生成一个token,以后每次访问的时候,在你的URL路径上面带着这个token,服务器就认为你是成功登录了的,就像是一个令牌一样,你把令牌给我看,我就让你执行对应的操作。当然,这个里面肯定设计到一些安全的问题,我们的服务器里面有一个secret密钥,而且只有服务器才有,所以不存在被盗用的情况,因为就算中间人获取到了你的token,也无能为力。

  1. 信息交换:是各方之间安全的传输信息的好方法,因为可以对JWT进行签名,例如使用公钥和私钥对,所以你可以确保发件人是他们所说的人。此外,由于签名是使用表头和有效载荷计算的,因此你还可以验证内容是否遭到了篡改。不常用。

数字签名

数字签名(又称公钥数字签名)是只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明。它是一种类似写在纸上的普通的物理签名,但是在使用了公钥加密领域的技术来实现的,用于鉴别数字信息的方法。一套数字签名通常定义两种互补的运算,一个用于签名,另一个用于验证。数字签名是非对称密钥加密技术与数字摘要技术的应用。

为什么是JWT?

基于传统的Session认证

在这里插入图片描述

先来看一下传统的Session认证的代码方式:

我们打开黑马的瑞吉外卖项目的UserController和Filter进行查看。

在这里插入图片描述

在我们的前后端分离的分布式架构中,有很多代理层(例如SpringCloud GateWay),我们如果用sessionId来验证的话,那么我们前端发过来请求之后,代理要拿到sessionId,拿到sessionId之后要把它传给我真正的服务,而sessionId在传递的过程中会耗费大量的资源。并且如果我们真正的服务是集群部署的话,那么还要实现sessionId的共享,把sessionId拷贝多份。

基于JWT认证

在这里插入图片描述

在这里插入图片描述

JWT和Token

JWT可以生成一个独一无二的Token字符串。

JWT适用于轻量级一点的,而Token + redis使用于重量级的系统。

如果项目并不大,JWT是个好选择,天然的去中心化模式,会话状态由令牌本身自解释,简单粗暴但是缺点页很明显,如题所示,一旦下发便不受服务端控制,如果发生token泄露,服务器也只能任其蹂躏,在其未过期期间不能有任何措施对此模式践行比较完善的框架推荐你了解一下token+redis,此框架扩展了token-session模型,将会话数据放在了Redis下,并提供了多种Session序列化方案,诸如注销登录、权限认证、踢人下线等常见功能全部一行代码就可以完成。

token是一个很宽泛的概念,翻译为令牌,一般用来表示经过验证之后得到的凭证,长度没有什么限制,多长都可以。

jwt是 JSON Web Token,它也自称是一种token,jwt就是一个很具体的标准了,用点号分为三段,分别表示头、信息和签名。

token有很多种,可以是标准的,也可以是你自己定义的,jwt则是其中一种token,而且是标准的token。和我们自己随意定义的token差别大是很自然的,因为我们自己定义的token只需要用来识别用户登录状态,一般很短的uuid都可以实现,所以比较短。

Token是无状态协议中认证用户的一种形式,相比于传统的cookie,不受域名限制
JWT只是一种实现形式,通过在客户端存储payload来降低服务端压力

在这里插入图片描述

为什么cookie不支持跨域访问?

在这里插入图片描述

JWT的结构是什么?

在这里插入图片描述

在这里插入图片描述

我用前面两个东西加上我的secret可以生成一个数字签名,然后等我用户给我发请求的那个token,把前面的取出来再加上secret,如果一样就代表合法。所以这个secret没有任何人可以知道。

在这里插入图片描述

在这里插入图片描述

JWT被盗用了怎么办?

在这里插入图片描述

密码盐值加密

把密码铭文存储到MySQL是不合理的,同时,使用MD5也是不合理的,因为虽然MD5是不可逆的,但是有人可以通过暴力的方式破解密码,所以这里相对好的方式是盐值加密,在MD5的基础上,加入一个盐值,同一个加密内容,盐值不同,加密出来的东西就不同,因此,只要破解方不知道我们的盐值,就几乎不可能破解成功。其次,使用手机验证码登录,可以从根本上解决这些问题。

风险控制

多设备登录校验

唯一token有效

  • 第一次登录的token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInVzZXJuYW1lIjoidG9tIiwiZXhwIjoxNjU3OTgzNzAxLCJpc3MiOiJnaW4tSU0iLCJuYmYiOjE2NTc4OTczMDF9.ipiIDgAdTwrv8EX45y0UD6wy0fOOdzhIDysyB8kJais
  • 第二次登录的token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInVzZXJuYW1lIjoidG9tIiwiZXhwIjoxNjU3OTgzODAxLCJpc3MiOiJnaW4tSU0iLCJuYmYiOjE2NTc4OTc0MDF9.3ZDrBr0FaFKpcicJpNkvEVCd8UdEQp079mg4fr2jBcc

通过测试可以发现,两个token都是有效的。只有到了指定的日期后,token才会失效。这显然是不合理的。所以我在这里的处理是:引入Redis

具体逻辑:使用redis中的hash结构。key为user的唯一标识uid;filed为该user的User-Agent,表示是哪一个设备(同一个设备只能有1个token有效);value存储该user的唯一有效token。

结构如下:

在这里插入图片描述

  • service/user.go
    

    中的

    UserLogin()
    

    增加逻辑:登录成功后签发token时,确定

    uid
    

    User-Agent
    

    。直接

    Rdb.HSet()
    

    即可

    • 因为Rdb.HSet()的处理逻辑是,如果存在就更新value;不存在就新建。
  • middleware/jwt.go中增加判定逻辑。通过uidUser-Agent(从解析token中包含的相关信息claims中获取uid),查出redis中的token。判定携带的token是否和redis中的token一样。如果不一样说明是旧的token,直接c.Abort()然后return

这只是我自己的一个想法,如果以后发现更好的解决方案,会继续更新的。

更新后:

  • 第一次token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInVzZXJuYW1lIjoidG9tIiwiZXhwIjoxNjU4MDU0NzUxLCJpc3MiOiJnaW4tSU0iLCJuYmYiOjE2NTc5NjgzNTF9.-FvhHHpJokeigiSJOUkTWaQ4ytsYDZcxaTklPLzJGR4
  • 第二次token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInVzZXJuYW1lIjoidG9tIiwiZXhwIjoxNjU4MDU0NzgwLCJpc3MiOiJnaW4tSU0iLCJuYmYiOjE2NTc5NjgzODB9.uBkmCpbTfEbr3fBiMQ26XrxOQc-hl6H5jvS_3BfW-2o

可以发现使用第一次token去请求会403:

img

据说微信就是这样做的(跟群友讨论的):

  • 这不就是提掉线吗?
  • 登录后 将以前此用户的token删除掉即可
  • 如果想多设备登录 就加入设备就可以 当前token和用户id,设备绑定
  • 微信就是这样做的

跟大家讨论,感觉基本都是基于redis缓存token的,踢掉用户也是这么干的。

不过感觉这样就跟jwt的无状态背道而驰了,回到了session。如果以后有更优雅更好的方式,会再记录的。

验证码接口防刷

验证码我们需要防刷的,后台也要做!!

有的人可能会问,为什么后台也要做呢?前端这边做一下限制不就可以了吗。我发送一次验证码值用户在一定的时间内就不可以继续发送了,这样不就实现了防刷了吗?但是实际上并不是这样的。因为前端虽然可以防住小白用户,但是有些很懂的人,可以抓包或者发送验证码的请求,然后用JMeter爆刷!!这样前端的限制就没有用了。

但是这个时候存在两个问题:

  • 我要的前台和后台都要去防止对面去恶意刷验证码。因为我们的请求的时候,发送验证码的请求已经被暴露了。对方懂行的人可以去用JMeter刷验证码的
  • 虽然我的前端限制验证码是过60秒之后才能继续刷的,但是我们刷新一次页面之后就会发现,这个限制没有了,因为我们没有后台记录所剩下的时间,我只需要在游览器上面刷新游览器的缓存,那么保存在游览器里面的时间就被刷新了,等于说这个东西是无意义的

我们接下来要解决这两个问题。

首先验证码可以存到redis里面,并且设置过期时间。

解决页面刷新之后验证码就可以马上发送的情况:

密码多次错误惩戒

跟验证码是做法是一样的,这里不做过多的展示。

核心代码

// JWTAuth 定义一个JWTAuth中间件
func JWTAuth() gin.HandlerFunc {
   return func(c *gin.Context) {
      // 通过http header中的token解析来认证
      token := c.GetHeader("token")
      if token == "" {
         c.JSON(http.StatusForbidden, gin.H{
            "status": http.StatusForbidden,
            "msg":    "请求未携带token,无权访问",
         })
         // 在被调用的函数中阻止后续中间件的执行
         c.Abort()
         return
      }
      // 解析token中包含的相关信息(有效载荷)
      claims, err := util.ParseToken(token)
      if err != nil {
         c.JSON(http.StatusForbidden, gin.H{
            "status": http.StatusForbidden,
            "msg":    "token解析失败",
            "error":  err.Error(),
         })
         c.Abort()
         return
      }
      // 判断该token是不是最新的token(从redis里面查)
      ua := c.GetHeader("User-Agent")
      val, err := dao2.Rdb.HGet(dao2.RCtx, strconv.Itoa(int(claims.Uid)), ua).Result()
      if err != nil {
         // 说明该token是其他User-Agent的token(你如说电脑端的token,当然不能用来登录手机端)
         c.JSON(http.StatusForbidden, gin.H{
            "status": http.StatusForbidden,
            "msg":    "token所属的User-Agent不匹配",
            "error":  err.Error(),
         })
         c.Abort()
         return
      }
      if token != val {
         // 请求携带的token与redis中存储的token不一致,说明是旧的token
         c.JSON(http.StatusForbidden, gin.H{
            "status": http.StatusForbidden,
            "msg":    "token失效!",
         })
         c.Abort()
         return
      }
      // 处理过期token
      if time.Now().Unix() > claims.ExpiresAt {
         c.JSON(http.StatusForbidden, gin.H{
            "status": http.StatusForbidden,
            "msg":    "token已经过期了", // token过期就得重新登录的
         })
         c.Abort()
         return
      }
   }
}
func (AuthService) GetCode(ctx context.Context, req *GetCodeRequest) (*GetCodeResponse, error) {
   redisCode, err := dao.Rdb.Get(dao.RCtx, common.AuthCode+req.Phone).Result()
   if !errors.Is(err, redis.Nil) {
      // 如果没有这个错误的话,就说明我的redis里面已经有验证码了
      // 我做出相应的处理,避免接口被刷
      split := strings.Split(redisCode, "_")
      s := split[1]
      redistime, _ := strconv.ParseInt(s, 10, 64)
      if time.Now().UnixNano()-redistime < 60*1000 {
         // 60s
         return &GetCodeResponse{Code: common.CodeRepeat}, nil
      }
   }
   // 我的redis里面没有验证码,那么此时就要往redis里面加入验证码了
   phone := req.Phone
   // 生成验证码
   code := util.Code()
   // 把验证码保存到redis里面去
   // 设计5分钟的过期时间
   dao.Rdb.Set(dao.RCtx, common.AuthCode+phone, code, time.Minute*5)
   // TODO 整合阿里云的短信服务
   return &GetCodeResponse{
      Code: http.StatusOK,
   }, nil
}

func (AuthService) Login(ctx context.Context, req *LoginRequest) (*Response, error) {
   var user model.User
   result := dao.MysqlDB.Where(&model.User{UserName: req.Username}).First(&user)
   err := result.Error
   if err != nil {
      if errors.Is(err, gorm.ErrRecordNotFound) {
         // 如果数据库中没有找到记录
         return &Response{
            Code: common.UserNotExist,
            Msg:  "用户不存在,请先注册",
         }, errors.New("该用户不存在")
      }
      // 不是用户不存在却还是继续出错,就说明是其他不可抗拒的因素
      return &Response{
         Code: http.StatusInternalServerError,
         Msg:  "查询数据库出现错误",
      }, nil
   }
   // 用户从数据库中找到了,检验密码
   ok, err := user.CheckPassword(req.Password)
   if err != nil {
      return &Response{
         Code: http.StatusInternalServerError,
         Msg:  "登录失败",
      }, nil
   }
   if !ok {
      return &Response{
         Code: common.PasswordErr,
         Msg:  "密码错误,登录失败",
      }, nil
   }
   // 登录成功要分发token(其他功能需要身份验证,给前端存储的)
   token, err := util.GenerateToken(uint(user.ID), user.UserName)
   if err != nil {
      return &Response{
         Code: http.StatusInternalServerError,
         Msg:  "token签发失败",
      }, nil
   }
   // 签发token后,存储到redis中
   m := map[string]string{req.UserAgent: token}
   dao.Rdb.HSet(dao.RCtx, common.AuthToken+strconv.FormatUint(uint64(user.ID), 10), m)
   return &Response{
      Code:  http.StatusOK,
      Msg:   "登录成功",
      Token: token,
   }, nil
}

func (AuthService) Register(ctx context.Context, rep *RegisterRequest) (*Response, error) {
   var user model.User
   var count int64
   dao.MysqlDB.Where(&model.User{
      UserName: rep.Username,
   }).First(&user).Count(&count)
   if count == 1 {
      return &Response{
         Code: common.UserHaveBeenRegister,
         Msg:  "用户已经注册过了",
      }, nil
   }
   // 如果数据库中没有该用户,就开始注册
   user.UserName = rep.Username
   worker, _ := util.NewWorker(0)
   id := worker.GetId()
   user.ID = id
   err := user.SetPassword(rep.Password)
   if err != nil {
      return &Response{
         Code:  http.StatusInternalServerError,
         Msg:   "数据库插入错误",
         Token: "",
      }, nil
   }
   // 加密成功就可以创建用户了
   err = dao.MysqlDB.Create(&user).Error
   if err != nil {
      return &Response{
         Code:  http.StatusInternalServerError,
         Msg:   "数据库添加数据出错",
         Token: "",
      }, nil
   }
   return &Response{
      Code: http.StatusOK,
      Msg:  "用户注册成功",
   }, nil
}

func (AuthService) Phone(ctx context.Context, req *PhoneRequest) (*Response, error) {
   phone := req.Phone
   userAgent := req.UserAgent
   code := req.Code // 验证码
   // 我们首先验证验证码,如果验证码都错误了,那么后续的工作就是免谈的
   result, err := dao.Rdb.Get(dao.RCtx, common.AuthCode+phone).Result()
   if errors.Is(err, redis.Nil) {
      // 说明没有所谓的验证码
      return &Response{
         Code: http.StatusForbidden,
         Msg:  "请输入验证码",
      }, nil
   }
   // 到这里就说明是有验证码的
   if result != code {
      // 如果验证码错误的话
      return &Response{
         Code: common.CodeErr,
         Msg:  "验证码错误",
      }, nil
   }
   var user model.User
   // 到这里说明验证码是没有任何问题的,我们就开始验证此用户是否存在
   err = dao.MysqlDB.Where(&model.User{Phone: phone}).First(&user).Error
   if err != nil {
      if errors.Is(err, gorm.ErrRecordNotFound) {
         // 说明这个用户不存在,我们给它注册一个新账号
         worker, _ := util.NewWorker(0)
         id := worker.GetId()
         user.ID = id
         user.Phone = phone
         user.UserName = common.UserNamePrefix + phone // 这里就直接把用户名随机的进行定义
         err = dao.MysqlDB.Create(&user).Error
         if err != nil {
            return &Response{
               Code: http.StatusInternalServerError,
               Msg:  "数据库添加数据出错",
            }, nil
         }
      }
      return &Response{
         Code: http.StatusInternalServerError,
         Msg:  "查询数据库出现错误",
      }, nil
   }
   // 登录成功,分发token
   token, err := util.GenerateToken(uint(user.ID), user.UserName)
   if err != nil {
      return &Response{
         Code: http.StatusInternalServerError,
         Msg:  "token签发失败",
      }, nil
   }
   // 签发token之后存储到redis中
   m := map[string]string{userAgent: token}
   dao.Rdb.HSet(dao.RCtx, strconv.FormatUint(uint64(user.ID), 10), m)
   // 到这里的时候,一定是已经存在账户了,那么我们就直接登录成功就可以了
   // 注意,这里需要删除验证码!否则我们的用户登录之后马上退出登录,然后用同一个验证码是可以成功的
   dao.Rdb.Del(dao.RCtx, common.AuthCode+phone)
   return &Response{
      Code: http.StatusOK,
      Msg:  "登录成功",
   }, nil
}

func (AuthService) Logout(ctx context.Context, req *LogoutRequest) (*LogoutResponse, error) {
   // 把redis里面的数据删除就可以了,其他的前端会给我们解决的
   userAgent := req.UserAgent
   // 我们从JWT里面来解析用户的ID
   token := req.Token
   parseToken, err := util.ParseToken(token)
   if err != nil {
      log.Println("token解析失败")
   }
   uid := parseToken.Uid
   err = dao.Rdb.HDel(dao.RCtx, common.AuthToken+strconv.Itoa(int(uid)), userAgent).Err()
   if err != nil {
      log.Println(err)
   }
   return &LogoutResponse{Code: http.StatusOK}, nil
}

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

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

相关文章

记录--前端性能监控初步实战

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 前言 在当下前后端分离的主流环境下&#xff0c;前端部分的优化变得越来越重要。为了提升前端的性能和用户体验&#xff0c;我觉得可能需要从三个维度采集数据进行分析。 前端埋点。通过埋点收集和统计…

SCRM与CRM的区别

当私域流量的概念兴起时&#xff0c;企业直接触达用户的场景也越来越丰富&#xff0c;SCRM形式的私域运营已然成为很多企业数字化转型布局的关键阵地。 前言 当私域流量的概念兴起时&#xff0c;企业直接触达用户的场景也越来越丰富&#xff0c;SCRM形式的私域运营已然成为很多…

服务器部署所有前后端分离项目

1、Linux服务器安装好jdk、mysql、redis、node 2、拉取最新代码 gitee仓库项目地址&#xff1a;https://gitee.com/y_project/RuoYi-Vue 拉取代码到本地 3、修改后端配置 3.1、修改系统内上传文件位置的配置&#xff1a; 默认是windows系统的配置&#xff0c;将此配置修改…

JMeter接口测试使用教程哪里有?

JMeter接口测试使用教程哪里有&#xff1f;接口测试是测试工程师的必备技能之一&#xff0c;运用JMeter工具一步步实现接口请求&#xff0c;数据参数化&#xff0c;断言等操作&#xff0c;通过常见的企业实际测试场景详解JMeter各项技术使用&#xff0c;最后结合Jenkins持续集成…

Django搭建个人博客Blog-Day01

创建虚拟环境虚拟环境相当于一个抽屉&#xff0c;在这个抽屉里面安装的任何软件&#xff0c;都不会影响到其他抽屉&#xff0c;所以利用虚拟环境就可以做到同时安装不同版本的第三方库&#xff0c;而互不影响。使用虚拟环境的目的是为了防止一些第三方库出现版本不兼容问题&…

c++11 标准模板(STL)(std::forward_list)(五)

定义于头文件 <forward_list> template< class T, class Allocator std::allocator<T> > class forward_list;(1)(C11 起)namespace pmr { template <class T> using forward_list std::forward_list<T, std::pmr::polymorphic_…

当我们在谈论DataOps时,我们到底在谈论什么

1. DataOps到底是什么&#xff1f; 伴随着全球数字化转型的高速发展&#xff0c;在云计算、物联网、5G、边缘计算、元宇宙等新技术的驱动下&#xff0c;数据爆炸的时代已经来临。IDC Global DataSphere显示&#xff0c;2021年&#xff0c;全球数据总量达到了84.5ZB&#xff0c…

Java多线程案例——阻塞队列(生产者消费者模型)

一&#xff0c;阻塞队列1.阻塞队列的概念和作用阻塞队列同数据结构中的队列一样都遵守“先进先出”的原则&#xff08;不了解队列相关知识的朋友请查看之前队列的博文&#xff1a;(6条消息) 栈和队列&#xff08;内附模拟实现代码&#xff09;_徐憨憨&#xff01;的博客-CSDN博…

功率放大模块如何选择(安泰功率放大器模块产品介绍)

功率放大器模块系列产品介绍 一、功率放大模块介绍 功率放大模块&#xff1a; 功率放大模块具有体积小&#xff0c;集成度高&#xff0c;使用方便&#xff0c;应用广泛等优点&#xff0c;凭借着输出频率广、输出电压高、输出功率大等特性&#xff0c;能够广泛应用在各种领域…

动态范围控制原理

DRC介绍 开门见山&#xff0c;动态范围的定义就是信号的最大幅值和最小幅值比值的对数(单位dB)&#xff0c; 动态范围会受到系统中各个环节的影响。例如同样是这段音乐&#xff0c;在一个40dB背景噪声的环境中播放&#xff0c;那么由于掩蔽效应等因素的影响&#xff0c;最终实际…

前端跳转第三方网页中间页

前端跳转安全提示 掘金跳转中间页背景介绍跳转过渡页的优点实现原理解析哈喽啊小伙伴们久等了 消失了有半年了 &#xff0c;因为个人工作原因没腾出时间给大家分享日常踩坑和特殊功能的讲解。不过这次我回来了就要好好分享了背景介绍 前端小伙伴一定知道CSDN 和 稀土掘金 两大…

Dev-C++下载安装详细教程

文章目录前言一、下载Dev-C二、安装Dev-C三、使用Dev-C打印HelloWorld总结前言 本文总结了关于Dev-C下载与安装的详细过程&#xff0c;并使用Dev-C打印了“Hello World!”。本篇博客面向C语言初学者&#xff0c;或者考研复试的学生使用&#xff0c;因为大部分学校的考研复试都使…

为什么 TCP 建立连接需要三次握手

TCP 协议是我们几乎每天都会接触到的网络协议&#xff0c;绝大多数网络连接的建立都是基于 TCP 协议的&#xff0c;学过计算机网络或者对 TCP 协议稍有了解的人都知道 —— 使用 TCP 协议建立连接需要经过三次握手&#xff08;three-way handshake&#xff09;。 如果让我们简…

多线程案例-线程池

1.什么是线程池线程存在的意义是当使用进程进行并发编程太重了,此时引入了一个"轻量级的"进程-线程.创建线程比创建进程更高效,销毁线程比销毁进程更高效,调度线程比调度进程更高效..此时我们就用多线程来代替进程进行并发编程了,但是随着对性能的要求的提高,线程相对…

大数据必学Java基础(一百一十八):什么是Maven和它的下载整合

文章目录 什么是Maven和它的下载整合 一、什么是Maven 二、IDEA默认整合了Maven 三、下载地址

【Linux】RHEL8 中nmcli使用,必备!

redhat8中nmcli日常使用 第 2 章 配置以太网连接 Red Hat Enterprise Linux 为管理员提供不同的选项来配置以太网连接。例如&#xff1a; 在命令行中使用 nmcli 配置连接。使用 nmtui 在基于文本的用户界面中配置连接。使用 RHEL 系统角色在一个或多个主机上自动配置连接。使…

电脑小问题:定时关机的设置

设置定时关机生活中&#xff0c;我们有时候需要对电脑进行定时关机。那么&#xff0c;如何设置定时关机呢&#xff1f;步骤如下&#xff1a; 1. 按 win R &#xff0c;弹出命令窗口&#xff0c;输入 taskschd.msc &#xff0c;点击确定。 2. 弹出任务计划程序窗口&#xff0c;…

使用ResNet34实现CIFAR10数据集的训练

如果对你有用的话&#xff0c;希望能够点赞支持一下&#xff0c;这样我就能有更多的动力更新更多的学习笔记了。&#x1f604;&#x1f604; 使用ResNet进行CIFAR-10数据集进行测试&#xff0c;这里使用的是将CIFAR-10数据集的分辨率扩大到32X32&#xff0c;因为算力相关的…

摘要/哈希/散列算法MD5 SHA1 SHA256 SHA512的区别和MAC算法

一、摘要算法大致都要经过以下步骤 1. 明文数据预处理 1.1 填充比特 MD5、SHA1、SHA256 的分组长度都是512bit 需要填充比特使其位长对512求余的结果等于448 SHA512 的分组长度是 1024bit 需要填充比特使其对1024求余的结果等于896 相同&am…

ECharts基本使用

文章目录Echarts概述Echarts初体验ECharts基础配置Echarts社区介绍Echarts-map使用Echarts概述 常见的数据可视化库&#xff1a; D3.js 目前 Web 端评价最高的 Javascript 可视化工具库(入手难)ECharts.js 百度出品的一个开源 Javascript 数据可视化库Highcharts.js 国外的前…