我们即将使用beego来构建一个cmdb,会用到很多beego的东西,包括。orm,context,log等。

创建目录每个目录代表不同的作用,我们手动创建即可。分别有conf,controller,forms,logs,models,routers,staic,temp,utils,views而后使用go mod init web进行初始化

第一部分#

我们想从第一部分main.go开始,命令行参数,命令行参数要满足一些基本的需要。如下

1,命令行参数#

我们会对几个关键字做输入解析,将会使用到flag,理想中,我们至少需要输入如下参数来完成开始部分。大概需要经过五个部分

1.初始化命令行参数#

我们使用flag包来做,如下:

    h := flag.Bool("h", false, "帮助信息")
    help := flag.Bool("help", false, "帮助信息")
    init := flag.Bool("init", false, "初始化")
    syncdb := flag.Bool("syncdb", false, "sync db")

    force := flag.Bool("force", false, "强制同步数据库(删除数据库信息)")
    verbose := flag.Bool("v", false, "verbose")

这些信息在输入-h或者--help的时候会被打印

    flag.Usage = func() {
        fmt.Println("Usage: -h")
        flag.PrintDefaults()
    }

2.解析命令行参数#

使用flag.Parse(),并且做判断,如果输入的是h或者help,将会打印flag设置的参数信息,后退出。如下

    flag.Parse()

    if *h || *help {
        flag.Usage()
        os.Exit(0)
    }

我们使用beego的log将日志打印在文件中,并设置日志级别为7,默认日志会进行滚动

    beego.SetLogger("file",
        `{"filename":
        "logs/web.log",
        "level":7}`)

假如你输入了-v参数,我们就开启debug模式,并将日志输出到屏幕,如果没有使用-v就打印到日志,如下

    if !*verbose {
        beego.BeeLogger.DelLogger("console") // 删除控制台日志
    } else {
        orm.Debug = true
    }

3.初始化orm#

beego的orm模块。我们使用orm链接数据库,导入"github.com/astaxie/beego/orm"

    orm.RegisterDriver("mysql", orm.DRMySQL)                                
    orm.RegisterDataBase("default", "mysql", beego.AppConfig.String("dsn"))

我们使用mysql驱动,导入_ "github.com/go-sql-driver/mysql"

4.测试数据库链接是否正常#

我们还要对数据库的链接进行判断是否正常。我们使用orm.GetDB和db.Ping。如下:

    if db, err := orm.GetDB(); err != nil || db.Ping() != nil {
        beego.Error("数据库连接错误")
        os.Exit(-1)
    }

这里使用beego.Error来抛出错误

5.根据参数选择执行流程#

现在进入参数选择执行的环节,我们使用switch cash

    case *init:
        orm.RunSyncdb("default", *force, *verbose)
        ormer := orm.NewOrm()
        admin := &models.User{Name: "admin", IsSuperman: true}
        if err := ormer.Read(admin, "name"); err == orm.ErrNoRows { // 如果有一行
            password := utils.RandString(6)
            admin.SetPassword(password)
            if _, err := ormer.Insert(admin); err == nil {
                beego.Informational("初始化admin成功,admin默认密码:", password)
            } else {
                beego.Error("初始化用户失败,错误:", err)
            }
        } else {
            beego.Informational("admin用户已存在,将会跳过")
        }
        beego.Informational("初始化用户")

当你输入-init时候,首先会做数据库的同步,orm.RunSyncdb, 创建一个 Ormer(NewOrm 的同时会执行 orm.BootStrap (整个 app 只执行一次),用以验证模型之间的定义并缓存。)

而后调用&models.User{Name: "admin", IsSuperman: true}赋值给admin

意思指想models模块下的User,User是一个结构体,其中包括有Name,和IsSuperman,name是名称,IsSuperman表示权限控制。当赋值了这两个参数后,将会被创建到数据库中

而后ormer.Read(admin,"name"),读取admin是否存在,如果存在,将会调用utils.RandString(6),而utils.RandString(6)是一串已经存在的预设密码串。

而后调用已经被赋值的admin。也就是在models模块中的SetPassword函数,将utils.RandString(6)赋值给password的结果传递给SetPassword。SetPassword是在models定义的方法,如下:

func (u *User) SetPassword(password string) {
    u.Password = utils.Md5Salt(password, "")
}

再接着,使用ormer.Insert(admin)插入,并且判断是否有错误,如果没有,就打印密码。如果没有就创建,需要syncdb

syncdb会执行如上过程

        orm.RunSyncdb("default", *force, *verbose)
        beego.Informational("同步数据库")

默认将会启动

beego.Run()

导入beego包"github.com/astaxie/beego"

第二部分#

conf配置文件,我们创建app.conf,在conf目录下,在conf/app.conf的配置部分是可以被代码应用的。这在后面将会提到

其中,我们配置如下:

appname=CMDB
runmode = ${RUNMODE||dev}

[dev]
sessionon = true
sessionprovider = file
sessionproviderconfig = temp/session
sessionname = sid
enablexsrf = true 
xsrfexpire = 3600
xsrfkey = U2F0IE5vdiAxNiAxMTowMDozNCBDU1QgMjAxOQo
httpport=8080

login=AuthController.Login
home=TestController.Test

include "db.conf"

[prod]
httpport=8081

db.conf

dsn=orm_test:passwordyace@tcp(172.25.50.250:3306)/gocmdb?charset=utf8mb4&parseTime=true&loc=Local

这里设置的RUNMODE是可以设置本地环境变量进行指定的。其中temp是存放session。login=AuthController.Login,home=TestController.Test也是在代码中进行调用的。

第三部分#

在controller下分了三个部分,我们先看base部分

base.go#

我们在controller下创建auth和base两个目录,在base目录下,我们创建一个BaseControole的结构体

package base

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

type BaseController struct {
    beego.Controller
}

auth.go#

登陆认证#

而后在auth下创建auth.go,其中创建LoginRequiredController结构体

package auth

import (
    "server/controller/base"
)
type LoginRequiredController struct {
    base.BaseController
}

我们在auth文件中的LoginRequiredController结构体外配置认证逻辑

func (c *LoginRequiredController) Prepare() {
    c.BaseController.Prepare()
    // 判断是Session认证还是Token认证
    // 如果是Session - c.GetSession("")
    // 如果是Token - Header ACCESSKEY SIGNATURE
    //manager := NewManager()
    if user := DefaultManage.IsLogin(c); user == nil {   // 未登录
        DefaultManage.GoToLoginPage(c)  // todo需要修改参数
        c.StopRun() // 停止后面的逻辑
    } else {        // 已经登陆
        c.User = user
        c.Data["user"] = user
    }
}

其中DefaultManage.IsLogin的Islogin是在manager中的函数。并且判断,如果赋值的user等于nil就说明没有登陆。如果没有登陆就使用GoToLoginPage,并且停止后面的逻辑,否则将Data返回

AuthController结构体内只有login和logout两个方法

type AuthController struct {
    base.BaseController
}

func (c *AuthController) Login() {
    DefaultManage.Login(c)
}

func (c *AuthController) logout() {
    DefaultManage.Logout(c)
}

其中一个被写入到配置文件中,在插件plugin中被调用login=AuthController.Login

manager.go#

manager中我们定义了接口,分别处理不同的操作,接口内的每个定义的类型也会定义相应的方法,这些方法在plugin中被调用

type AuthPlugin interface {
    Name() string
    Is(*context.Context) bool
    IsLogin(*LoginRequiredController) *models.User
    GoToLoginPage(*LoginRequiredController)
    Login(*AuthController) bool
    Logout(*AuthController)
}

导入模板:"github.com/astaxie/beego/context"

而后创建一个Manage的结构体,这个结构体内部存储了映射关系AuthPlugin

type Manager struct {
    plugins map[string]AuthPlugin
}

在创建NewManager函数将Manager返回

func NewManager() *Manager {
    return &Manager{
        plugins: map[string]AuthPlugin{},
    }
}

并且NewManager会在这里设置产量。最终调用DefaultManage来调用控制内的方法

var DefaultManage = NewManager()

而后配置接口中的方法

方法#

注册方法

func (m *Manager) Register(p AuthPlugin) {
    m.plugins[p.Name()] = p
}

检测插件方法.

遍历plugin查看插件存在与否,这里的plugin.Is在plugin的模块中

func (m *Manager) GetPlugin(c *context.Context) AuthPlugin {
    for _, plugin := range m.plugins {
        if plugin.Is(c) {
            return plugin
        }
    }
    return nil
}

检测插件

如果调用的是某个插件就调用某个插件的功能

func (m *Manager) IsLogin(c *LoginRequiredController) *models.User {
    if plugin := m.GetPlugin(c.Ctx); plugin != nil {
        return plugin.IsLogin(c)
    }
    return nil
}

跳转到登陆页面的方法

func (m *Manager) GoToLoginPage(c *LoginRequiredController) {
    if plugin := m.GetPlugin(c.Ctx); plugin != nil {
        plugin.GoToLoginPage(c)
    }
}

登陆和推出

func (m *Manager) Login(c *AuthController) bool {
    if plugin := m.GetPlugin(c.Ctx); plugin != nil {
        return plugin.Login(c)
    }
    return false
}
func (m *Manager) Logout(c *AuthController) {
    if plugin := m.GetPlugin(c.Ctx); plugin != nil {
        plugin.Logout(c)
    }
}

第三部分#

plugin.go#

创建Session的结构体

type Session struct{}
func (s *Session) Name() string { return "session" }
func (s *Session) Is(c *context.Context) bool {
    return c.Input.Header("Authentication") == ""
}

如果用户的session存在就返回

func (s *Session) IsLogin(c *LoginRequiredController) *models.User {
    if session := c.GetSession("user"); session != nil {
        if uid, ok := session.(int); ok {
            return models.DefaultUserManager.GetByID(uid)
        }
    }
    return nil
}

这里将会跳转到AuthController.Login,是通过c.Redirect(beego.URLFor(beego.AppConfig.String("login")),中的beego.AppConfig.String("login"),而beego.AppConfig.String("login")在app.conf中定义

func (s *Session) GoToLoginPage(c *LoginRequiredController) {
    c.Redirect(beego.URLFor(beego.AppConfig.String("login")), http.StatusFound)
}

这里设计到forms.LoginForm,LoginForm是一个结构体,用做用户名密码的实际登陆检测

如果输入的是 post请求正常,开始验证表单中是否有错误,如果ok加入到表单。并且跳转到home。home这里也在app.conf中配置。home=TestController.Test对应的是/test/test。登陆失败跳转到auth/login.html

func (s *Session) Login(c *AuthController) bool {
    form := &forms.LoginForm{}
    valid := &validation.Validation{}
    if c.Ctx.Input.IsPost() {
        if err := c.ParseForm(form); err != nil {
            valid.SetError("error", err.Error())
        } else {
            if ok, err := valid.Valid(form); err != nil {
                valid.SetError("error", err.Error())
            } else if ok {
                c.SetSession("user", form.User.Id)
                c.Redirect(beego.URLFor(beego.AppConfig.String("home")), http.StatusFound)
                return true
            }
        }
    }
    // 登陆失败跳转到登陆界面
    c.TplName = "auth/login.html"
    c.Data["form"] = form
    c.Data["valid"] = valid
    return false
}

beego.AppConfig.String("login") 指向app.conf文件中的login=AuthController.Login

func (s *Session) Logout(c *AuthController) {
    c.DestroySession()
    c.Redirect(beego.URLFor(beego.AppConfig.String("login")), http.StatusFound)
}

token#

第四部分#

form#

在form下面有auth.go文件,其中LoginForm结构体是用作用户验证

type LoginForm struct {
    Name     string `form:"name"`
    Password string `form:"password"`
    User     *models.User
}

登陆验证

func (f *LoginForm) Valid(v *validation.Validation) {
    f.Name = strings.TrimSpace(f.Name)
    f.Password = strings.TrimSpace(f.Password)

    fmt.Println(f.Name, f.Password)

    if f.Name == "" || f.Password == "" {
        v.SetError("error", "用户名密码错误")
    } else {
        // 通过name去查找用户
        if user := models.DefaultUserManager.GetByName(f.Name); user == nil || !user.ValidatePassword(f.Password) {
            fmt.Println("Valid-name:", f.Name, user)
            fmt.Println("Valid-password:", f.Password)
            // 查到后调用ValidatePassword进行验证
            v.SetError("error", "用户名或密码错误")
        } else if user.IsLock() {
            v.SetError("error", "用户名被锁定")
        } else {
            f.User = user
        }
        // Lock状态检查
    }
}

其中判断用户名密码是否为空,如果不是空就会去models模块下的DefaultUserManager.GetByName查找name是否存在,并且调用user下的ValidatePassword将加盐的密码判断返回是否为ture。如果正常返回

第五部分#

在models的User中,我们需要使用orm创建数据库的表结构以及指定的Token

user.go#

结构体如下:

type User struct {
    Id          int        `orm:"column(id);"`
    Name        string     `orm:"column(name);size(32)"`
    Password    string     `orm:"column(password);size(1024)"`
    Gender      int        `orm:"column(gender);default(0);"`
    Birthday    *time.Time `orm:"column(birthday);null;default(null)"`
    Tel         string     `orm:"column(tel);size(1024)"`
    Email       string     `orm:"column(email);size(1024)"`
    Addr        string     `orm:"column(addr);size(1024)"`
    Remark      string     `orm:"column(remark);size(1024)"`
    IsSuperman  bool       `orm:"column(is_superman);default(false);"`
    Status      int        `orm:"column(status)"`
    CreatedTime *time.Time `orm:"column(createdtime);auto_now_add"`
    UpdatedTime *time.Time `orm:"column(updatedtime);auto_now"`
    DeletedTime *time.Time `orm:"column(deletedtime);null;default(null)"`
}

在user中第一个方法是进行配置password,调用的是utils.Md5Salt,utils.Md5Salt在下面查看

func (u *User) SetPassword(password string) {
    u.Password = utils.Md5Salt(password, "")
}

还有一个加盐的方法ValidatePassword,而ValidatePassword首先获取原来的盐,而后通过当前密码进行加密,并且与原来的密码进行比较,返回一个Bool的true或者false

func (u *User) ValidatePassword(password string) bool {
    salt, _ := utils.SplitMd5Salt(u.Password) 
    return utils.Md5Salt(password, salt) == u.Password 
}

这里做了一个IsLock来锁定状态

func (u *User) IsLock() bool {
    return u.Status == StatusLock
}

用户验证#

UserManager#

UserManager用来验证用户名密码,我们创建UserManager的结构体,并定义一个新函数

type UserManager struct {
}

func NewUserManager() *UserManager {
    return &UserManager{}
}

而后会进行实例化

var DefaultUserManager = NewUserManager()

查找到用户ID,返回单条数据

func (m *UserManager) GetByID(id int) *User { // 获取用户id
    user := &User{}
    ormer := orm.NewOrm()

    err := ormer.QueryTable(user).Filter("Id__exact", id).Filter("DeletedTime__isnull", true).One(user)
    if err == nil {
        return user
    }
    return nil
}

查找到用户,返回单条数据

func (m *UserManager) GetByName(name string) *User { // 获取用户名
    user := &User{}
    err := orm.NewOrm().QueryTable(user).Filter("Name__exact", name).Filter("DeletedTime__isnull", true).One(user)

    if err == nil {
        return user
    }
    return nil
}

user token部分#

创建token结构体

type Token struct {
    Id          int        `orm:"column(id);"`
    User        *User      `orm:"column(user);rel(one);"`
    AccessKey   string     `orm:"column(access_key);size(1024);"`
    Secrectkey  string     `orm:"column(secrect_key);size(1024);"`
    CreatedTime *time.Time `orm:"column(created_time);auto_now_add;"`
    UpdatedTime *time.Time `orm:"column(updated_time);auto_now;"`
}

创建tokenMager结构体。并且创建一个新函数NewToeknManger将TokenManager返回

type TokenManager struct{}

func NewTokenManager() *TokenManager {
    return &TokenManager{}
}

获取数据库内的key

func (m *TokenManager) GetByKey(accesskey, secrectkey string) *Token {
    token := &Token{AccessKey: accesskey, Secrectkey: secrectkey}
    ormer := orm.NewOrm()
    if err := ormer.Read(token, "AccessKey", "SecrectKey"); err == nil {
        fmt.Println(token)

        ormer.LoadRelated(token, "User") // LoadRelated 获取token
        return token
    }

这里需要在User结构体中加入这个佳构体

Token *Token `orm:"reverse(one);"`

第六部分#

utils和routers#

ramd如下

package utils

import (
    "math/rand"
    "time"
)

func RandString(length int) string {
    letters := "abcdefSXXXASXZAkjxkljczoijiqll[]=-+#@#!$!%# SA5512x/**-"
    count := len(letters)
    chars := make([]byte, length)

    for i := 0; i < length; i++ {
        chars[i] = letters[rand.Int()%count]
    }

    return string(chars)
}

// 时间种子
func init() {
    rand.Seed(time.Now().UnixNano())
}

如上所示,在RandString中有 一串原始的字符串用作对以时间为种子的密码的加密

在crypto中对密码字符串进行了处理,在Md5Salt中的加盐部分传入上面的函数,而后返回一串密码

package utils

import (
    "crypto/md5"
    "fmt"
    "strings"
)

func Md5Salt(text string, salt string) string {
    if salt == "" {
        salt = RandString(8)
    }
    return fmt.Sprintf("%s:%x", salt, md5.Sum([]byte(fmt.Sprintf("%s:%s", salt, text))))

}
func SplitMd5Salt(text string) (string, string) {
    nodes := strings.SplitN(text, ":", 2)
    if len(nodes) >= 2 {
        return nodes[0], nodes[1]
    } else {
        return "", nodes[0]
    }
}
package router

import (
    "server/controller/auth"

    "github.com/astaxie/beego"
    "server/controller"
)

func init() {
    beego.AutoRouter(&auth.AuthController{})
    beego.AutoRouter(&controller.TestController{})
}