对应视频v2版本
gin项目投票系统4
1.增加一个注册账号的功能
增加接口
参数校验:(需求)
- 确认密码需要一致,不为空
- 用户名必须唯一, 不为空
- 用户名大于8小于16位
- 密码大于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 passwordhash := 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 stringCaptchaType stringVerifyValue stringDriverString *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 = idret.Data = b64sreturn 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 toolsimport ("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 对象}
效果:
可在控制台和文件中都生成日志;
拓展:日志可以生成不同类型的:
比如JSON,在日志中再添加字段
logStore.SetFormatter(&logrus.JSONFormatter{})
//生成json格式的日志
Logger = logStore.WithFields(logrus.Fields{"name": "香香编程喵喵喵","app": "voteV2",
})//添加两个字段
Hook函数(钩子函数)是什么:
Hook 函数是一种编程技术,其作用是在特定事件发生时插入自定义的代码,以便执行额外的逻辑或修改程序的行为。这样的插入点通常称为 “hook points”,而插入自定义代码的函数就是 “hook 函数”。
主要作用和用途包括:
- 扩展功能: Hook 函数可以用于在程序运行时动态地添加或修改功能。通过在特定事件上挂钩,可以在不修改源代码的情况下扩展应用程序的行为。
- 调试和日志: Hook 函数常常用于记录日志、跟踪程序执行流程,或在特定条件下触发调试信息。这对于排查问题、性能优化以及了解程序行为非常有用。
- 事件通知: Hook 函数还可以用于向其他部分发送通知,让它们在特定事件发生时执行相应的操作。这种方式用于实现观察者模式等。
- 修改数据: 有时 Hook 函数也用于修改或过滤数据。例如,在数据保存到数据库之前执行某些处理。
- 安全性: 在安全领域,Hook 函数可以用于拦截和处理潜在的安全威胁,比如输入验证或访问控制。
在实际编程中,Hook 函数通常通过回调函数或事件监听机制来实现。编程框架和库通常提供了一些预定义的 hook points,同时也允许开发者定义自己的 hook points。
ZAP
logrus
和 zap
都是 Go 语言中流行的日志库,它们各自有自己的特点和适用场景。下面是对它们的一些比较:
logrus:
- 易用性:
logrus
相对于zap
来说,更容易上手,其 API 设计更为简单直观。 - 社区支持: 由于
logrus
的存在时间较长,因此在社区中有更多的用户和资源,相应的社区支持更丰富。 - 扩展性:
logrus
支持很多的插件和扩展,可以很容易地集成到各种不同的环境和系统中。 - 灵活性: 可以通过 Hook 的方式灵活扩展
logrus
的功能。
zap:
- 性能:
zap
以高性能为目标,被设计成尽可能地快,具有更低的内存分配和更高的吞吐量。适用于高并发和性能敏感的应用。 - 结构化日志:
zap
倡导结构化日志,即将日志信息存储为结构体,使日志更容易分析和查询。 - 零分配:
zap
设计了零分配(zero-allocation)的原则,以减少垃圾回收的影响。 - Sugar API:
zap
提供了一个 Sugar API,用于简化常用日志操作的调用。
如何选择:
- 如果你更看重易用性、社区支持和扩展性,可以选择
logrus
。 - 如果你的应用对性能要求很高,且你希望采用结构化日志,那么
zap
可能更适合。
最终的选择取决于你的具体需求和项目的特点。如果不确定,可以先使用其中一个,后期根据实际情况进行评估和切换。
日志既然这么麻烦又为什么要引入日志呢?
- 故障排查和调试: 在应用程序出现问题时,日志是排查错误和调试的关键工具。通过查看日志,开发人员可以追踪代码的执行路径、发现异常行为,并更容易地定位问题。
- 性能监控: 日志也是性能监控的一部分。通过记录关键操作的执行时间、资源使用情况等信息,开发人员可以了解应用程序的性能状况,并进行优化。
- 安全审计: 在一些敏感的系统中,日志记录也用于安全审计。记录用户的操作、访问尝试以及其他安全相关事件,以便进行审计和追踪。
- 业务分析: 日志还可以用于业务分析。通过记录用户的行为、交易记录等信息,可以为业务决策提供数据支持。
- 运维监控: 运维团队通过监控日志可以实时掌握系统运行状态,及时发现和处理潜在问题。
- 历史记录: 日志可以作为系统的历史记录,帮助了解系统的演变和变更历史。
虽然引入日志可能增加了代码的复杂性,但日志对于维护和监控大型应用是至关重要的。它为开发者提供了一种实时的、非侵入式的了解应用运行状况的手段。在生产环境中,日志往往是排查问题的最主要工具之一。
将验证码功能加入到前端,并加入点击验证码更新的操作
<!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