我们即将使用beego来构建一个cmdb,会用到很多beego的东西,包括。orm,context,log等。
创建目录每个目录代表不同的作用,我们手动创建即可。分别有conf,controller,forms,logs,models,routers,staic,temp,utils,views
而后使用go mod init web
进行初始化
第一部分#
我们想从第一部分main.go开始,命令行参数,命令行参数要满足一些基本的需要。如下
- 通过命令行参数来对cmdb初始化
- 初始化admin用户密码
- 创建应该有的表
- 强制初始化
- 不强制初始化,但同步表结构,执行语句
1,命令行参数#
我们会对几个关键字做输入解析,将会使用到flag,理想中,我们至少需要输入如下参数来完成开始部分。大概需要经过五个部分
- 1.初始化命令行参数
- 2.解析命令行参数
- 3.初始化orm
- 4.测试数据库链接是否正常
- 5.根据参数选择执行流程
1.初始化命令行参数#
- h : 帮助信息
- help : 帮助信息
- init : 初始化
- force: 强制初始化
- syncdb: 同步数据库
我们使用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"
- 注册mysql驱动
orm.RegisterDriver("mysql", orm.DRMySQL)
- 链接数据库
orm.RegisterDataBase("default", "mysql", beego.AppConfig.String("dsn"))
我们使用mysql驱动,导入_ "github.com/go-sql-driver/mysql"
beego.AppConfig.String
使用beego的内置函数调用的是conf/app.conf中的dns,通过这种方式读取。而在app.conf中,我们include=db.conf。db.conf中的内容便是配置的mysql细节:dsn=orm_test:passwordyace@tcp(172.25.50.250:3306)/gocmdb?charset=utf8mb4&parseTime=true&loc=Local
这些数据库的库是需要创建和授权。
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
- init
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
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#
登陆认证#
- LoginRequiredController
而后在auth下创建auth.go,其中创建LoginRequiredController结构体
package auth
import (
"server/controller/base"
)
type LoginRequiredController struct {
base.BaseController
}
我们在auth文件中的LoginRequiredController结构体外配置认证逻辑
-
- prepare方法
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
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结构体
创建Session的结构体
type Session struct{}
-
- 创建name 的方法
func (s *Session) Name() string { return "session" }
-
- 创建Is方法,Is方法在manager的GetPlugin中被调用做检测插件
func (s *Session) Is(c *context.Context) bool {
return c.Input.Header("Authentication") == ""
}
-
- 创建Islogin。这里的
models.DefaultUserManager.GetByID(uid)
获取的是models模块下的GetByID
- 创建Islogin。这里的
如果用户的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
}
-
- 创建GoToLoginPage
这里将会跳转到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)
}
-
- login登陆
这里设计到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
}
-
- logout
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()
-
- GetByID
查找到用户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
}
-
- GetByName
查找到用户,返回单条数据
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#
- utils中分别有两个文件是crypto和ramd两个文件
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]
}
}
- routers
package router
import (
"server/controller/auth"
"github.com/astaxie/beego"
"server/controller"
)
func init() {
beego.AutoRouter(&auth.AuthController{})
beego.AutoRouter(&controller.TestController{})
}