Beego --- 其他

6. 错误处理

1. 错误处理

我们在做 Web 开发的时候,经常需要页面跳转和错误处理,beego 这方面也进行了考虑,通过 Redirect 方法来进行跳转:

func (this *AddController) Get() {
    this.Redirect("/", 302)
}

如何中止此次请求并抛出异常,beego 可以在控制器中这样操作:

func (this *MainController) Get() {
    this.Abort("401")
    v := this.GetSession("asta")
    if v == nil {
        this.SetSession("asta", int(1))
        this.Data["Email"] = 0
    } else {
        this.SetSession("asta", v.(int)+1)
        this.Data["Email"] = v.(int)
    }
    this.TplName = "index.tpl"
}

这样 this.Abort("401") 之后的代码不会再执行,而且会默认显示给用户如下页面:

beego 框架默认支持 401、403、404、500、503 这几种错误的处理。用户可以自定义相应的错误处理,例如下面重新定义 404 页面:

func page_not_found(rw http.ResponseWriter, r *http.Request){
    t,_:= template.New("404.html").ParseFiles(beego.BConfig.WebConfig.ViewsPath+"/404.html")
    data :=make(map[string]interface{})
    data["content"] = "page not found"
    t.Execute(rw, data)
}

func main() {
    beego.ErrorHandler("404",page_not_found)
    beego.Router("/", &controllers.MainController{})
    beego.Run()
}

我们可以通过自定义错误页面 404.html 来处理 404 错误。

beego 更加人性化的还有一个设计就是支持用户自定义字符串错误类型处理函数,例如下面的代码,用户注册了一个数据库出错的处理页面:

func dbError(rw http.ResponseWriter, r *http.Request){
    t,_:= template.New("dberror.html").ParseFiles(beego.BConfig.WebConfig.ViewsPath+"/dberror.html")
    data :=make(map[string]interface{})
    data["content"] = "database is now down"
    t.Execute(rw, data)
}

func main() {
    beego.ErrorHandler("dbError",dbError)
    beego.Router("/", &controllers.MainController{})
    beego.Run()
}

一旦在入口注册该错误处理代码,那么你可以在任何你的逻辑中遇到数据库错误调用 this.Abort("dbError") 来进行异常页面处理。

2. controller 定义 Error

从 1.4.3 版本开始,支持 Controller 方式定义 Error 错误处理函数,这样就可以充分利用系统自带的模板处理,以及 context 等方法。

package controllers

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

type ErrorController struct {
    beego.Controller
}

func (c *ErrorController) Error404() {
    c.Data["content"] = "page not found"
    c.TplName = "404.tpl"
}

func (c *ErrorController) Error501() {
    c.Data["content"] = "server error"
    c.TplName = "501.tpl"
}


func (c *ErrorController) ErrorDb() {
    c.Data["content"] = "database is now down"
    c.TplName = "dberror.tpl"
}

通过上面的例子我们可以看到,所有的函数都是有一定规律的,都是 Error 开头,后面的名字就是我们调用 Abort 的名字,例如 Error404 函数其实调用对应的就是 Abort("404")

我们就只要在 beego.Run 之前采用 beego.ErrorController 注册这个错误处理函数就可以了

package main

import (
    _ "btest/routers"
    "btest/controllers"

    "github.com/astaxie/beego"
)

func main() {
    beego.ErrorController(&controllers.ErrorController{})
    beego.Run()
}

7. View 视图

1. Request

func (c *BMainController) Post() {
        // 获取字符串类型的数据
	name := c.GetStirng("字段名")

        // 获取数值类型的数据........Get数据类型()
        id,err := c.GetInt("字段名")


        // 获取文件
        f,h,err := c.GetFile("字段名")
        defer f.Close()  // 关闭文件句柄
        
        endName := path.Ext(fileName)  // 获取文件后缀
      . size := h.Size   // 字节数


        if err != nil{
            // 文件上传错误
            return
        } else{
            c.SaveToFile("文件名称","存放路径/" + h.FileName)
        }

        // 设置cookie
        c.Ctx.SetCookie("key","value",time)
        c.Ctx.GetCookie("key")

        // cookie失效
        c.Ctx.SetCookie("key","value",-1)
        
}

1. form-data 获取参数(表单数据提交)

我们经常需要获取用户传递的数据,包括 Get、POST 等方式的请求,beego 里面会自动解析这些数据,你可以通过如下方式获取数据:

GetString(key string) string
GetStrings(key string) []string
GetInt(key string) (int64, error)
GetBool(key string) (bool, error)
GetFloat(key string) (float64, error)

使用例子如下:

func (this *MainController) Post() {
    jsoninfo := this.GetString("jsoninfo")
    if jsoninfo == "" {
        this.Ctx.WriteString("jsoninfo is empty")
        return
    }
}

如果你需要的数据可能是其他类型的,例如是 int 类型而不是 int64,那么你需要这样处理:

func (this *MainController) Post() {
    id := this.Input().Get("id")
    intid, err := strconv.Atoi(id)
}

1.1 直接解析到 struct

如果要把表单里的内容赋值到一个 struct 里,除了用上面的方法一个一个获取再赋值外,beego 提供了通过另外一个更便捷的方式,
就是通过 struct 的字段名或 tag 与表单字段对应直接解析到 struct。
定义 struct:

type user struct {
    Id    int         `form:"-"`
    Name  interface{} `form:"username"`
    Age   int         `form:"age"`
    Email string
}

表单:

<form id="user">
    名字:<input name="username" type="text" />
    年龄:<input name="age" type="text" />
    邮箱:<input name="Email" type="text" />
    <input type="submit" value="提交" />
</form>

Controller 里解析:

func (this *MainController) Post() {
    u := user{}
    if err := this.ParseForm(&u); err != nil {
        //handle error
    }
}

注意:

1. StructTag form 的定义和renderform方法共用一个标签
2. 定义 struct 时,字段名后如果有 form 这个 tag,则会以把 form 表单里的 name 和 tag 的名称一样的字段赋值给这个字段,
否则就会把 form 表单里与字段名一样的表单内容赋值给这个字段。如上面例子中,会把表单中的 username 和 age 分别赋值给 user 里的 Name 和 Age 字段,
而 Email 里的内容则会赋给 Email 这个字段。
3. 调用 Controller ParseForm 这个方法的时候,传入的参数必须为一个 struct 的指针,否则对 struct 的赋值不会成功并返回 xx must be a struct pointer 的错误。
4. 如果要忽略一个字段,有两种办法,一是:字段名小写开头,二是:form 标签的值设置为 -

2. 获取 Request Body 里的内容

在 API 的开发中,我们经常会用到 JSON 或 XML 来作为数据交互的格式,如何在 beego 中获取 Request Body 里的 JSON 或 XML 的数据呢?

  1. 在配置文件里设置 copyrequestbody = true
  2. 在 Controller 中
func (this *ObjectController) Post() {
    var ob models.Object
    var err error
    if err = json.Unmarshal(this.Ctx.Input.RequestBody, &ob); err == nil {
        objectid := models.AddOne(ob)
        this.Data["json"] = "{\"ObjectId\":\"" + objectid + "\"}"
    } else {
        this.Data["json"] = err.Error()
    }
    this.ServeJSON()
}

3. 文件上传

在 beego 中你可以很容易的处理文件上传,就是别忘记在你的 form 表单中增加这个属性

enctype="multipart/form-data"

否则你的浏览器不会传输你的上传文件。

文件上传之后一般是放在系统的内存里面,如果文件的 size 大于设置的缓存内存大小,
那么就放在临时文件中,默认的缓存内存是 64M,你可以通过如下来调整这个缓存内存大小:

beego.MaxMemory = 1<<22

或者在配置文件中通过如下设置:

maxmemory = 1<<22

Beego 提供了两个很方便的方法来处理文件上传:

  1. GetFile(key string) (multipart.File, *multipart.FileHeader, error)
    该方法主要用于用户读取表单中的文件名 the_file,然后返回相应的信息,用户根据这些变量来处理文件上传:过滤、保存文件等。
  2. SaveToFile(fromfile, tofile string) error
    该方法是在 GetFile 的基础上实现了快速保存的功能 fromfile 是提交时候的 html 表单中的 name
<form enctype="multipart/form-data" method="post">
    <input type="file" name="uploadname" />
    <input type="submit">
</form>

保存的代码的例子如下

func (c *FormController) Post() {
    f, h, err := c.GetFile("uploadname")
    if err != nil {
        log.Fatal("getfile err ", err)
    }
    defer f.Close()
    c.SaveToFile("uploadname", "static/upload/" + h.Filename) // 保存位置在 static/upload, 没有文件夹要先创建
}

4. 数据绑定

支持从用户请求中直接数据 bind 到指定的对象,例如请求地址如下

?id=123&isok=true&ft=1.2&ol[0]=1&ol[1]=2&ul[]=str&ul[]=array&user.Name=astaxie
var id int
this.Ctx.Input.Bind(&id, "id")  //id ==123

var isok bool
this.Ctx.Input.Bind(&isok, "isok")  //isok ==true

var ft float64
this.Ctx.Input.Bind(&ft, "ft")  //ft ==1.2

ol := make([]int, 0, 2)
this.Ctx.Input.Bind(&ol, "ol")  //ol ==[1 2]

ul := make([]string, 0, 2)
this.Ctx.Input.Bind(&ul, "ul")  //ul ==[str array]

user struct{Name}
this.Ctx.Input.Bind(&user, "user")  //user =={Name:"astaxie"}

2. Response

重定向,不能传递数据,速度快,无需渲染

c.Redirect("路径",状态码(int))

返回html页面

c.data["a"] = "bbb"   // 模板取值{{ .a }}
c.TplName = "index.html"

返回字符串

c.Ctx.WriteString("hello world")

返回json数据

package index

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

type BMainController struct {
	beego.Controller
}

type myStruct struct {
	Code int `json:"code"`
	Data interface{} `json:"data"`
	Errmsg string `json:"errmsg"`
}

func (c *BMainController) Get() {
	myMap := make(map[string]int,3)
	myMap["1"] = 3
	myMap["2"] = 33
	myMap["3"] = 333
	myMap["4"] = 3333
	myMap["5"] = 33333
	Mytruct := &myStruct{
		0,&myMap,"",
	}
	c.Data["json"] = Mytruct
	fmt.Println(c.Data["json"])
	c.ServeJSON()
}

1.JSON

beego 当初设计的时候就考虑了 API 功能的设计,而我们在设计 API 的时候经常是输出 JSON 或者 XML 数据,那么 beego 提供了这样的方式直接输出:

注意 struct 属性应该 为 exported Identifier 首字母应该大写

func (this *AddController) Get() {
      mystruct := { ... }
      this.Data["json"] = &mystruct
      this.ServeJSON()
  }

调用 ServeJSON 之后,会设置 content-type 为 application/json,然后同时把数据进行 JSON 序列化输出。

2. XML数据直接输出

  func (this *AddController) Get() {
      mystruct := { ... }
      this.Data["xml"]=&mystruct
      this.ServeXML()
  }

调用 ServeXML 之后,会设置 content-type 为 application/xml,同时数据会进行 XML 序列化输出。

3.Jsonp调用

func (this *AddController) Get() {
      mystruct := { ... }
      this.Data["jsonp"] = &mystruct
      this.ServeJSONP()
  }

调用 ServeJSONP 之后,会设置 content-type 为 application/javascript,然后同时把数据进行 JSON 序列化,然后根据请求的 callback 参数设置 jsonp 输出。

开发模式下序列化后输出的是格式化易阅读的 JSON 或 XML 字符串;在生产模式下序列化后输出的是压缩的字符串。

3. Flash 数据

这个 flash 与 Adobe/Macromedia Flash 没有任何关系。它主要用于在两个逻辑间传递临时数据,flash 中存放的所有数据会在紧接着的下一个逻辑中调用后清除。
一般用于传递提示和错误消息。它适合 Post/Redirect/Get 模式。下面看使用的例子:

// 显示设置信息
func (c *MainController) Get() {
    flash:=beego.ReadFromRequest(&c.Controller)
    if n,ok:=flash.Data["notice"];ok{
        // 显示设置成功
        c.TplName = "set_success.html"
    }else if n,ok=flash.Data["error"];ok{
        // 显示错误
        c.TplName = "set_error.html"
    }else{
        // 不然默认显示设置页面
        c.Data["list"]=GetInfo()
        c.TplName = "setting_list.html"
    }
}

// 处理设置信息
func (c *MainController) Post() {
    flash:=beego.NewFlash()
    setting:=Settings{}
    valid := Validation{}
    c.ParseForm(&setting)
    if b, err := valid.Valid(setting);err!=nil {
        flash.Error("Settings invalid!")
        flash.Store(&c.Controller)
        c.Redirect("/setting",302)
        return
    }else if b!=nil{
        flash.Error("validation err!")
        flash.Store(&c.Controller)
        c.Redirect("/setting",302)
        return
    }
    saveSetting(setting)
    flash.Notice("Settings saved!")
    flash.Store(&c.Controller)
    c.Redirect("/setting",302)
}

上面的代码执行的大概逻辑是这样的:

  1. Get 方法执行,因为没有 flash 数据,所以显示设置页面。
  2. 用户设置信息之后点击递交,执行 Post,然后初始化一个 flash,通过验证,验证出错或者验证不通过设置 flash 的错误,如果通过了就保存设置,然后设置 flash 成功设置的信息。
  3. 设置完成后跳转到 Get 请求。
  4. Get 请求获取到了 Flash 信息,然后执行相应的逻辑,如果出错显示出错的页面,如果成功显示成功的页面。

默认情况下 ReadFromRequest 函数已经实现了读取的数据赋值给 flash,所以在你的模板里面你可以这样读取数据:

{{.flash.error}}
{{.flash.warning}}
{{.flash.notice}}

flash对象有三个级别的设置:

  1. Notice 提示信息
  2. Warning 警告信息
  3. Error 错误信息

8. Session

beego 内置了 session 模块,目前 session 模块支持的后端引擎包括 memory、cookie、file、mysql、redis、couchbase、memcache、postgres,用户也可以根据相应的 interface 实现自己的引擎。

beego 中使用 session 相当方便,只要在 main 入口函数中设置如下:

beego.BConfig.WebConfig.Session.SessionOn = true

或者通过配置文件配置如下:

sessionon = true

通过这种方式就可以开启 session,如何使用 session,请看下面的例子:

func (this *MainController) Get() {
    v := this.GetSession("asta")
    if v == nil {
        this.SetSession("asta", int(1))
        this.Data["num"] = 0
    } else {
        this.SetSession("asta", v.(int)+1)
        this.Data["num"] = v.(int)
    }
    this.TplName = "index.tpl"
}

session 有几个方便的方法:

SetSession(name string, value interface{})
GetSession(name string) interface{}
DelSession(name string)
SessionRegenerateID()
DestroySession()

session 操作主要有设置 session、获取 session、删除 session。
当然你可以通过下面的方式自己控制这些逻辑:

sess:=this.StartSession()
defer sess.SessionRelease()

sess 对象具有如下方法:

sess.Set()
sess.Get()
sess.Delete()
sess.SessionID()
sess.Flush()

但是我还是建议大家采用 SetSession、GetSession、DelSession 三个方法来操作,避免自己在操作的过程中资源没释放的问题。

关于 Session 模块使用中的一些参数设置:

beego.BConfig.WebConfig.Session.SessionOn

# 设置是否开启 Session,默认是 false,配置文件对应的参数名:sessionon。

beego.BConfig.WebConfig.Session.SessionProvider

# 设置 Session 的引擎,默认是 memory,目前支持还有 file、mysql、redis 等,配置文件对应的参数名:sessionprovider。

beego.BConfig.WebConfig.Session.SessionName

# 设置 cookies 的名字,Session 默认是保存在用户的浏览器 cookies 里面的,默认名是 beegosessionID,配置文件对应的参数名是:sessionname。

beego.BConfig.WebConfig.Session.SessionGCMaxLifetime

# 设置 Session 过期的时间,默认值是 3600 秒,配置文件对应的参数:sessiongcmaxlifetime。

beego.BConfig.WebConfig.Session.SessionProviderConfig

# 设置对应 file、mysql、redis 引擎的保存路径或者链接地址,默认值是空,配置文件对应的参数:sessionproviderconfig。

beego.BConfig.WebConfig.Session.SessionHashFunc

# 默认值为 sha1,采用 sha1 加密算法生产 sessionid

beego.BConfig.WebConfig.Session.SessionHashKey

# 默认的 key 是 beegoserversessionkey,建议用户使用的时候修改该参数

beego.BConfig.WebConfig.Session.SessionCookieLifeTime

# 设置 cookie 的过期时间,cookie 是用来存储保存在客户端的数据。

从 beego1.1.3 版本开始移除了第三方依赖库,也就是如果你想使用 mysql、redis、couchbase、memcache、postgres 这些引擎,那么你首先需要安装

go get -u github.com/astaxie/beego/session/mysql

然后在你的 main 函数中引入该库, 和数据库的驱动引入是一样的:

import _ "github.com/astaxie/beego/session/mysql"

当 SessionProvider 为 file SessionProviderConfig 是指保存文件的目录,如下所示:

beego.BConfig.WebConfig.Session.SessionProvider="file"
beego.BConfig.WebConfig.Session.SessionProviderConfig = "./tmp"

当 SessionProvider 为 mysql 时,SessionProviderConfig 是链接地址,采用 go-sql-driver,如下所示:

beego.BConfig.WebConfig.Session.SessionProvider = "mysql"
beego.BConfig.WebConfig.Session.SessionProviderConfig = "username:password@protocol(address)/dbname?param=value"

# 需要特别注意的是,在使用 mysql 存储 session 信息的时候,需要事先在 mysql 创建表,建表语句如下
 CREATE TABLE `session` (
        `session_key` char(64) NOT NULL,
        `session_data` blob,
        `session_expiry` int(11) unsigned NOT NULL,
        PRIMARY KEY (`session_key`)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8;

当 SessionProvider 为 redis 时,SessionProviderConfig 是 redis 的链接地址,采用了 redigo,如下所示:

beego.BConfig.WebConfig.Session.SessionProvider = "redis"
beego.BConfig.WebConfig.Session.SessionProviderConfig = "127.0.0.1:6379"

当 SessionProvider 为 memcache 时,SessionProviderConfig 是 memcache 的链接地址,采用了 memcache,如下所示:

beego.BConfig.WebConfig.Session.SessionProvider = "memcache"
beego.BConfig.WebConfig.Session.SessionProviderConfig = "127.0.0.1:7080"

当 SessionProvider 为 postgres 时,SessionProviderConfig 是 postgres 的链接地址,采用了 postgres,如下所示:

beego.BConfig.WebConfig.Session.SessionProvider = "postgresql"
beego.BConfig.WebConfig.Session.SessionProviderConfig = "postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full"

当 SessionProvider 为 couchbase 时,SessionProviderConfig 是 couchbase 的链接地址,采用了 couchbase,如下所示:

beego.BConfig.WebConfig.Session.SessionProvider = "couchbase"
beego.BConfig.WebConfig.Session.SessionProviderConfig = "http://bucketname:bucketpass@myserver:8091"

因为 session 内部采用了 gob 来注册存储的对象,例如 struct,所以如果你采用了非 memory 的引擎,请自己在 main.go 的 init 里面注册需要保存的这些结构体,不然会引起应用重启之后出现无法解析的错误

9. 控制器函数

1. 基本使用

提示:在 v1.6 中,此文档所涉及的 API 有重大变更,this.ServeJson() 更改为 this.ServeJSON(),this.TplNames 更改为 this.TplName

基于 beego 的 Controller 设计,只需要匿名组合 beego.Controller 就可以了,如下所示:

type xxxController struct {
    beego.Controller
}

beego.Controller 实现了接口 beego.ControllerInterface,beego.ControllerInterface 定义了如下函数:

Init(ct *context.Context, childName string, app interface{})

# 这个函数主要初始化了 Context、相应的 Controller 名称,模板名,初始化模板参数的容器 Data,app 即为当前执行的 Controller 的 reflecttype,这个 app 可以用来执行子类的方法。

Prepare()

# 这个函数主要是为了用户扩展用的,这个函数会在下面定义的这些 Method 方法之前执行,用户可以重写这个函数实现类似用户验证之类。

Get()

# 如果用户请求的 HTTP Method 是 GET,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Get 请求。

Post()

# 如果用户请求的 HTTP Method 是 POST,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Post 请求。

Delete()

# 如果用户请求的 HTTP Method 是 DELETE,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Delete 请求。

Put()

# 如果用户请求的 HTTP Method 是 PUT,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Put 请求.

Head()

# 如果用户请求的 HTTP Method 是 HEAD,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Head 请求。

Patch()

# 如果用户请求的 HTTP Method 是 PATCH,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Patch 请求.

Options()

# 如果用户请求的HTTP Method是OPTIONS,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Options 请求。

Finish()

# 这个函数是在执行完相应的 HTTP Method 方法之后执行的,默认是空,用户可以在子 struct 中重写这个函数,执行例如数据库关闭,清理数据之类的工作。

Render() error

# 这个函数主要用来实现渲染模板,如果 beego.AutoRender 为 true 的情况下才会执行。

所以通过子 struct 的方法重写,用户就可以实现自己的逻辑,接下来我们看一个实际的例子:

ype AddController struct {
    beego.Controller
}

func (this *AddController) Prepare() {

}

func (this *AddController) Get() {
    this.Data["content"] = "value"
    this.Layout = "admin/layout.html"
    this.TplName = "admin/add.tpl"
}

func (this *AddController) Post() {
    pkgname := this.GetString("pkgname")
    content := this.GetString("content")
    pk := models.GetCruPkg(pkgname)
    if pk.Id == 0 {
        var pp models.PkgEntity
        pp.Pid = 0
        pp.Pathname = pkgname
        pp.Intro = pkgname
        models.InsertPkg(pp)
        pk = models.GetCruPkg(pkgname)
    }
    var at models.Article
    at.Pkgid = pk.Id
    at.Content = content
    models.InsertArticle(at)
    this.Ctx.Redirect(302, "/admin/index")
}

从上面的例子可以看出来,通过重写方法可以实现对应 method 的逻辑,实现 RESTful 结构的逻辑处理。
下面我们再来看一种比较流行的架构,首先实现一个自己的基类 baseController,实现一些初始化的方法,然后其他所有的逻辑继承自该基类:

type NestPreparer interface {
        NestPrepare()
}

// baseRouter implemented global settings for all other routers.
type baseController struct {
        beego.Controller
        i18n.Locale
        user    models.User
        isLogin bool
}
// Prepare implemented Prepare method for baseRouter.
func (this *baseController) Prepare() {

        // page start time
        this.Data["PageStartTime"] = time.Now()

        // Setting properties.
        this.Data["AppDescription"] = utils.AppDescription
        this.Data["AppKeywords"] = utils.AppKeywords
        this.Data["AppName"] = utils.AppName
        this.Data["AppVer"] = utils.AppVer
        this.Data["AppUrl"] = utils.AppUrl
        this.Data["AppLogo"] = utils.AppLogo
        this.Data["AvatarURL"] = utils.AvatarURL
        this.Data["IsProMode"] = utils.IsProMode

        if app, ok := this.AppController.(NestPreparer); ok {
                app.NestPrepare()
        }
}

上面定义了基类,大概是初始化了一些变量,最后有一个 Init 函数中那个 app 的应用,
判断当前运行的 Controller 是否是 NestPreparer 实现,如果是的话调用子类的方法,下面我们来看一下 NestPreparer 的实现:

type BaseAdminRouter struct {
    baseController
}

func (this *BaseAdminRouter) NestPrepare() {
    if this.CheckActiveRedirect() {
            return
    }

    // if user isn't admin, then logout user
    if !this.user.IsAdmin {
            models.LogoutUser(&this.Controller)

            // write flash message
            this.FlashWrite("NotPermit", "true")

            this.Redirect("/login", 302)
            return
    }

    // current in admin page
    this.Data["IsAdmin"] = true

    if app, ok := this.AppController.(ModelPreparer); ok {
            app.ModelPrepare()
            return
    }
}

func (this *BaseAdminRouter) Get(){
    this.TplName = "Get.tpl"
}

func (this *BaseAdminRouter) Post(){
    this.TplName = "Post.tpl"
}

这样我们的执行器执行的逻辑是这样的,首先执行 Prepare,这个就是 Go 语言中 struct 中寻找方法的顺序,
依次往父类寻找。执行 BaseAdminRouter 时,查找他是否有 Prepare 方法,没有就寻找 baseController,
找到了,那么就执行逻辑,然后在 baseController 里面的 this.AppController 即为当前执行的控制器
BaseAdminRouter,因为会执行 BaseAdminRouter.NestPrepare 方法。然后开始执行相应的 Get 方法或者 Post 方法。

2. 提前终止运行

我们应用中经常会遇到这样的情况,在 Prepare 阶段进行判断,如果用户认证不通过,就输出一段信息,
然后直接中止进程,之后的 Post、Get 之类的不再执行,那么如何终止呢?可以使用 StopRun 来终止执行逻辑,
可以在任意的地方执行。

type RController struct {
    beego.Controller
}

func (this *RController) Prepare() {
    this.Data["json"] = map[string]interface{}{"name": "astaxie"}
    this.ServeJSON()
    this.StopRun()
}

调用 StopRun 之后,如果你还定义了 Finish 函数就不会再执行,如果需要释放资源,
那么请自己在调用 StopRun 之前手工调用 Finish 函数

3. 在表单中使用 PUT 方法

首先要说明, 在 XHTML 1.x 标准中, 表单只支持 GET 或者 POST 方法. 虽然说根据标准, 你不应该将表单提交到 PUT 方法,
但是如果你真想的话, 也很容易, 通常可以这么做:

首先表单本身还是使用 POST 方法提交, 但是可以在表单中添加一个隐藏字段:

<form method="post" ...>
  <input type="hidden" name="_method" value="put" />

接着在 Beego 中添加一个过滤器来判断是否将请求当做 PUT 来解析:

ar FilterMethod = func(ctx *context.Context) {
    if ctx.BeegoInput.Query("_method")!="" && ctx.BeegoInput.IsPost(){
          ctx.Request.Method = ctx.BeegoInput.Query("_method")
    }
}

beego.InsertFilter("*", beego.BeforeRouter, FilterMethod)

10. 过滤器 (中间件)

1.基本使用

beego 支持自定义过滤中间件,例如安全验证,强制跳转等。
过滤器函数如下所示:

beego.InsertFilter(pattern string, position int, filter FilterFunc, params ...bool)

InsertFilter 函数的三个必填参数,一个可选参数

pattern 路由规则,可以根据一定的规则进行路由,如果你全匹配可以用 *

position 执行 Filter 的地方,五个固定参数如下,分别表示不同的执行过程
        --- BeforeStatic 静态地址之前
        --- BeforeRouter 寻找路由之前
        --- BeforeExec 找到路由之后,开始执行相应的 Controller 之前
        --- AfterExec 执行完 Controller 逻辑之后执行的过滤器
        --- FinishRouter 执行完逻辑之后执行的过滤器

filter filter 函数 type FilterFunc func(*context.Context)

params
  1.设置 returnOnOutput 的值(默认 true), 如果在进行到此过滤之前已经有输出,是否不再继续执行此过滤器,默认设置为如果前面已有输出(参数为true),则不再执行此过滤器
  2. 是否重置 filters 的参数,默认是 false,因为在 filters 的 pattern 和本身的路由的 pattern 冲突的时候,可以把 filters 的参数重置,这样可以保证在后续的逻辑中获取到正确的参数,
        例如设置了 /api/* 的 filter,同时又设置了 /api/docs/* 的 router,那么在访问 /api/docs/swagger/abc.js 的时候,在执行 filters 的时候设置 :splat 参数为 docs/swagger/abc.js,
        但是如果不清楚 filter 的这个路由参数,就会在执行路由逻辑的时候保持 docs/swagger/abc.js,如果设置了 true,就会重置 :splat 参数.

AddFilter 从beego1.3 版本开始已经废除

如下例子所示,验证用户是否已经登录,应用于全部的请求:

var FilterUser = func(ctx *context.Context) {
    _, ok := ctx.Input.Session("uid").(int)
    if !ok && ctx.Request.RequestURI != "/login" {
        ctx.Redirect(302, "/login")
    }
}

beego.InsertFilter("/*",beego.BeforeRouter,FilterUser)

这里需要特别注意使用 session 的 Filter 必须在 BeforeStatic 之后才能获取,因为 session 没有在这之前初始化。

还可以通过正则路由进行过滤,如果匹配参数就执行:

var FilterUser = func(ctx *context.Context) {
    _, ok := ctx.Input.Session("uid").(int)
    if !ok {
        ctx.Redirect(302, "/login")
    }
}
beego.InsertFilter("/user/:id([0-9]+)",beego.BeforeRouter,FilterUser)

2. 过滤器实现路由

beego1.1.2 开始 Context.Input 中增加了 RunController 和 RunMethod, 这样我们就可以在执行路由查找之前,在 filter 中实现自己的路由规则.

如下示例实现了如何实现自己的路由规则:

var UrlManager = func(ctx *context.Context) {
    // 数据库读取全部的 url mapping 数据
    urlMapping := model.GetUrlMapping()
    for baseurl,rule:=range urlMapping {
        if baseurl == ctx.Request.RequestURI {
            ctx.Input.RunController = rule.controller
            ctx.Input.RunMethod = rule.method
            break
        }
    }
}

beego.InsertFilter("/*",beego.BeforeRouter,UrlManager)

11. 日志

beego 之前介绍的时候说过是基于几个模块搭建的,beego 的日志处理是基于 logs 模块搭建的,内置了一个变量 BeeLogger,默认已经是 logs.BeeLogger 类型,初始化了 console,也就是默认输出到 console。

1. 控制台输出

一般在程序中我们使用如下的方式进行输出:

beego.Emergency("this is emergency")
beego.Alert("this is alert")
beego.Critical("this is critical")
beego.Error("this is error")
beego.Warning("this is warning")
beego.Notice("this is notice")
beego.Informational("this is informational")
beego.Debug("this is debug")

2. 写入日志文件

我们的程序往往期望把信息输出到 log 中,现在设置输出到文件很方便,如下所示:

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

更多详细的日志配置请查看日志配置

这个默认情况就会同时输出到两个地方,一个 console,一个 file,如果只想输出到文件,就需要调用删除操作:

beego.BeeLogger.DelLogger("console")

3. 设置日志级别

日志的级别如上所示的代码这样分为八个级别:

LevelEmergency
LevelAlert
LevelCritical
LevelError
LevelWarning
LevelNotice
LevelInformational
LevelDebug

级别依次降低,默认全部打印,但是一般我们在部署环境,可以通过设置级别设置日志级别

beego.SetLevel(beego.LevelInformational)

4. 输出文件名和行号

日志默认不输出调用的文件名和文件行号,如果你期望输出调用的文件名和文件行号,可以如下设置

beego.SetLogFuncCall(true)

开启传入参数 true, 关闭传入参数 false, 默认是关闭的.

5. 完整示例

func internalCalculationFunc(x, y int) (result int, err error) {
    beego.Debug("calculating z. x:", x, " y:", y)
    z := y
    switch {
    case x == 3:
        beego.Debug("x == 3")
        panic("Failure.")
    case y == 1:
        beego.Debug("y == 1")
        return 0, errors.New("Error!")
    case y == 2:
        beego.Debug("y == 2")
        z = x
    default:
        beego.Debug("default")
        z += x
    }
    retVal := z - 3
    beego.Debug("Returning ", retVal)

    return retVal, nil
}

func processInput(input inputData) {
    defer func() {
        if r := recover(); r != nil {
            beego.Error("Unexpected error occurred: ", r)
            outputs <- outputData{result: 0, error: true}
        }
    }()
    beego.Informational("Received input signal. x:", input.x, " y:", input.y)

    res, err := internalCalculationFunc(input.x, input.y)
    if err != nil {
        beego.Warning("Error in calculation:", err.Error())
    }

    beego.Informational("Returning result: ", res, " error: ", err)
    outputs <- outputData{result: res, error: err != nil}
}

func main() {
    inputs = make(chan inputData)
    outputs = make(chan outputData)
    criticalChan = make(chan int)
    beego.Informational("App started.")

    go consumeResults(outputs)
    beego.Informational("Started receiving results.")

    go generateInputs(inputs)
    beego.Informational("Started sending signals.")

    for {
        select {
        case input := <-inputs:
            processInput(input)
        case <-criticalChan:
            beego.Critical("Caught value from criticalChan: Go shut down.")
            panic("Shut down due to critical fault.")
        }
    }
}

99. 安全

1. XSRF 攻击

1.原理

跨站请求伪造(Cross-site request forgery), 简称为 XSRF,是 Web 应用中常见的一个安全问题。前面的链接也详细讲述了 XSRF 攻击的实现方式。

当前防范 XSRF 的一种通用的方法,是对每一个用户都记录一个无法预知的 cookie 数据,然后要求所有提交的请求(POST/PUT/DELETE)中都必须带有这个 cookie 数据。
如果此数据不匹配 ,那么这个请求就可能是被伪造的。
beego 有内建的 XSRF 的防范机制,要使用此机制,你需要在应用配置文件中加上 enablexsrf 设定:

enablexsrf = true
xsrfkey = 61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o
xsrfexpire = 3600

或者直接在 main 入口处这样设置:

beego.EnableXSRF = true
beego.XSRFKEY = "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o"
beego.XSRFExpire = 3600  //过期时间,默认1小时

如果开启了 XSRF,那么 beego 的 Web 应用将对所有用户设置一个 _xsrf 的 cookie 值(默认过期 1 小时),
如果 POST PUT DELET 请求中没有这个 cookie 值,那么这个请求会被直接拒绝。如果你开启了这个机制,
那么在所有被提交的表单中,你都需要加上一个域来提供这个值。你可以通过在模板中使用 专门的函数 XSRFFormHTML() 来做到这一点:

过期时间上面我们设置了全局的过期时间 beego.XSRFExpire,但是有些时候我们也可以在控制器中修改这个过期时间,专门针对某一类处理逻辑:

func (this *HomeController) Get(){
    this.XSRFExpire = 7200
    this.Data["xsrfdata"]=template.HTML(this.XSRFFormHTML())
}

2. 在表单中使用

在 Controller 中这样设置数据:

func (this *HomeController) Get(){
    this.Data["xsrfdata"]=template.HTML(this.XSRFFormHTML())
}

然后在模板中这样设置:

<form action="/new_message" method="post">
  {{ .xsrfdata }}
  <input type="text" name="message"/>
  <input type="submit" value="Post"/>
</form>

3. 在JavaScript 中使用

如果你提交的是 AJAX 的 POST 请求,你还是需要在每一个请求中通过脚本添加上 _xsrf 这个值。
下面是在 AJAX 的 POST 请求,使用了 jQuery 函数来为所有请求都添加 _xsrf 值:
jQuery cookie插件:https://github.com/carhartl/jquery-cookie base64
插件:http://phpjs.org/functions/base64_decode/

jQuery.postJSON = function(url, args, callback) {
   var xsrf, xsrflist;
   xsrf = $.cookie("_xsrf");
   xsrflist = xsrf.split("|");
   args._xsrf = base64_decode(xsrflist[0]);
    $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
        success: function(response) {
        callback(eval("(" + response + ")"));
    }});
};

1. 扩展jQuery

通过扩展 ajax 给每个请求加入 xsrf 的 header

需要你在 html 里保存一个 _xsrf 值

func (this *HomeController) Get(){
    this.Data["xsrf_token"] = this.XSRFToken()
}

放在你的 head 中

<head>
    <meta name="_xsrf" content="{{.xsrf_token}}" />
</head>

扩展 ajax 方法,将 _xsrf 值加入 header,扩展后支持 jquery post/get 等内部使用了 ajax 的方法

var ajax = $.ajax;
$.extend({
    ajax: function(url, options) {
        if (typeof url === 'object') {
            options = url;
            url = undefined;
        }
        options = options || {};
        url = options.url;
        var xsrftoken = $('meta[name=_xsrf]').attr('content');
        var headers = options.headers || {};
        var domain = document.domain.replace(/\./ig, '\\.');
        if (!/^(http:|https:).*/.test(url) || eval('/^(http:|https:)\\/\\/(.+\\.)*' + domain + '.*/').test(url)) {
            headers = $.extend(headers, {'X-Xsrftoken':xsrftoken});
        }
        options.headers = headers;
        return ajax(url, options);
    }
});

对于 PUT 和 DELETE 请求(以及不使用将 form 内容作为参数的 POST 请求)来说,你也可以在 HTTP 头中以 X-XSRFToken 这个参数传递 XSRF token。

如果你需要针对每一个请求处理器定制 XSRF 行为,你可以重写 Controller 的 CheckXSRFCookie 方法。例如你需要使用一个不支持 cookie 的 API, 你可以通过将 CheckXSRFCookie() 函数设空来禁用 XSRF 保护机制。然而如果 你需要同时支持 cookie 和非 cookie 认证方式,那么只要当前请求是通过 cookie 进行认证的,
你就应该对其使用 XSRF 保护机制,这一点至关重要。

4. 持controller 级别的屏蔽

XSRF 之前是全局设置的一个参数,如果设置了那么所有的 API 请求都会进行验证,但是有些时候API 逻辑是不需要进行验证的,因此现在支持在controller 级别设置屏蔽:

type AdminController struct{
    beego.Controller
}

func (a *AdminController) Prepare() {
    a.EnableXSRF = false
}
posted @ 2024-09-19 17:30  河图s  阅读(93)  评论(0)    收藏  举报