从整体上看,HTTP就是一个通用的单纯协议机制。因此它具备较多优势,但是在安全性方面则呈劣势。
HTTP的不足
●通信使用明文(不加密),内容可能会被窃听
●不验证通信方的身份,因此有可能遭遇伪装
●无法证明报文的完整性,所以有可能已遭篡改
在Web应用中,从浏览器那接收到的HTTP请求的全部内容,都可以在客户端自由地变更、篡改。所以Web应用可能会接收到与预期数据不相同的内容。
客户端校验只是为了用户体验,要保证安全性就一定要做服务端校验;
传输参数进行加密:前端密码进行MD5不可逆加密;
传输使用https协议。
安全存储用户密码的原则是:如果网站数据泄露了,密码也不能被还原。
简单的方式是通过md5 多层加密及加盐。比如:
md5( md5( password + salt )[8:20] )
服务端数据库存储密码加密bcrypt
- 验证码防止暴力破解;
- 为用户体验,可多次相同ip或帐号错误,再进行验证码验证;
- 多次同一帐号错误,进行一段时间的帐号锁定。
跨站脚本攻击(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.yaml
│ server.crt
│ server.key
│
├─public
│ md5.js
│
├─sql
│ init.sql
│
├─template
│ index.html
│ user_index.html
│
└─test
test.http
# session存储方式file,memory,redis
SessionStorage: redis
server:
Address: :80
ServerRoot: public
SessionIdName: gSessionId
SessionPath: ./gession
SessionMaxAge: 1m
DumpRouterMap: true
AccessLogEnabled: true # 系统访问日志
ErrorLogEnabled: true # 系统异常日志panic
LogPath: gflogs # 系统日志目录,启动,访问,异常
logger:
path: logs # 标准日志目录
level: all # 日志级别
viewer: # 模板引擎配置
Path: template
DefaultFile: index.html
Delimiters:
- ${
- "}"
redis: # Redis数据库配置
default: 192.168.31.128:6379,0
database:
logger:
Path: ./dblogs
Level: all
Stdout: true
default:
link: mysql:root:123456@tcp(192.168.31.128:3306)/gf-login
debug: true
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);
package main
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/gsession"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/gvalid"
"golang.org/x/crypto/bcrypt"
)
const SessionUser = "SessionUser"
func main() {
s := g.Server()
ctx := gctx.New()
// 设置存储方式
sessionStorage := g.Config().MustGet(ctx, "SessionStorage").String()
if sessionStorage == "redis" {
s.SetSessionStorage(gsession.NewStorageRedis(g.Redis()))
s.SetSessionIdName(g.Config().MustGet(ctx, "server.SessionIdName").String())
} 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.Get("username").String()
password := r.Get("password").String()
// 使用结构体定义的校验规则和错误提示进行校验
if e := gvalid.New().Data(User{username, password}).Run(r.GetCtx()); e != nil {
r.Response.WriteJson(g.Map{
"code": -1,
"msg": e.Error(),
})
r.Exit()
}
record, err := g.DB().Model("sys_user").Where("login_name = ? ", username).One()
// 查询数据库异常
if err != nil {
glog.Error(r.GetCtx(), "查询数据错误", 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.MustGet(SessionUser).Map()["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 ok, _ := r.Session.Contains(SessionUser); ok {
r.Middleware.Next()
} else {
// 获取用错误码
r.Response.WriteJson(g.Map{
"code": 403,
"msg": "您访问超时或已登出",
})
}
}
package main
import (
"fmt"
"github.com/gogf/gf/v2/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")
}
}