session 服务器端存储:内存、本地磁盘文件、数据库中,但是我们都知道将数据从内存写入文件中有一个序列化过程,但是 beego 的话不用关注这个过程。在持久化存储的时候必须序列化,session 在 beego 中持久化使用的是 gob 编码,也就是说我在 session 中放一个自定义类型的需要注册然后生成 gob 编码持久化
session 反序列化:在用户请求的时候会从 cookie 里面去找到 session id ,假设 session id 在数据库里面就连接数据库,如果在本地文件就需要打开文件读取数据,获取到数据之后就需要反序列化。
session 序列化:在服务器响应的时候进行序列化
也就是说每次请求都会有 session 序列化和反序列化这两个动作
session 需要关注的几点:
session 是唯一的,一定要确保 sessionid 的安全性
-
Controller 中对 session 的操作
-
操作:我们可以把 session 理解为
key:value
的结构,key 是 string,value 为 int-
session 获取:
GetSession(key)interface{} 检查权限
-
session 更新:
SetSession(key,value) 登录认证成功
-
session 删除:
DelSession(key)
-
session 销毁:
DestoySession() 退出的时候
-
-
-
配置一般放到配置文件
conf/app.conf
(ini 格式配置文件,key=value 的格式)-
session 存储类型:
SessionProvider=memory/file/mysql/redis(默认 memory,存储到数据库,表名和表结构 beego 已经设置好了)
-
session 存储的位置:
SessionProviderConfig
-
是否开启 session :
SessionOn=true/false (true开启 false 关闭)
-
session 的失效时间:
SessionGCMaxLifetime=3600(默认时间 3600S)
-
Cookie 失效时间:
SessionCookieLifetime
-
session SID 名称:
SessionName=beegoSessionId
ini 配置文件格式 [default] key=value [name] key=value
-
-
Session 存储数据:
-
尽量少,不要存储大量的数据到 session 中
-
如果能用基本类型就是用基本类型
-
自定义类型需要使用 gob 注册
-
1.1 session 实现用户登录功能
用户登录的功能:
-
用户名密码
-
密码加密方式决定:验证方式,修改密码,添加用户 这三个地方,一般通过 hash 运算:md5、sha512、hash 加盐、慢速加盐算法(bcrypt)
这里使用的是慢速加盐算法,慢速加盐算法包:golang.org/x/crypto/bcrypt
1.1.1 慢速加盐算法使用范例
慢速加盐每次对同一个数据进行计算,得到的加密数据都不一样
// password []byte:我们需要加密得密码
// cost int:慢速加盐有一个计算过程的计算次数,这里计算 16 次
func GenerateFromPassword(password []byte, cost int) ([]byte, error)
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
hashed, err := bcrypt.GenerateFromPassword([]byte("123@456"), 16)
fmt.Println(string(hashed), err)
}
执行
# 第一次
[15:21:53 root@go testbcrypt]#go run main.go
$2a$16$cn/KdOEVg2gtf6UC.huhW.kPjKrvEtQ14/sqw.SpljfIM60UuiGEa <nil>
# 第二次
[15:22:24 root@go testbcrypt]#go run main.go
$2a$16$Zbk4bMIdEaBbB8XZ9UnJ5epZS4/RZ4bjsgXiHEsak3BDgdS17pQe2 <nil>
# 123@456 加盐之后数据
# error 为 nil
1.1.2 对加盐后的数据进行验证
// 第一个参数为 hash加盐后的数据
// 第二个参数为 我们对数据加密的明文密码
// 返回值为 error
func CompareHashAndPassword(hashedPassword, password []byte) error
虽然得到的数据是不一样得,但是我们可以通过验证
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
hash1 := "$2a$16$cn/KdOEVg2gtf6UC.huhW.kPjKrvEtQ14/sqw.SpljfIM60UuiGEa"
hash2 := "$2a$16$Zbk4bMIdEaBbB8XZ9UnJ5epZS4/RZ4bjsgXiHEsak3BDgdS17pQe2"
// 都是对 "123@456" 这个密码进行验证
fmt.Println(bcrypt.CompareHashAndPassword([]byte(hash2), []byte("123@456")))
fmt.Println(bcrypt.CompareHashAndPassword([]byte(hash1), []byte("123@456")))
}
# 得到得错误返回值为 nil 也就是说验证成功
[15:29:09 root@go testbcrypt]#go run main.go
<nil>
<nil>
现在登录的密码加密解决了,接着我们看登陆的逻辑
1.1.3 用户登录功能实现
-
打开登陆页面
-
用户输入用户名密码,点击提交
-
进行用户名密码验证
-
成功登录:跳转到用户列表
-
失败:返回原来页面,并提示用户登录失败
-
1.编写登录时候的用户结构体
package models
type LoginForm struct {
Username string `form:"username"`
Password string `form:"password"`
}
2.编写认证功能
package controller
import (
"beegouser/models"
"beegouser/service"
"fmt"
"github.com/astaxie/beego"
)
type AuthController struct {
beego.Controller
}
// 打开页面和点击登录都交给 Login 来处理
func (c *AuthController) Login() {
var errMsg string
// 如果是 post 请求就登录
if c.Ctx.Input.IsPost() {
// 将用户提交的数据。解析我们的 loginform 结构体中
form := models.LoginForm{}
if err := c.ParseForm(&form); err == nil {
if user := service.Auth(&form); user != nil {
// 登录成功就跳转值 /user/listuser 界面
fmt.Println("登陆成功")
c.Redirect("/user/listuser", 302)
return
} else {
errMsg = "提示:用户名或密码输入错误"
}
}
}
c.Data["form"] = nil
c.Data["error"] = errMsg
// 如果不是 post 打开登陆页面
c.TplName = "auth/login.html"
}
func (c *AuthController) Logout() {
}
3.编写 service 中获取 user 信息以及获取 password 信息
// 指定查询登录的 name 字段是否正确
func GetUserByName(name string) *models.User {
var (
uid int64
uname string
upassword string
)
err := config.DB.QueryRow("select id,name,password from WEBuser where name=?", name).Scan(&uid, &uname, &upassword)
if err != nil {
return nil
}
user := models.NewUser(uid, uname, "", false, "")
user.Password = upassword
return user
}
func Auth(form *models.LoginForm) *models.User {
// 接着通过用户名去查询用户信息(至少包含密码 hash 值)
if user := GetUserByName(form.Username); user == nil {
// 如果 user 在 GetUserByName 查询到的返回结果为 nil 就说明用户输入的用户名错误
return nil
} else {
// 如果有该用户的话我们就检查一下密码,密码匹配就返回 user
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(form.Password)); err == nil {
return user
} else {
return nil
}
}
}
4.编写 login.html 页面
<html>
<head>
<meta charset="utf-8" />
<title>用户登录</title>
<body>
<h1>用户登录</h1>
<form action="/auth/login/" method="post">
<!-- 如果 error 为真就输出 error -->
{{ if .error }}
<div>{{ .error }}</div>
{{ end }}
<div>
<label>用户名:</label>
<input name="username" type="text" value="{{ .form.Username}}"/>
</div>
<div>
<label>密码:</label>
<input name="password" type="password" value="" />
</div>
<div>
<input type="submit" value="登录" />
</div>
</form>
</body>
</head>
</html>
5.查询数据库,默认密码为 123@456
MariaDB [hellodb]> select * from WEBuser;
+----+--------+-----+-----+--------+---------------------+---------------------+------------+--------------------------------------------------------------+
| id | name | age | sex | addr | created_at | updated_at | deleted_at | password |
+----+--------+-----+-----+--------+---------------------+---------------------+------------+--------------------------------------------------------------+
| 28 | 秒云 | 12 | 1 | 北京 | 2021-08-09 21:51:42 | 2021-08-09 21:51:42 | NULL | $2a$16$cn/KdOEVg2gtf6UC.huhW.kPjKrvEtQ14/sqw.SpljfIM60UuiGEa |
| 31 | zhang | 132 | 0 | 北京 | 2021-08-09 22:00:17 | 2021-08-09 22:00:17 | NULL | $2a$16$cn/KdOEVg2gtf6UC.huhW.kPjKrvEtQ14/sqw.SpljfIM60UuiGEa |
| 32 | 周周 | 13 | 0 | 重庆 | 2021-08-09 22:01:37 | 2021-08-09 22:01:37 | NULL | $2a$16$cn/KdOEVg2gtf6UC.huhW.kPjKrvEtQ14/sqw.SpljfIM60UuiGEa |
| 33 | test | 123 | 0 | qwe | 2021-08-10 12:42:05 | 2021-08-10 12:42:05 | NULL | $2a$16$cn/KdOEVg2gtf6UC.huhW.kPjKrvEtQ14/sqw.SpljfIM60UuiGEa |
+----+--------+-----+-----+--------+---------------------+---------------------+------------+--------------------------------------------------------------+
6.浏览器访问http://10.0.0.10:8000/auth/login/
输入错误的用户名和密码就会被跳转到登录页面
正确的用户名和密码就会跳转至用户显示页面:test用户是存在数据库中的,密码为 123@456
但是现在我们可以看到没有 cookie,其实可以认为每次请求都是第一次,没有登陆也可以实现对用户列表的访问,那我们如何来限制这个登录以后才可以访问呢?我们就得使用 session 了
1.1.4 添加 session 实现链路跟踪
-
这就涉及到 session 的 存、取、销毁
-
存放 session 一般都在登录成功以后(存放的时候只存放用户的 id),肯定是要在查询之前就进行检查
-
取一般是在二次登录的时候,(取也是取当前用户的 id,如果取出来的这个 id 为 nil 就可以理解为没有登陆,如果取到了 id 就认为是登录了)
-
销毁一般是退出登录的时候
-
我们定义 session 的方法,然后再登录进来之后对用户的每个操作中都要验证我们的 session,而且获取 session 的这些代码都是统一的,那我们可以将他们写到一个统一的地方
-
因为在 controller 函数中他的执行顺序如下:
-
init
-
prepare
:一般做一些数据的验证,所以我们就可以将用户的检查机制放到 Preare 中,在 beego 中如果 prepare 中有输出就不会再执行下面的函数了 -
Action/Get/Post/Index
-
Render
-
Finished
-
1.登录成功读取我们的 session id
package controller
import (
"beegouser/errors"
"beegouser/models"
"beegouser/service"
"fmt"
"github.com/astaxie/beego"
)
type AuthController struct {
beego.Controller
}
// 打开页面和点击登录都交给 Login 来处理
func (c *AuthController) Login() {
// 登录成功就不让在访问 login 的页面
if user := c.GetSession("user"); user != nil {
c.Redirect("/user/listuser", 302)
return
}
// 有的时候错误信息可能是多个,所以这里定义一个 errors 的map,并且 value 是[]
errors := errors.NewErrors()
form := models.LoginForm{}
// 如果是 post 请求就登录
if c.Ctx.Input.IsPost() {
// 将用户提交的数据。解析我们的 loginform 结构体中
if err := c.ParseForm(&form); err == nil {
if user := service.Auth(&form); user != nil {
// 登录成功就跳转值 /user/listuser 界面
fmt.Println("登陆成功")
// 登录成功存储 session 读取 user.ID
c.SetSession("user", user.ID)
c.Redirect("/user/listuser", 302)
return
} else {
errors.AddError("default", "提示:用户名或密码输入错误")
}
}
}
// 用户输入数据回显
c.Data["form"] = form
c.Data["errors"] = errors
// 如果不是 post 打开登陆页面
c.TplName = "auth/login.html"
}
func (c *AuthController) Logout() {
c.DestroySession()
c.Redirect("/auth/login", 302)
}
2.在每次对用户的操作都要通过 Prepare 方法验证 session 是否正确
package controller
import (
"beegouser/models"
"beegouser/service"
"fmt"
"log"
"github.com/astaxie/beego"
)
type UserController struct {
beego.Controller
}
// 定义检查 session 方法
func (c *UserController) Prepare() {
user := c.GetSession("user")
if user == nil {
// 如果 user 为 nil 就是未登录,就重定向到登录页面
c.Redirect("/auth/login", 302)
return
}
}
func (c *UserController) ListUser() {
c.Data["user"] = service.GetUser()
c.TplName = "users/user.html"
}
func (c *UserController) Add() {
if c.Ctx.Input.IsPost() {
var form models.User
c.ParseForm(&form)
service.AddUser(form.Name, form.Age, form.Addr, form.Sex)
c.Redirect("/user/listuser", 302)
} else {
c.TplName = "users/add.html"
}
}
func (c *UserController) Delete() {
if id, err := c.GetInt64("id"); err == nil {
service.DeleteUser(id)
c.Redirect("/user/listuser", 302)
} else {
log.Println(err)
return
}
}
func (c *UserController) Edit() {
type users struct {
Name string `form:"name"`
Age string `form:"age"`
Sex bool `form:"sex"`
Addr string `form:"addr"`
}
id, _ := c.GetInt64("id")
if c.Ctx.Input.IsPost() {
fmt.Println("post", id)
var form users
c.ParseForm(&form)
fmt.Println("form", form)
service.Edit(id, form.Name, form.Age, form.Addr, form.Sex)
c.Redirect("/user/listuser", 302)
} else {
fmt.Println(id)
user, _ := service.IDFindUser(id)
//users = user
c.Data["user"] = user
c.TplName = "users/edit.html"
}
}
3.在 conf 文件中开启 session 功能,并且将他设置为持久化,以避免我们重启服务 session 丢失
RunMode=dev
SessionOn=true // 开启 session
SessionProvider=file // 以 file 的方式存储
SessionProviderConfig=./tmp/session/ // session 的存储地址,在程序当前路径下 tmp/session 下
这里我直接访问 http://10.0.0.10:8000/user/listuser 就会被 302 重定向至 http://10.0.0.10:8000/auth/login
登录成功浏览器访问就存在了 session id
但是现在有个想法就是如果我在另外的 Controller 中也需要用户认证功能那岂不是还得需要在写一个 Prepare 方法?由此引出下面的功能
1.1.5 多个 Controller 中调用 Prepare
比如我这里还有一个 home 的 controller 控制器,但是也想实现 session 功能,怎么办呢。当然也可以在 home 中添加一个 beego 的 Prepare 方法,但是这样的代码就显得冗余,所以我们单独的写一个处理 session 方法
1.编写 Prepare 函数
package controllers
import "github.com/astaxie/beego"
type RequiredAuthController struct {
// 嵌套了 beego.Controller
beego.Controller
}
func (c *RequiredAuthController) Prepare() {
user := c.GetSession("user")
if user == nil {
// 如果 user 为 nil 就是未登录,就重定向到登录页面
c.Redirect("/auth/login", 302)
}
}
2.编写 home controller
package controller
import (
base "beegouser/base/controllers"
)
type HomeController struct {
base.RequiredAuthController
}
func (c *HomeController) Index() {
c.Ctx.WriteString("home")
}
3.在 user controller 中将 RequiredAuthController 中的 Prepare 写入
浏览器访问 http://10.0.0.10:8000/home/index 被重定向到登录页面
浏览器访问 http://10.0.0.10:8000/user/listuser 用户列表页面也被重定向到了登录页面
但是现在还没有退出功能
1.1.6 退出登录
// 通过 beego 的 DestroySession 退出登录
func (c *AuthController) Logout() {
c.DestroySession()
c.Redirect("/auth/login", 302)
}
1.1.7 添加 errors 信息
用户输入数据中含有错误信息,我们需要将其处理,但是错误信息可能不止一个甚至多个,所以我们要编写一个 error 的一个 map 并且 value 为[]string
1.编写 error 代码
package errors
type Errors map[string][]string
func NewErrors() Errors {
return make(Errors)
}
// 增加 error
func (e Errors) AddError(key, err string) {
e[key] = append(e[key], err)
}
// 删除 error
func (e Errors) Clear() {
keys := make([]string, 0, len(e))
for k := range e {
keys = append(keys, k)
}
for _, key := range keys {
delete(e, key)
}
}
2.将 error 导入到我们的 登录认证的代码中
// 打开页面和点击登录都交给 Login 来处理
func (c *AuthController) Login() {
// 有的时候错误信息可能是多个,所以这里定义一个 errors 的 map,并且 value 是[]
errors := errors.NewErrors()
form := models.LoginForm{}
// 如果是 post 请求就登录
if c.Ctx.Input.IsPost() {
// 将用户提交的数据。解析我们的 loginform 结构体中
if err := c.ParseForm(&form); err == nil {
if user := service.Auth(&form); user != nil {
// 登录成功就跳转值 /user/listuser 界面
fmt.Println("登陆成功")
// 登录成功存储 session 读取 user.ID
c.SetSession("user", user.ID)
c.Redirect("/user/listuser", 302)
return
} else {
errors.AddError("default", "提示:用户名或密码输入错误")
}
}
}
// 用户输入数据回显
c.Data["form"] = form
c.Data["errors"] = errors
// 如果不是 post 打开登陆页面
c.TplName = "auth/login.html"
}
然后再修改我们的 auth.html
<html>
<head>
<meta charset="utf-8" />
<title>用户登录</title>
<body>
<h1>用户登录</h1>
<form action="/auth/login/" method="post">
<!-- 如果 error 为真就输出 error -->
{{ if .errors.default }}
<!-- 拿出错误切片的第一个元素 -->
<div>{{ index .errors.default 0 }}</div>
{{ end }}
<div>
<label>用户名:</label>
<input name="username" type="text" value="{{ .form.Username}}"/>
</div>
<div>
<label>密码:</label>
<input name="password" type="password" value="" />
</div>
<div>
<input type="submit" value="登录" />
</div>
</form>
</body>
</head>
</html>
浏览器访问并且输入错误的用户名和密码
完整的项目地址: