允许跨域请求

第一个例子,也是比较常见的功能需求。

我们需要在所有API请求之前增加允许跨域请求的返回Header信息,该功能可以通过中间件实现:

package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()
	r.Middleware.Next()
}

func main() {
	s := g.Server()
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareCORS)
		group.ALL("/user/list", func(r *ghttp.Request) {
			r.Response.Writeln("list")
		})
	})
	s.SetPort(8199)
	s.Run()
}

执行后,终端打印出路由表信息如下:

SERVER  | DOMAIN  | ADDRESS | METHOD |       ROUTE       |      HANDLER      |     MIDDLEWARE
|---------|---------|---------|--------|-------------------|-------------------|---------------------|
  default | default | :8199   | ALL    | /api.v2/user/list | main.main.func1.1 | main.MiddlewareCORS
|---------|---------|---------|--------|-------------------|-------------------|---------------------|

这里我们使用group.Middleware(MiddlewareCORS)将跨域中间件通过分组路由的形式注册绑定到了/api.v2路由下所有的服务函数中。随后我们可以通过请求 http://127.0.0.1:8199/api.v2/user/list 来查看允许跨域请求的Header信息是否有返回。

请求鉴权处理

我们在跨域请求中间件的基础之上加上鉴权中间件。

为了简化示例,在该示例中,当请求带有token参数,并且参数值为123456时可以通过鉴权,并且允许跨域请求,执行请求方法;否则返回403 Forbidden状态码。

package main

import (
	"net/http"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareAuth(r *ghttp.Request) {
	token := r.Get("token")
	if token.String() == "123456" {
		r.Response.Writeln("auth")
		r.Middleware.Next()
	} else {
		r.Response.WriteStatus(http.StatusForbidden)
	}
}

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.Writeln("cors")
	r.Response.CORSDefault()
	r.Middleware.Next()
}

func main() {
	s := g.Server()
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareCORS, MiddlewareAuth)
		group.ALL("/user/list", func(r *ghttp.Request) {
			r.Response.Writeln("list")
		})
	})
	s.SetPort(8199)
	s.Run()
}

执行后,终端打印出路由表信息如下:

SERVER  | DOMAIN  | ADDRESS | METHOD |       ROUTE       |      HANDLER      |               MIDDLEWARE
|---------|---------|---------|--------|-------------------|-------------------|-----------------------------------------|
  default | default | :8199   | ALL    | /api.v2/user/list | main.main.func1.1 | main.MiddlewareCORS,main.MiddlewareAuth
|---------|---------|---------|--------|-------------------|-------------------|-----------------------------------------|

可以看到,我们的服务方法绑定了两个中间件,跨域中间件和而鉴权中间件。 请求时将会按照中间件注册的先后顺序,先执行MiddlewareCORS全局中间件,再执行MiddlewareAuth分组中间件。 随后我们可以通过请求 http://127.0.0.1:8199/api.v2/user/listhttp://127.0.0.1:8199/api.v2/user/list?token=123456 对比来查看效果。


鉴权例外处理

使用分组路由中间件可以很方便地添加鉴权例外,因为只有当前分组路由下注册的服务方法才会绑定并执行鉴权中间件,否则并不会执行到鉴权中间件。

package main

import (
	"net/http"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareAuth(r *ghttp.Request) {
	token := r.Get("token")
	if token.String() == "123456" {
		r.Middleware.Next()
	} else {
		r.Response.WriteStatus(http.StatusForbidden)
	}
}

func main() {
	s := g.Server()
	s.Group("/admin", func(group *ghttp.RouterGroup) {
		group.ALL("/login", func(r *ghttp.Request) {
			r.Response.Writeln("login")
		})
		group.Group("/", func(group *ghttp.RouterGroup) {
			group.Middleware(MiddlewareAuth)
			group.ALL("/dashboard", func(r *ghttp.Request) {
				r.Response.Writeln("dashboard")
			})
		})
	})
	s.SetPort(8199)
	s.Run()
}

执行后,终端打印出路由表信息如下:

SERVER  | ADDRESS | DOMAIN  | METHOD | P |      ROUTE       |       HANDLER       |     MIDDLEWARE
|---------|---------|---------|--------|---|------------------|---------------------|---------------------|
  default |  :8199  | default |  ALL   | 2 | /admin/dashboard | main.main.func1.2.1 | main.MiddlewareAuth
|---------|---------|---------|--------|---|------------------|---------------------|---------------------|
  default |  :8199  | default |  ALL   | 2 | /admin/login     | main.main.func1.1   |
|---------|---------|---------|--------|---|------------------|---------------------|---------------------|

可以看到,只有/admin/dashboard路由的服务方法绑定了鉴权中间件main.MiddlewareAuth,而/admin/login路由的服务方法并没有添加鉴权处理。 随后我们访问以下URL查看效果: 

  1. http://127.0.0.1:8199/admin/login
  2. http://127.0.0.1:8199/admin/dashboard
  3. http://127.0.0.1:8199/admin/dashboard?token=123456



统一的错误处理

基于中间件,我们可以在服务函数执行完成后做一些后置判断的工作,特别是统一数据格式返回、结果处理、错误判断等等。这种需求我们可以使用后置的中间件类型来实现。我们使用一个简单的例子,用来演示如何使用中间件对所有的接口请求做后置判断处理,作为一个抛砖引玉作用。

package main

import (
	"net/http"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareAuth(r *ghttp.Request) {
	token := r.Get("token")
	if token.String() == "123456" {
		r.Middleware.Next()
	} else {
		r.Response.WriteStatus(http.StatusForbidden)
	}
}

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()
	r.Middleware.Next()
}

func MiddlewareErrorHandler(r *ghttp.Request) {
	r.Middleware.Next()
	if r.Response.Status >= http.StatusInternalServerError {
		r.Response.ClearBuffer()
		r.Response.Writeln("哎哟我去,服务器居然开小差了,请稍后再试吧!")
	}
}

func main() {
	s := g.Server()
	s.Use(MiddlewareCORS)
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareAuth, MiddlewareErrorHandler)
		group.ALL("/user/list", func(r *ghttp.Request) {
			panic("db error: sql is xxxxxxx")
		})
	})
	s.SetPort(8199)
	s.Run()
}

执行后,终端打印出路由表信息如下:

SERVER  | DOMAIN  | ADDRESS | METHOD |       ROUTE       |       HANDLER       |                   MIDDLEWARE
|---------|---------|---------|--------|-------------------|---------------------|-------------------------------------------------|
  default | default | :8199   | ALL    | /*                | main.MiddlewareCORS | GLOBAL MIDDLEWARE
|---------|---------|---------|--------|-------------------|---------------------|-------------------------------------------------|
  default | default | :8199   | ALL    | /api.v2/user/list | main.main.func1.1   | main.MiddlewareAuth,main.MiddlewareErrorHandler
|---------|---------|---------|--------|-------------------|---------------------|-------------------------------------------------|

在该示例中,我们在后置中间件中判断有无系统错误,如果有则返回固定的提示信息,而不是把敏感的报错信息展示给用户。当然,在真实的项目场景中,往往还有是需要解析返回缓冲区的数据,例如JSON数据,根据当前的执行结果进行封装返回固定的数据格式等等。

执行该示例后,访问 http://127.0.0.1:8199/api.v2/user/list?token=123456 查看效果。

自定义日志处理

我们来更进一步完善一下以上示例,我们将请求日志包括状态码输出到终端。这里我们必须得使用”全局中间件”了,这样可以拦截处理到所有的服务请求,甚至404请求。

package main

import (
	"net/http"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareAuth(r *ghttp.Request) {
	token := r.Get("token")
	if token.String() == "123456" {
		r.Middleware.Next()
	} else {
		r.Response.WriteStatus(http.StatusForbidden)
	}
}

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()
	r.Middleware.Next()
}

func MiddlewareLog(r *ghttp.Request) {
	r.Middleware.Next()
	errStr := ""
	if err := r.GetError(); err != nil {
		errStr = err.Error()
	}
	g.Log().Println(r.Response.Status, r.URL.Path, errStr)
}

func main() {
	s := g.Server()
	s.SetConfigWithMap(g.Map{
		"AccessLogEnabled": false,
		"ErrorLogEnabled":  false,
	})
	s.Use(MiddlewareLog, MiddlewareCORS)
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareAuth)
		group.ALL("/user/list", func(r *ghttp.Request) {
			panic("啊!我出错了!")
		})
	})
	s.SetPort(8199)
	s.Run()
}


可以看到,我们注册了一个全局的日志处理中间件以及跨域中间件,而鉴权中间件是注册到/api.v2路由下。

执行后,我们可以通过请求 http://127.0.0.1:8199/api.v2/user/listhttp://127.0.0.1:8199/api.v2/user/list?token=123456 对比来查看效果,并查看终端的日志输出情况。

Content Menu

  • No labels

8 Comments

  1. 鉴权例外处理


    请问这种场景用规范路由怎么实现,group.bind,一个controller list不需要鉴权,add需要


    1. 同问,目前也遇到这样的场景了!

      1. 做两个分组,一个注册MiddlewareAuth一个不加.例如

        group.Group("/v1", func(group *ghttp.RouterGroup) {
        //no auth group
        group.Group("/user", func(group *ghttp.RouterGroup) {
        group.ALL("sign-in", controller.Users.SignIn)
        })

        //auth role group
        group.Group("/", func(group *ghttp.RouterGroup) {
        group.Middleware(service.Middleware().Auth)
        //group.Middleware(service.Middleware().BrandAuth)
        group.Group("/user", func(group *ghttp.RouterGroup) {
        group.GET("/", controller.Users.Info)
        })
        })
        })
        1. 这种不还是相当于把规范路由拆成按函数方法路由了么

          1. 规范路由实现不了.tag上没有middleware的设定(沟通作者不推荐加上这个标签说会增加心智负担).如果有这需求可以fork代码自行修改然后replace(我增加过并且比较容易实现).

    2.     server.SetRouteOverWrite(true) // 允许路由覆盖
      	server.Group("/", func(group *ghttp.RouterGroup) {
      		// 需要鉴权的路由
      		group.Group("/", func(group *ghttp.RouterGroup) {
      			group.Middleware(Auth)
      			group.Bind(
      				controller.User,
      			)
      		})
              // 例外路由,Map也支持规范路由注册
      		group.Map(g.Map{
      			"POST:/login": controller.User.Login,
      		})
      	})
  2. 规范路由的ACL可以这么做,上面那位老哥用`Map`会显得没那么”规范“

    s.SetRouteOverWrite(true) // 路由覆盖,用于无需登录的鉴权排除
    s.Group("/api", func(group *ghttp.RouterGroup) {
    	cUser := user.New()
    	group.Group("/user", func(group *ghttp.RouterGroup) {
    		group.Middleware(middleware.RequestAuthHandle) // 鉴权
    		group.Bind(cUser)
    	})
    	group.Group("/user", func(group *ghttp.RouterGroup) {
    		group.Bind(cUser.Signin)
    	})
    })
  3. 关于规范路由,中间鉴权,部分路由又不需要走鉴权,可以做一个路由白名单来玩,如下:

    // [1]router.go 或 g.Server().Bind() 逻辑
    s.Group("/", func(group *ghttp.RouterGroup) {
    		group.Middleware(
    			service.CorsMiddleware().CORS,         // 挂载CORS
    			service.ResponseMiddleware().Response, // 通用http response
    		)
    		group.Group("/v1", func(group *ghttp.RouterGroup) {
    			// 不走签名验证
    			group.Bind(
    				controller.Select, // select
    			)
    			// 签名验证
    			group.Middleware(service.TokenMiddleware().TokenCheck)
    			group.Bind(
    				controller.Order,    // 订单
    			)
    		})
    	})
    
    
    // [2]token_middleware.go
    type (
    	sTokenMiddleware struct{}
    )
    
    func init() {
    	service.RegisterTokenMiddleware(NewTokenMiddleware())
    }
    
    func NewTokenMiddleware() *sTokenMiddleware {
    	return &sTokenMiddleware{}
    }
    func (s *sTokenMiddleware) TokenCheck(r *ghttp.Request) {
    	if !s.validateWhiteRoute(r) {
    		// 不是白名单需要做鉴权
    	}
    	r.Middleware.Next()
    }
    // validateWhiteRoute 路由白名单,用于验证路由是否强制验证Token
    func (s *sTokenMiddleware) validateWhiteRoute(r *ghttp.Request) bool {
    	// 获得配置文件中的白名单路由切片
    	// 大概效果类似[]string{"/v1/order/list","/v2/order/list"}
    	isPass := garray.NewStrArrayFrom(utility.GetConfigWhiteRouteList(r.GetCtx())).Contains(r.Request.URL.Path)
    	if utility.IsDev() {
    		isPass = !utility.GetConfigWhiteRouteOpen(r.GetCtx()) || isPass
    	}
    	return isPass
    }
    
    
    // [3]----------------------  config.yaml文件关于路由白名单配置
    # 白名单
    white:
      # 接口路由白名单,注意路由大小写,所有路由都是以/开始
      route:
        # 开启除白名单外的所有接口都走token验证 true 开启 false 关闭 关闭表示所有的接口都不验证Token,该配置项只在开发环境生效
        open: false
        list:
          - /v1/order/list
          - /v2/order/list