GoFrame登錄實戰之登錄安全
從整體上看,HTTP就是一個通用的單純協議機制。因此它具備較多優勢,但是在安全性方面則呈劣勢。
HTTP的不足
●通信使用明文(不加密),內容可能會被竊聽
●不驗證通信方的身份,因此有可能遭遇僞裝
●無法證明報文的完整性,所以有可能已遭篡改
一、在瀏覽器端HTTP是可以隨意修改的
在Web應用中,從瀏覽器那接收到的HTTP請求的全部內容,都可以在客戶端自由地變更、篡改。所以Web應用可能會接收到與預期數據不相同的內容。
客戶端校驗只是爲了用戶體驗,要保證安全性就一定要做服務端校驗;
二、避免傳輸攔截
傳輸參數進行加密:前端密碼進行MD5不可逆加密;
傳輸使用https協議。
三、數據庫泄露
安全存儲用戶密碼的原則是:如果網站數據泄露了,密碼也不能被還原。
簡單的方式是通過md5 多層加密及加鹽。比如:
md5( md5( password + salt )[8:20] )
服務端數據庫存儲密碼加密bcrypt
四、防止暴力破解
- 驗證碼防止暴力破解;
- 爲用戶體驗,可多次相同ip或帳號錯誤,再進行驗證碼驗證;
- 多次同一帳號錯誤,進行一段時間的帳號鎖定。
五、常用Web的攻擊方式
跨站腳本攻擊(Cross-Site Scripting,XSS)
SQL注入攻擊(SQL Injection)
系統命令注入攻擊(OS Command Injection)
DoS攻擊(Denial of Service attack)
六、示例
目錄
D:.
│ bcrypt_test.go
│ go.mod
│ go.sum
│ main.go
│
├─config
│ config.toml
│ server.crt
│ server.key
│
├─public
│ md5.js
│
├─sql
│ init.sql
│
├─template
│ index.html
│ user_index.html
│
└─test
test.http
config.toml
# session存儲方式file,memory,redis
SessionStorage = "redis"
[server]
Address = ":80"
ServerRoot = "public"
SessionIdName = "gSessionId"
SessionPath = "./gession"
SessionMaxAge = "1m"
DumpRouterMap = true
# 系統訪問日誌
AccessLogEnabled = true
# 系統異常日誌panic
ErrorLogEnabled = true
# 系統日誌目錄,啓動,訪問,異常
LogPath = "gflogs"
[logger]
# 標準日誌目錄
path = "logs"
# 日誌級別
level = "all"
# 模板引擎配置
[viewer]
Path = "template"
DefaultFile = "index.html"
Delimiters = ["${", "}"]
# Redis數據庫配置
[redis]
default = "192.168.31.128:6379,0"
[database]
[database.logger]
Path = "./dblogs"
Level = "all"
Stdout = true
[database.default]
link = "mysql:root:123456@tcp(192.168.31.128:3306)/gf-login"
debug = true
init.sql
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`uuid` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'UUID',
`login_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '登錄名/11111',
`password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密碼',
`real_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '真實姓名',
`enable` tinyint(1) NULL DEFAULT 1 COMMENT '是否啓用//radio/1,啓用,2,禁用',
`update_time` varchar(24) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新時間',
`update_id` int(11) NULL DEFAULT 0 COMMENT '更新人',
`create_time` varchar(24) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '創建時間',
`create_id` int(11) NULL DEFAULT 0 COMMENT '創建者',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_user_username`(`login_name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 23 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用戶' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, '94091b1fa6ac4a27a06c0b92155aea6a', 'admin', 'e10adc3949ba59abbe56e057f20f883e', '系統管理員', 1, '2019-12-24 12:01:43', 1, '2017-03-19 20:41:25', 1);
INSERT INTO `sys_user` VALUES (2, '84091b1fa6ac4a27a06c0b92155aea6b', 'test', 'e10adc3949ba59abbe56e057f20f883e', '測試用戶', 1, '2019-12-24 12:01:43', 1, '2017-03-19 20:41:25', 1);
main.go
package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
"github.com/gogf/gf/os/glog"
"github.com/gogf/gf/os/gsession"
"github.com/gogf/gf/util/gconv"
"github.com/gogf/gf/util/gvalid"
"golang.org/x/crypto/bcrypt"
)
const SessionUser = "SessionUser"
func main() {
s := g.Server()
// 設置存儲方式
sessionStorage := g.Config().GetString("SessionStorage")
if sessionStorage == "redis" {
s.SetSessionStorage(gsession.NewStorageRedis(g.Redis()))
s.SetSessionIdName(g.Config().GetString("server.SessionIdName"))
} else if sessionStorage == "memory" {
s.SetSessionStorage(gsession.NewStorageMemory())
}
// 常規註冊
group := s.Group("/")
group.GET("/", func(r *ghttp.Request) {
r.Response.WriteTpl("index.html", g.Map{
"title": "登錄頁面",
})
})
// 用戶對象
type User struct {
Username string `gvalid:"username @required|length:5,16#請輸入用戶名稱|用戶名稱長度非法"`
Password string `gvalid:"password @required|length:31,33#請輸入密碼|密碼長度非法"`
}
group.POST("/login", func(r *ghttp.Request) {
username := r.GetString("username")
password := r.GetString("password")
// 使用結構體定義的校驗規則和錯誤提示進行校驗
if e := gvalid.CheckStruct(User{username, password}, nil); e != nil {
r.Response.WriteJson(g.Map{
"code": -1,
"msg": e.Error(),
})
r.Exit()
}
record, err := g.DB().Table("sys_user").Where("login_name = ? ", username).One()
// 查詢數據庫異常
if err != nil {
glog.Error("查詢數據錯誤", err)
r.Response.WriteJson(g.Map{
"code": -1,
"msg": "查詢失敗",
})
r.Exit()
}
// 帳號信息錯誤
if record == nil {
r.Response.WriteJson(g.Map{
"code": -1,
"msg": "帳號信息錯誤",
})
r.Exit()
}
// 直接存入前端傳輸的
successPwd := record["password"].String()
comparePwd := password
// 加鹽密碼
// salt := "123456"
// comparePwd, _ = gmd5.EncryptString(comparePwd + salt)
// bcrypt驗證
err = bcrypt.CompareHashAndPassword([]byte(successPwd), []byte(comparePwd))
//if comparePwd == successPwd {
if err == nil {
// 添加session
r.Session.Set(SessionUser, g.Map{
"username": username,
"realName": record["real_name"].String(),
})
r.Response.WriteJson(g.Map{
"code": 0,
"msg": "登錄成功",
})
r.Exit()
}
r.Response.WriteJson(g.Map{
"code": -1,
"msg": "登錄失敗",
})
})
// 用戶組
userGroup := s.Group("/user")
userGroup.Middleware(MiddlewareAuth)
// 列表頁面
userGroup.GET("/index", func(r *ghttp.Request) {
realName := gconv.String(r.Session.GetMap(SessionUser)["realName"])
r.Response.WriteTpl("user_index.html", g.Map{
"title": "用戶信息列表頁面",
"realName": realName,
"dataList": g.List{
g.Map{
"date": "2020-04-01",
"name": "朱元璋",
"address": "江蘇110號",
},
g.Map{
"date": "2020-04-02",
"name": "徐達",
"address": "江蘇111號",
},
g.Map{
"date": "2020-04-03",
"name": "李善長",
"address": "江蘇112號",
},
}})
})
userGroup.POST("/logout", func(r *ghttp.Request) {
// 刪除session
r.Session.Remove(SessionUser)
r.Response.WriteJson(g.Map{
"code": 0,
"msg": "登出成功",
})
})
// 生成祕鑰文件
// openssl genrsa -out server.key 2048
// 生成證書文件
// openssl req -new -x509 -key server.key -out server.crt -days 365
s.EnableHTTPS("config/server.crt", "config/server.key")
s.SetHTTPSPort(8080)
s.SetPort(8199)
s.Run()
}
// 認證中間件
func MiddlewareAuth(r *ghttp.Request) {
if r.Session.Contains(SessionUser) {
r.Middleware.Next()
} else {
// 獲取用錯誤碼
r.Response.WriteJson(g.Map{
"code": 403,
"msg": "您訪問超時或已登出",
})
}
}
bcrypt_test.go
package main
import (
"fmt"
"github.com/gogf/gf/crypto/gmd5"
"golang.org/x/crypto/bcrypt"
"testing"
)
func TestMd5(t *testing.T) {
md5, _ := gmd5.EncryptString("123456")
fmt.Println(md5)
}
func TestMd5Salt(t *testing.T) {
md5, _ := gmd5.EncryptString("123456")
fmt.Println(md5)
fmt.Println(gmd5.EncryptString(md5 + "123456"))
}
func TestBcrypt(t *testing.T) {
passwordOK := "123456"
passwordOK, _ = gmd5.EncryptString(passwordOK)
passwordERR := "12345678"
passwordERR, _ = gmd5.EncryptString(passwordERR)
hash, err := bcrypt.GenerateFromPassword([]byte(passwordOK), bcrypt.DefaultCost)
if err != nil {
fmt.Println(err)
}
//fmt.Println(hash)
encodePW := string(hash) // 保存在數據庫的密碼,雖然每次生成都不同,只需保存一份即可
fmt.Println("###", encodePW)
hash, err = bcrypt.GenerateFromPassword([]byte(passwordOK), bcrypt.DefaultCost)
if err != nil {
fmt.Println(err)
}
encodePW = string(hash) // 保存在數據庫的密碼,雖然每次生成都不同,只需保存一份即可
fmt.Println("###", encodePW)
// 其中:$是分割符,無意義;2a是bcrypt加密版本號;10是cost的值;而後的前22位是salt值;
// 再然後的字符串就是密碼的密文了。
// 正確密碼驗證
err = bcrypt.CompareHashAndPassword([]byte(encodePW), []byte(passwordOK))
if err != nil {
fmt.Println("pw wrong")
} else {
fmt.Println("pw ok")
}
// 錯誤密碼驗證
err = bcrypt.CompareHashAndPassword([]byte(encodePW), []byte(passwordERR))
if err != nil {
fmt.Println("pw wrong")
} else {
fmt.Println("pw ok")
}
}
視頻地址
- 騰訊課堂教程地址:https://ke.qq.com/course/2587868?taid=9171133864049884&tuin=13b4f9bd
- bilibili教程地址:https://www.bilibili.com/video/BV1oT4y1G7ge/
代碼地址
- github:https://github.com/goflyfox/gfstudy
- gitee:https://gitee.com/goflyfox/gfstudy
- 公衆號搜索:GoWeb學習之路