投票 V2 版本
1. 增加一个注册账号的功能
增加接口
参数校验:
- 确认密码需要一致, 不为空
- 用户名必须唯一,不为空
- 用户名大于8小于16位
- 密码大于8小于16位,并且不能为纯数字
引入正则表达式,字符串长度,是否是数字,是否是身份证,是否是邮箱,是否是abc开头
func CreateUser(context *gin.Context) {
var user reUser
if err := context.ShouldBind(&user); err != nil {
context.JSON(http.StatusOK, tools.ECode{
Code: 10001,
Message: err.Error(), //这里有风险
})
return
}
//对数据进行校验
if user.Name == "" || user.Password == "" || user.Password2 == "" {
context.JSON(http.StatusOK, tools.ECode{
Code: 10003,
Message: "账号或者密码不能为空", //这里有风险
})
return
}
//校验密码
if user.Password != user.Password2 {
context.JSON(http.StatusOK, tools.ECode{
Code: 10003,
Message: "两次密码不同!", //这里有风险
})
return
}
//校验用户是否存在,这种写法非常不安全。有严重的并发风险
if oldUser := model.GetUser(user.Name); oldUser.Id > 0 {
context.JSON(http.StatusOK, tools.ECode{
Code: 10004,
Message: "用户名已存在", //这里有风险
})
return
}
//判断位数
lenName := len(user.Name)
lenPwd := len(user.Password)
if lenName < 8 || lenName > 16 || lenPwd < 8 || lenPwd > 16 {
context.JSON(http.StatusOK, tools.ECode{
Code: 10005,
Message: "用户名或者密码要大于等于8,小于等于16!", //这里有风险
})
return
}
//密码不能是纯数字
regex := regexp.MustCompile(`^[0-9]+$`)
if regex.MatchString(user.Password) {
context.JSON(http.StatusOK, tools.ECode{
Code: 10006,
Message: "密码不能为纯数字", //这里有风险
})
return
}
//开始添加用户
newUser := model.User{
Name: user.Name,
Password: user.Password,
CreatedTime: time.Now(),
UpdatedTime: time.Now(),
}
if err := model.CreateUser(&newUser); err != nil {
context.JSON(http.StatusOK, tools.ECode{
Code: 10006,
Message: "用户创建失败", //这里有风险
})
return
}
//返回添加成功
context.JSON(http.StatusOK, tools.OK)
return
}
使用ap ifox 测试。postman
思考题:如果同时有100人用同样的name 注册,是否会出现重复呢?
增加密码加密
为什么 要对密码进行加密?个人信息进行脱敏。150****1234
密码不能太简单,不能容易被猜到,不能是常见字符。admin 123456 woaini1314
// 最基础的版本
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 {
newPwd := pwd + "香香编程喵喵喵" //不能随便起,且不能暴露
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
}
将加密后的密码存入数据库,也是一个技术活。这里有个问题:
存密码的pwd字段,用char 还是varchar ?
引申:什么是加盐?
salt值 (盐值) 在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。
使用APIFOX进行测试
后续普遍会使用这个工具进行接口测试。
2.增加一个验证码功能
引入验证码包
https://github.com/dchest/vv 实现较为简单,使用方便。但是,已经被破解了
https://github.com/mojocn/base64Captcha 比较全面,较多使用,支持多种语言
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)
}
base64 转图片:https://www.lddgo.net/convert/base64-to-image
测试接口
//验证码
{
r.GET("/captcha", func(context *gin.Context) {
captcha, err := tools.CaptchaGenerate()
if err != nil {
context.JSON(http.StatusOK, tools.ECode{
Code: 10005,
Message: err.Error(),
})
return
}
context.JSON(http.StatusOK, tools.ECode{
Data: captcha,
})
})
r.POST("/captcha/verify", func(context *gin.Context) {
var param tools.CaptchaData
if err := context.ShouldBind(¶m); err != nil {
context.JSON(http.StatusOK, tools.ParamErr)
return
}
fmt.Printf("参数为:%+v", param)
if !tools.CaptchaVerify(param) {
context.JSON(http.StatusOK, tools.ECode{
Code: 10008,
Message: "验证失败",
})
return
}
context.JSON(http.StatusOK, tools.OK)
})
}
为登录接口添加验证码接口
<!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="image-captcha">
</div>
</main>
<script>
$(document).ready(function(){
loadCaptcha()
$("#login_sub").on("click",function () {
$.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("登录成功")
}
});
});
$("#image-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)
var img = new Image();
$("#image-captcha").empty()
img.onload = function() {
// 将图片添加到页面上
$("#image-captcha").append(img);
};
img.src = data.data.data
$("#captcha_id").val(data.data.captcha_id)
},
error:function () {
alert("登录成功")
}
});
}
</script>
</body>
</html>
- 验证码有什么用?判断用户行为是脚本还是人工
- 常见的验证码有哪些?图片验证码,拖动验证码,按照顺序点击,9张图选择有红绿灯的。邮箱验证码,手机验证码
- 补充,这个包是如何工作的?a.验证码是如何生成的?math , 三角函数,b.jpg png base64 直接存在内存中,c.base64吐给前端,渲染到页面上。d.输入验证码+验证码的ID,服务端进行校验
注意!这个接口存在非常严重的安全隐患
1.点击验证码 重新生成新的验证码。
引申,DDOS攻击和CC攻击
后端开发工程师:SQL注入,CSRF/XSS DDOS,CC攻击
WAF花钱,买一个高防服务器。-》验证码功能
不让服务器生成 验证码。前端来做。
3.增加校验,防止刷票
增加是否投票的校验,防止刷票
第一种方式:在事务中查询,增加了事务的逻辑,成本非常高。
//检查是否投过票
var oldUser VoteOptUser
if err := tx.Table("vote_opt_user").Where("user_id = ? and vote_id = ?", userId, voteId).First(&oldUser).Error; err != nil {
fmt.Printf("err:%s", err.Error())
tx.Rollback()
return false
}
if oldUser.Id > 0 {
fmt.Printf("err:%s", "用户已经投过票了!")
tx.Rollback()
return false
}
第二种方式:前置查询
//查询是否投过票了
voteUser := model.GetVoteHistory(userID, voteId)
if len(voteUser) > 0 {
context.JSON(http.StatusOK, tools.ECode{
Code: 10010,
Message: "您已投过票了",
})
return
}
思考题:和登录一样,如果同时一个人同时发起100个请求,进行投票,会出现重复投票的现象么?
以上两种方式都无法完美解决这个问题。悲观锁,乐观锁,分布式锁。消息队列
4.增加一个定时器,到期自动关闭
package schedule
import (
"time"
"xxvote/app/model"
)
func Start() {
go voteEnd()
return
}
func voteEnd() {
t := time.NewTicker(1)
defer t.Stop()
for {
select {
case <-t.C:
//fmt.Printf("定时器 voteEnd 启动")
_ = model.EndVote()
}
}
}
定时任务,并不是最好的解决方案。有没有其他更好的方案?
那必须有,内存处理,或者延时队列
5.引入一个日志包
官方包
//官方日志包
log.Printf("[printf]ret:%+v\n", ret)
log.Panicf("[Panicf]ret:%+v\n", ret)
log.Fatalf("[Fatalf]ret:%+v", ret)
结论,太鸡肋了。几乎没有人用,除非花时间进行二次封装。
logrus
https://github.com/sirupsen/logrus
日志级别
PanicLevel: 记录日志,panic
FatalLevel: 记录日志,程序 exit
ErrorLevel: 错误级别日志
WarnLevel: 警告级别日志
InfoLevel: 关键信息级别日志
DebugLevel: 调试级别
TraceLevel: 追踪级别
package tools
import (
log "github.com/sirupsen/logrus"
"io"
"os"
)
var Logger *log.Entry
func NewLogs() {
logStore := log.New()
logStore.SetLevel(log.TraceLevel)
// 同时写到多个输出
w1 := os.Stdout //写到控制台
w2, _ := os.OpenFile("./vote.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
logStore.SetOutput(io.MultiWriter(w1, w2)) // io.MultiWriter 返回一个 io.Writer 对象
logStore.SetFormatter(&log.JSONFormatter{})
//Logger = logStore.WithContext()
// 增加一些默认默认字段
Logger = logStore.WithFields(log.Fields{
"name": "香香编程喵喵喵",
"app": "voteV2",
})
//可以增加hook 函数,当触发某些特殊的日志后,执行某些函数。比如:邮件告警,日志分割与上报等。
//Logger.AddHook()
}
ZAP
https://github.com/uber-go/zap
为什么要使用日志?
- 开发和调试的时候,比较方便。比format 好用的多。。
- 记录线上BUG的现场情况,便于后续排查。
- 未雨绸缪。
- 新服务上线,肯定需要持续一段时间观察日志情况。
- 老服务定时摸查冒烟点,避免生产事故。