目前网上找得相关Beego资料大部分都不完整和比较陈旧,因为项目需要,编写了一个简单的用户流程。其中包括Beego 用户登陆、注册、注销、密码加密(Model模型、Session保持登陆状态、Valid包表单汉化和自定义验证)

一、前言

我太难了
目前网上找得相关Beego资料大部分都不完整和比较陈旧,因为项目需要,编写了一个简单的用户流程。

1、系统环境

Beego     : 1.12.0
GoVersion : go1.12.9

二、路由Router

路径:项目/routers/user.go

package routers

import (
	"github.com/astaxie/beego"
	"项目/controllers"
)

func init() {
	// 登陆路由
	beego.Router("/user/login/", &controllers.UserLoginController{})
	// 注销路由
	beego.Router("/user/logout/", &controllers.UserLogoutController{})
	// 注册路由
	beego.Router("/user/register/", &controllers.UserRegisterController{})
}

三、用户模型Model

路径: 项目/model/user.go

type User struct {
	Id         int
	Username   string    // 用户名
	Password   string    // 密码
	CreateTime time.Time `orm:"auto_now_add;type(datetime)"` // 创建时间
}

四、用户状态控制器UserStatusController

1、结构体

路径:项目名/controller/user.go

type UserStatusController struct {
	beego.Controller
	isLogin bool        // 登陆状态标记
	User    models.User // 登陆的用户
}

2、方法

这里重写的是对象的 Prepare 方法,这样可以在其他控制器继承该方法来判断用户是否已经登陆,实现 保持登陆状态 功能。

func (c *UserStatusController) Prepare() {
	// 设置默认值
	c.isLogin = false
	c.Data["isLogin"] = false
	c.User = models.User{}
	// 获取Session信息 - 注意这里返回的是interface类型
	useridInterface := c.GetSession("userid")
	userid, ok := useridInterface.(int)
	// 优化性能,如果Id不存在就不需要再获取Password
	if !ok {
		return
	}
	// 接下来是验证Password是否被更新 - 防止数据库密码更新后仍保持Session
	passwordInterface := c.GetSession("password")
	password, _ := passwordInterface.(string)
	user := models.User{Id: userid}
	// ORM对象
	o := orm.NewOrm()
	// 这里如果在其他地方没有用到用户其他字段的话
	// 建议改成One方法仅返回密码来判断
	// 即 o.QueryTable("user").Filter("id", &user.id).One(&user,"password")
	err := o.Read(&user)
	// 如果无法读取到用户就结束
	if err != nil {
		return
	}
	// 判断密码是否修改过
	err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
	if err != nil {
		return
	}
	// 最后才设置登陆状态
	c.isLogin = true
	c.Data["isLogin"] = true
	c.User = user
}

五、登陆控制器LoginController

路径:项目名/controller/user.go

1、结构体

继承于UserStatusController ,实现检查用户是否已经登陆,防止重复登陆。

type UserLoginController struct {
	UserStatusController
}

2、方法

表单提交使用 POST 方法

func (c *UserLoginController) Post() {
	// 创建logs(记录器)
	l := logs.BeeLogger{}
	l.SetPrefix("用户登陆API:")
	// 判断是否已经登陆 - 已登陆就终结登陆流程
	if c.isLogin == true {
		l.Debug("用户已登陆")
		c.Ctx.ResponseWriter.WriteHeader(202)
		// util.CreateMsg - 这个是我自己写的方法,请参考我博文下面的源代码
		c.Data["json"] = util.CreateMsg(202, "用户已登陆")
		c.ServeJSON()
		return
	}
	// 用户登陆
	username := strings.TrimSpace(c.GetString("username"))
	password := strings.TrimSpace(c.GetString("password"))
	// 验证表单数据 - 这里使用官方表单数据验证包(结合自定义验证) 
	// github.com/astaxie/beego/validation
	// 可以下面附加的验证工具,或者直接把这一段删掉
	u := &valid.ValidateUser{
		Username: username,
		Password: password,
	}
	err := u.ValidUser()
	if err != nil {
		l.Debug("用户注册失败,原因:%s", err.Error())
		c.Ctx.ResponseWriter.WriteHeader(403)
		c.Data["json"] = util.CreateMsg(403, err.Error())
		c.ServeJSON()
		return
	}
	// 创建User对象 - 不设置密码
	user := models.User{Username: username}
	// 创建ORM对象
	o := orm.NewOrm()
	// 读取用户对象 - 这里使用One方法而不使用Read可以避免读取整个User对象,稍微优化
	// 虽然在这里没有什么卵用,但是如果用户模型比较复杂的情况下还是有效果的
	err = o.QueryTable("user").Filter("username", &user.Username).One(&user, "id","password")
	if err != nil {
		l.Debug("用户不存在")
		c.Ctx.ResponseWriter.WriteHeader(401)
		c.Data["json"] = util.CreateMsg(401, "用户不存在")
		c.ServeJSON()
		return
	}
	// 验证用户密码
	// 第一个参数是数据库存储的加密密码,第二个参数是原密码
	// 密码是单方面加密,不可解密
	// bcrypt包提供的安全加密可以应对大部分的安全要求,具体实现细节请自行百度
	err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
	if err != nil {
		l.Debug("用户密码错误")
		c.Ctx.ResponseWriter.WriteHeader(401)
		c.Data["json"] = util.CreateMsg(401, "密码错误")
		c.ServeJSON()
		return
	}
	// 设置服务器Session存储
	c.SetSession("userid", user.Id)
	c.SetSession("password", password) // 这里存储的是原始密码

	// 返回成功信息
	c.Data["json"] = util.CreateMsg(200, "用户登陆成功")
	c.ServeJSON()
}

六、注册控制器UserRegisterController

路径:项目名/controller/user.go

注册功能只是简单演示,这里直接在App.conf(项目配置文件)里面添加了一个配置参数registertoken ,实现内部注册验证,如果要使用邮箱注册请自己再琢磨一下,注册环节

1、结构体

因为注册不需要验证是否已经登陆,直接继承beego原本的控制器就好了。如果需要验证登陆状态的话,直接改成继承UserStatusController 然后像登陆一样,判断 c.isLogin 是否为真,然后return就好。

type UserRegisterController struct {
	beego.Controller
}

2、方法

表单提交使用 POST 方法

func (c *UserRegisterController) Post() {
	l := logs.GetBeeLogger()
	l.SetPrefix("用户注册API:")
	// 内部用户注册
	// 获取Post表单数据
	registerToken := strings.TrimSpace(c.GetString("token"))
	// 验证Token - 这里是在App.conf里面设置的registertoken
	if registerToken != beego.AppConfig.String("registertoken") {
		l.Debug("注册测试Token验证失败,用户输入:%s,系统环境设置:%s", registerToken, beego.AppConfig.String("registertoken"))
		c.Ctx.ResponseWriter.WriteHeader(403)
		// util.CreateMsg - 这个是我自己写的方法,请参考我博文下面的源代码
		c.Data["json"] = util.CreateMsg(403, "token验证失败")
		c.ServeJSON()
		return
	}
	// 获取表单数据  - 去除两边的空格
	username := strings.TrimSpace(c.GetString("username"))
	password := strings.TrimSpace(c.GetString("password"))
	// 验证表单数据 - 这里使用官方表单数据验证包(结合自定义验证) 
	// github.com/astaxie/beego/validation
	// 可以下面附加的验证工具,或者直接把这一段删掉
	u := &valid.ValidateUser{
		Username: username,
		Password: password,
	}
	err := u.ValidUser()
	if err != nil {
		l.Debug("用户注册失败,原因:%s", err.Error())
		c.Ctx.ResponseWriter.WriteHeader(403)
		c.Data["json"] = util.CreateMsg(403, err.Error())
		c.ServeJSON()
		return
	}
	// 生成加密密码 - 这里使用了Golang官方加密包
	// golang.org/x/crypto/bcrypt
	// 注意 - 每次生成的密码都不一样,但是没关系
	// 登陆是验证原密码再加密后是否能和存储的加密是否一致
	// bcrypt.DefaultCost = 10
	encodePW, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		l.Debug("密码加密失败", err)
		c.Ctx.ResponseWriter.WriteHeader(500)
		c.Data["json"] = util.CreateMsg(500, "密码生成失败")
		c.ServeJSON()
		return
	}
	// 创建User对象
	user := models.User{Username: username, Password: string(encodePW)}
	// 创建ORM对象
	o := orm.NewOrm()
	// ReadOrCreate - 先读取再创建
	status, id, err := o.ReadOrCreate(&user, "username")
	// 数据库操作失败
	if err != nil {
		l.Error("发生错误:%s", err)
		c.Ctx.ResponseWriter.WriteHeader(500)
		c.Data["json"] = util.CreateMsg(500, "数据库连接失败,用户注册失败。")
		c.ServeJSON()
		return
	}
	// 创建失败 - 用户名已经存在
	if !status {
		l.Debug("用户名已经存在,id为%d", id)
		c.Ctx.ResponseWriter.WriteHeader(403)
		c.Data["json"] = util.CreateMsg(403, "用户注册失败:用户已存在")
		c.ServeJSON()
		return
	}
	// 如果需要注册后直接保持登陆状态
	// 设置服务器Session存储
	// c.SetSession("userid", user.Id)
	// c.SetSession("password", password) // 这里存储的是原始密码
	// 创建用户成功 - 返回用户名
	c.Data["json"] = util.CreateMsg(200, fmt.Sprintf("用户%s注册成功", username))
	c.ServeJSON()
}

七、注销控制器UserLogoutController

路径:项目名/controller/user.go

1、结构体

继承 UserStatusController 来判断用户的登陆状态

type UserLogoutController struct {
	UserStatusController
}

2、方法

func (c *UserLogoutController) Get() {
	// 用户注销
	if !c.isLogin {
		c.Ctx.ResponseWriter.WriteHeader(403)
		// util.CreateMsg - 这个是我自己写的方法,请参考我博文下面的源代码
		c.Data["json"] = util.CreateMsg(403, "用户未登陆")
		c.ServeJSON()
		return
	}
	// 删除Session - session删除后就不能使用 UserStatusController的 Prepare 
	// 来保持登陆状态,即实现注销方法
	c.DestroySession()
	c.Data["json"] = util.CreateMsg(200, "用户注销成功")
	c.ServeJSON()
}

八、表单验证

表单验证参考了 fengke549015beego validate验证包的修改原有验证提示信息与使用自定义函数验证的方法 ,非常感谢。这里用简便的方式来实现Valid包的提示信息汉化。

1、汉化Valid包

路径:项目/valid/init.go

package valid

import (
	"github.com/astaxie/beego/validation"
	"reflect"
)

func init() {
	// 汉化官方Validation
	var MessageTmpls = map[string]string{
		"Required":     "不能为空",
		"Min":          "最小为 %d",
		"Max":          "最大为 %d",
		"Range":        "范围在 %d 至 %d",
		"MinSize":      "最小长度为 %d",
		"MaxSize":      "最大长度为 %d",
		"Length":       "长度必须是 %d",
		"Alpha":        "必须是有效的字母字符",
		"Numeric":      "必须是有效的数字字符",
		"AlphaNumeric": "必须是有效的字母或数字字符",
		"Match":        "必须匹配格式 %s",
		"NoMatch":      "必须不匹配格式 %s",
		"AlphaDash":    "必须是有效的字母或数字或破折号(-_)字符",
		"Email":        "必须是有效的邮件地址",
		"IP":           "必须是有效的IP地址",
		"Base64":       "必须是有效的base64字符",
		"Mobile":       "必须是有效手机号码",
		"Tel":          "必须是有效电话号码",
		"Phone":        "必须是有效的电话号码或者手机号码",
		"ZipCode":      "必须是有效的邮政编码",
	}
	if len(MessageTmpls) == 0 {
		return
	}
	// 替换原本的 MessageTmpls,有兴趣的直接看一下源代码,要注意import的顺序。
	for k, _ := range MessageTmpls {
		validation.MessageTmpls[k] = MessageTmpls[k]
	}
}

// 这里将 fengke549015 中获取Alias的方法提取出来,个人感觉更优雅,如果其他
// 自定义验证需要使用该方法,可以直接调用
func GetAlias(i interface{}, field string) (alias string) {
	// 获取字段名
	// i - 结构体
	// field - 字段名
	st := reflect.TypeOf(i)
	filed, _ := st.FieldByName(field)
	return filed.Tag.Get("alias")
}

然后在 main.goimport 加入valid模块的初始化可以了,个人感觉比 fengke549015 的方法优雅一点。

import (
	"github.com/astaxie/beego"
	_ "项目/models"
	_ "项目/routers"
	_ "项目/valid"	// 就是这玩意
)

2、自定义User表单验证

路径: 项目/valid/user.go

type ValidateUser struct {
	Username string `alias:"用户名" valid:"Required;MinSize(3);MaxSize(20)"`
	Password string `alias:"密码" valid:"Required;MinSize(3);MaxSize(20)"`
}

func (u *ValidateUser) ValidUser() (err error) {
	// 先创建一个Validation
	valid := validation.Validation{}
	// 验证传入的User信息
	status, _ := valid.Valid(u)
	// 如果验证失败
	if !status {
		for _, err := range valid.Errors {
			// 获取字段别称
			var alias = GetAlias(ValidateUser{}, err.Field)
			// 返回验证的错误信息
			return errors.New(alias + err.Message)
		}
	}
	return nil
}

九、util.CreateMsg

这个方法其实就是返回 Json 内容,具体实现细节比较基础,百度比较详细,下面是源代码。

路径: 项目/util/msg.go

package util

// Json信息返回模版
type JsonMsg struct {
	Code int
	Msg  string
}

func CreateMsg(code int, msg string) (m *JsonMsg) {
	m = &JsonMsg{
		Code: code,
		Msg:  msg,
	}
	return
}