Go语言框架 beego 进阶与实战之 session

1 会话 session

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 实现用户登录功能

用户登录的功能:

  1. 用户名密码

  2. 密码加密方式决定:验证方式,修改密码,添加用户 这三个地方,一般通过 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 页面

<!DOCTYPE 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

<!DOCTYPE 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>

浏览器访问并且输入错误的用户名和密码

完整的项目地址:https://github.com/As9530272755/webuser

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇