對應視頻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