gin投票项目4

2023-12-13 03:55:51

对应视频v2版本

gin项目投票系统4

1.增加一个注册账号的功能

增加接口

参数校验:(需求)

  1. 确认密码需要一致,不为空
  2. 用户名必须唯一, 不为空
  3. 用户名大于8小于16位
  4. 密码大于8小于16位,并且不能为纯数字
正则表达式

必须知道这东西的存在!!!和知道怎么用,不一定能自己写出来,可以靠软件或gpt但一定要了解用法。

要增加注册账号功能需要在login.go中新建一个结构体

// 新创建一个结构体
type CUser struct {
    Name      string `json:"name"`
    Password  string `json:"password"`
    Password2 string `json:"password_2"`
}

因为账号密码是隐私信息,不敢随便放到请求中

先写处理用户注册的各个逻辑

//判断两次输入密码是否相同
if user.Password != user.Password2 {
    context.JSON(200, tools.ECode{
       Code:    10003,
       Message: "两次密码不同",
    })
    return
}
//会有风险,并发安全,判断用户是否已经存在
if oldUser := model.GetUser(user.Name); oldUser.Id > 0 {
    context.JSON(200, tools.ECode{
       Code:    10004,
       Message: "用户名已存在!",
    })
    return
}

//判断账号密码是否符合长度要求。
nameLen := len(user.Name)
passwordLen := len(user.Password)
if nameLen > 16 || nameLen < 8 || passwordLen > 16 || passwordLen < 8 {
    context.JSON(200, tools.ECode{
       Code:    10005,
       Message: "账号或密码要大于8位小于16位!",
    })
    return
}

正则最好用自动生成正则的工具或者gpt来实现;

这里需要正则来表示:密码中至少包括数字,小写字母,大写字母;

regex := regexp.MustCompile(`^[0-9]+$`)
if regex.MatchString(user.Password) {
    context.JSON(http.StatusOK, tools.ECode{
       Code:    10006,
       Message: "密码不能为纯数字", // 这里有风险
    })
    return
}

下一步

增加密码加密

为什么要加密?防止被黑客sql注入,如果明文存储,整个数据库就会被盗走;

介绍三种

// 数据加密:第一种方式,直接用md5加密,很容易被装库试出来
func encrypt(pwd string) string {
    hash := md5.New()
    hash.Write([]byte(pwd))
    hashBytes := hash.Sum(nil)

    hashString := hex.EncodeToString(hashBytes)
    fmt.Printf("加密后的密码: %s\n", hashString)
    return hashString
}

// 数据加密:第二种方式,在第一种基础上进行操作,具体来说是把传过来的密码使用
func encryptV1(pwd string) string {
    secretString := "香香编程喵喵喵"
    newPwd := pwd + secretString // Concatenate the secret string to the password
    hash := md5.New()
    hash.Write([]byte(newPwd))
    hashBytes := hash.Sum(nil)
    hashString := hex.EncodeToString(hashBytes)
    fmt.Printf("加密后的密码: %s\n", hashString)
    return hashString
}

//第三种方式加密。使用不同的加密方式:
func encryptV2(pwd string) string {
    // 基于Blowfish实现加密。简单快速,但有安全风险
    // golang.org/x/crypto/中有大量的加密算法
    newPwd, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
    if err != nil {
       fmt.Println("密码加密失败:", err)
       return ""
    }
    newPwdStr := string(newPwd)
    fmt.Printf("加密后的密码: %s\n", newPwdStr)
    return newPwdStr
}

2.增加验证码功能

使用base64实现将2进制文件转化成图片;

package tools
import (
    "github.com/mojocn/base64Captcha"
)
type CaptchaData struct {
    CaptchaId string `json:"captcha_id"`
    Data      string `json:"data"`
}
type driverString struct {
    Id            string
    CaptchaType   string
    VerifyValue   string
    DriverString  *base64Captcha.DriverString  // 字符串
    DriverChinese *base64Captcha.DriverChinese // 中文
    DriverMath    *base64Captcha.DriverMath    // 数学
    DriverDigit   *base64Captcha.DriverDigit   // 数字
}
// 数字驱动
var digitDriver = base64Captcha.DriverDigit{
    Height:   50,  // 生成图片高度
    Width:    150, // 生成图片宽度
    Length:   5,   // 验证码长度
    MaxSkew:  1,   // 文字的倾斜度越大倾斜越狠,越不容易看懂
    DotCount: 1,   // 背景的点数,越大,字体越模糊
}
var store = base64Captcha.DefaultMemStore
func CaptchaGenerate() (CaptchaData, error) {
    var ret CaptchaData
    // 注意,这里直接使用digitDriver会报错。必须传一个指针。原因参考接口实现课程中的内容
    c := base64Captcha.NewCaptcha(&digitDriver, store)
    id, b64s, err := c.Generate()
    if err != nil {
       return ret, err
    }
    ret.CaptchaId = id
    ret.Data = b64s
    return ret, nil
}
func CaptchaVerify(data CaptchaData) bool {
    return store.Verify(data.CaptchaId, data.Data, true)
}
 
// Other driver initialization here...
为登陆接口添加验证码接口

1.验证码的作用:判断用户行为是脚本还是人工

2.常见验证码有哪些:图片验证码,拖动验证码,按照顺序点击,9张图里选择有红绿灯的,邮箱验证码,手机验证码

3.调用的验证码包是怎么工作的:a.验证码如何生成:其实里边是使用了go的math包,三角函数

b.生成的验证码是以什么形式存在的:用bas64直接存在内存中,

c.base64传给前端,渲染到页面上,

d.输入验证码,以及验证码的id,服务端进行校验

注意这个验证码接口存在很严重的安全隐患

当点击验证码时,可以重新生成新的验证码。

DDOS攻击和CC攻击。

常见的后端攻击手段:SQL注入,CSRF/XSS(伪造跨站攻击),DDOS,CC攻击

方法:WAF花钱,买一个高防服务器;

3.增加校验,防止刷票

增加是否投票的校验,防止刷票

第一种方式,在事务中查询是否投过票,增加了事务的逻辑,成本非常高

var oldVoteUser VoteOptUser
err = tx.Table("vote_opt_user").Where("vote_id=? ans", voteId).First(&oldVoteUser).Error
if err != nil {
    fmt.Printf("err:%s")
    tx.Rollback()
}
if oldVoteUser.Id > 0 {
    fmt.Printf("用户已投票")
    tx.Rollback()
}

第二种方式,前置查询,直接先查询一下是否投过,如果投过直接返回

old := model.GetVoteHistory(userId, voteId)
if len(old) >= 1 {
    context.JSON(200, tools.ECode{
       Code:    10010,
       Message: "您已投过票",
    })
}

当同时有一个人同时发起100个请求投票,会出现重复投票现象吗?

以上两种方式都无法完美解决这个问题,肯定会出现

需要学习,悲观锁,乐观锁,分布式锁(较好用)。

最好用的是消息队列,很重要,以后学到。

4.增加一个定时器,到期自动关闭

多种定时器,不同的能解决不同的问题。

以后是需要学习不同环境中定时器的作用的

具体代码还是比较重要的,具体实现的功能

package schedule

//增加定时器功能
import (
    "fmt"
    "time"
    "toupiao/application/model"
)

func Start() {

    //Start 函数启动一个 goroutine,在这个 goroutine中调用了 voteEnd 函数。
    go func() { //使用 go 关键字创建 goroutine 表示这个函数是异步执行的,不会阻塞当前程序的执行。
       EndVote()
    }()
    return
}

func EndVote() {
    t := time.NewTicker(5 * time.Second)
    //每秒触发一次
    defer t.Stop() //最后运行关闭定时器

    for {
       select { //监听定时器的触发事件,
       case <-t.C:
          fmt.Printf("定时器voteEnd启动")
          //执行函数
          model.EndVote()
          fmt.Println("EndVote 运行完毕")
       }
    }
}

设定时间5秒,即5秒后自动关闭投表

引入日志包

官方包

log.Printf("[print]ret:%+v", ret)
log.Panicf("[fatal]ret:%+v", ret)

很不好用,除非花时间二次封装

logrus

日志级别

PanicLevel:记录日志,panic
FatalLevel:记录日志,程序exit

线上: (
ErrorLevel:错误级别日志
WarnLevel:警告级别日志     ) 

Infolevel:关键信 息级别日志

开发时通常用:(
DebugLevel:调试级别
TraceLevel:追踪级别       )

测试:

新建tools中logger文件:

package tools

import (
    "fmt"
    "github.com/sirupsen/logrus"
    "io"
    "os"
)
var Logger *logrus.Logger
func NewLogger() {
    Logger = logrus.New()
    Logger.SetLevel(logrus.DebugLevel)
    // 同时写到多个输出
    w1 := os.Stdout // 写到控制台
    w2, err := os.OpenFile("./vote.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    //写到文件中;
    if err != nil {
       // 错误处理,例如打印错误并退出程序
       fmt.Println("Error opening log file:", err)
       os.Exit(1)
    }
    Logger.SetOutput(io.MultiWriter(w1, w2)) // io.MultiWriter 返回一个 io.Writer 对象

}

效果:
image-20231128162101149

可在控制台和文件中都生成日志;

拓展:日志可以生成不同类型的:

比如JSON,在日志中再添加字段

logStore.SetFormatter(&logrus.JSONFormatter{}) 
//生成json格式的日志
Logger = logStore.WithFields(logrus.Fields{
    "name": "香香编程喵喵喵",
    "app":  "voteV2",
})//添加两个字段

image-20231128164443846

Hook函数(钩子函数)是什么:

Hook 函数是一种编程技术,其作用是在特定事件发生时插入自定义的代码,以便执行额外的逻辑或修改程序的行为。这样的插入点通常称为 “hook points”,而插入自定义代码的函数就是 “hook 函数”。

主要作用和用途包括:

  1. 扩展功能: Hook 函数可以用于在程序运行时动态地添加或修改功能。通过在特定事件上挂钩,可以在不修改源代码的情况下扩展应用程序的行为。
  2. 调试和日志: Hook 函数常常用于记录日志、跟踪程序执行流程,或在特定条件下触发调试信息。这对于排查问题、性能优化以及了解程序行为非常有用。
  3. 事件通知: Hook 函数还可以用于向其他部分发送通知,让它们在特定事件发生时执行相应的操作。这种方式用于实现观察者模式等。
  4. 修改数据: 有时 Hook 函数也用于修改或过滤数据。例如,在数据保存到数据库之前执行某些处理。
  5. 安全性: 在安全领域,Hook 函数可以用于拦截和处理潜在的安全威胁,比如输入验证或访问控制。

在实际编程中,Hook 函数通常通过回调函数或事件监听机制来实现。编程框架和库通常提供了一些预定义的 hook points,同时也允许开发者定义自己的 hook points。

ZAP

logruszap 都是 Go 语言中流行的日志库,它们各自有自己的特点和适用场景。下面是对它们的一些比较:

logrus:

  • 易用性: logrus 相对于 zap 来说,更容易上手,其 API 设计更为简单直观。
  • 社区支持: 由于 logrus 的存在时间较长,因此在社区中有更多的用户和资源,相应的社区支持更丰富。
  • 扩展性: logrus 支持很多的插件和扩展,可以很容易地集成到各种不同的环境和系统中。
  • 灵活性: 可以通过 Hook 的方式灵活扩展 logrus 的功能。

zap:

  • 性能: zap 以高性能为目标,被设计成尽可能地快,具有更低的内存分配和更高的吞吐量。适用于高并发和性能敏感的应用。
  • 结构化日志: zap 倡导结构化日志,即将日志信息存储为结构体,使日志更容易分析和查询。
  • 零分配: zap 设计了零分配(zero-allocation)的原则,以减少垃圾回收的影响。
  • Sugar API: zap 提供了一个 Sugar API,用于简化常用日志操作的调用。

如何选择:

  • 如果你更看重易用性、社区支持和扩展性,可以选择 logrus
  • 如果你的应用对性能要求很高,且你希望采用结构化日志,那么 zap 可能更适合。

最终的选择取决于你的具体需求和项目的特点。如果不确定,可以先使用其中一个,后期根据实际情况进行评估和切换。

日志既然这么麻烦又为什么要引入日志呢?

  1. 故障排查和调试: 在应用程序出现问题时,日志是排查错误和调试的关键工具。通过查看日志,开发人员可以追踪代码的执行路径、发现异常行为,并更容易地定位问题。
  2. 性能监控: 日志也是性能监控的一部分。通过记录关键操作的执行时间、资源使用情况等信息,开发人员可以了解应用程序的性能状况,并进行优化。
  3. 安全审计: 在一些敏感的系统中,日志记录也用于安全审计。记录用户的操作、访问尝试以及其他安全相关事件,以便进行审计和追踪。
  4. 业务分析: 日志还可以用于业务分析。通过记录用户的行为、交易记录等信息,可以为业务决策提供数据支持。
  5. 运维监控: 运维团队通过监控日志可以实时掌握系统运行状态,及时发现和处理潜在问题。
  6. 历史记录: 日志可以作为系统的历史记录,帮助了解系统的演变和变更历史。

虽然引入日志可能增加了代码的复杂性,但日志对于维护和监控大型应用是至关重要的。它为开发者提供了一种实时的、非侵入式的了解应用运行状况的手段。在生产环境中,日志往往是排查问题的最主要工具之一。

将验证码功能加入到前端,并加入点击验证码更新的操作

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>香香编程-投票项目</title>
    <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<main class="main">
    <input type="text" name="name" id="name" placeholder="Your name">
    <input type="password" name="password" id="password" placeholder="Password">
    <input type="hidden" name="captcha_id" id="captcha_id" >
    <input type="text" name="captcha_value" id ="captcha_value">
    <button type="submit" id="login_sub">Sign in</button>
    <div id="img_captcha">


    </div>
</main>
<script>
    $(document).ready(function(){
        loadCaptcha()
        //确保在页面完全加载后才执行内部的代码。
        $("#login_sub").on("click",function () {//事件监听器,它绑定了一个点击事件到sign in按钮
            $.ajax({//ajax函数内部,用于异步发送请求参数
                //请求资源路径
                url:"/login",
                //请求参数
                data:{
                    name:$("#name").val(),
                    password:$("#password").val(),
                    captcha_id:$("#captcha_id").val(),
                    captcha_value:$("#captcha_value").val(),

        },
                //请求方式
                type:"post",
                //数据形式
                dataType:"json",
                //请求成功后调用的回调函数
                success:function (data) {
                    console.log(data)

                    if (data.code !== 0){
                        alert(data.message)
                    }else{
                        alert("已登录")
                       setTimeout("pageRedirect()", 3000);
                        //三秒后调转
                    }
                },
                //请求失败后调用的回调函数
                error:function () {
                    alert("请求失败!")
                }
            });
        });
        $("#img_captcha").on("click", function(){
        loadCaptcha()
        })
    });
    //实现跳转的函数
    function pageRedirect() {
        window.location.replace("/index");
    }

    function loadCaptcha() {
        $.ajax({
            url:"/captcha",
            type:"get",
            dataType:"json",
            success:function (data) {
                console.log(data)
                $("#img_captcha").empty()
              var img=new Image()
                img.onload=function (){
                    //图片加载到页面上
                    $("#img_captcha").append(img)
                }
                img.src=data.data.data
                $("#captcha_id").val(data.data.captcha_id)
            },
            error:function () {
                alert("请求失败!")
            }
        });
    }
</script>
</body>
</html>

ZAP

文章来源:https://blog.csdn.net/qq_63728673/article/details/134905844
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。