中间件的意思是,我们运行博客后,用户访问一个链接,程序会最终执行这个链接对应的控制器。在执行控制器里面的代码之前运行的其他代码,我们就叫做中间件。比如常用到的中间件有:authorization权限判断,logger访问日志,cors跨域处理options请求等。这些中间件有做拦截的,有做旁路的等等功能。

我们要制作的博客,同样需要使用到中间件功能,我们的博客设计中用到的功能有管理员权限判断、cors判断等。

cors跨域处理options请求

options叫做预检请求,就是查看是否可以或者允许跨域请求。

出于安全原因,浏览器限制从脚本内发起的跨源HTTP请求。当一个资源从与该资源本身所在的服务器不同的域或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

我们在前面已经创建的middleware文件夹,创建一个options.go文件,并写上下面的代码:

package middleware

import "github.com/kataras/iris/v12"

func Cors(ctx iris.Context) {
	ctx.Header("Access-Control-Allow-Origin", "*")
	if ctx.Request().Method == "OPTIONS" {
		ctx.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH,OPTIONS")
		ctx.Header("Access-Control-Allow-Headers", "Content-Type, Api, Accept, Authorization, Version, Token")
		ctx.StatusCode(204)
		return
	}
	ctx.Next()
}

这段代码的意思,其中ctx.Header("Access-Control-Allow-Origin", "*")表示允许所有的域访问。

再判断请求方法是否是OPTIONS,如果是,则发送头部声明支持GET,POST,PUT,DELETE,PATCH,OPTIONS这几种请求方法。并且支持Content-Type, Api, Accept, Authorization, Version, Token这些字段。

最后返回204状态,表示这个请求已经执行成功,但是没有任何数据,浏览器不用刷新页面.也不用导向新的页面。如果是其他的请求,则执行ctx.Next()继续执行下一个方法。这样这个中间件就算写完了。这个是一个拦截中间件,拦截了OPTIONS请求。

管理员权限验证中间件

博客中,有一些页面是只有管理员才能访问的,普通用户访问就会返回无权限等,或者导航到登录页面。比如发布文章(publish)页面,我们就需要做权限判断。

我们在middleware文件夹,再创建一个auth.go文件,并写上下面的代码:

package middleware

import (
	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/sessions"
)

var Sess = sessions.New(sessions.Config{Cookie: "irisweb"})

func Auth(ctx iris.Context) {
	//检查登录状态
	session := Sess.Start(ctx)
	hasLogin := session.GetBooleanDefault("hasLogin", false)
	ctx.Values().Set("hasLogin", hasLogin)
	ctx.ViewData("hasLogin", hasLogin)

	ctx.Next()
}

这里,我们使用的是sessions,因为本身博客还是一个比较简单的系统,不用一下子就搞得太复杂。sessions也适合从php转过来的用户,能看懂它的逻辑。因为php我们经常会使用sessions存储用户登录状态信息。

当然,我们也可以使用更专业一点的登录权限判断中间件jwt。下面是使用jwt-go的方法:

package middleware

import (
	"github.com/dgrijalva/jwt-go"
	jwtmiddleware "github.com/iris-contrib/middleware/jwt"
	"github.com/kataras/iris/v12"
)

var TokenSecret = "PfkdKrDSXRAh2DD2uxqY7W0MXXMFzlO4"

func JWT() *jwtmiddleware.Middleware {
	jwtHandler := jwtmiddleware.New(jwtmiddleware.Config{
		ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
			return []byte(TokenSecret), nil
		},
		ContextKey:          "",
		ErrorHandler:        ServError,
		CredentialsOptional: false,
		Extractor:           nil,
		EnableAuthOnOptions: false,
		SigningMethod:       jwt.SigningMethodHS256,
		Expiration:          false,
	})

	return jwtHandler
}

func ServError(ctx iris.Context, err error) {
	ctx.JSON(iris.Map{
			"code": -1,
			"msg":  "您尚未登录,请登录后操作",
		})
		return
}

这里的意思是,默认从header获取token,如果验证通过,则会继续执行后面的控制器代码,如果错误,则抛出一个json。这里往往用在前后端分离的授权验证中,或者是api接口的授权验证。

我们的博客目前设计不是使用前后端分离,因此使用session更合适一一些。

中间件写好了,我们还需要将它注册到路由器中,它才能生效。

上面的Cors()函数是全局拦截函数,因此我们在bootstrap.go 中定义个 loadGlobalMiddleware() 函数:

func (bootstrap *Bootstrap) loadGlobalMiddleware() {
	bootstrap.Application.Use(recover.New())
	bootstrap.Application.Use(middleware.Cors)
	bootstrap.Application.Use(middleware.Auth)
}

并在 bootstrap.go 中的 Serve()函数中,添加上这个函数的调用:

func (bootstrap *Bootstrap) Serve() {
	bootstrap.Application.Logger().SetLevel(bootstrap.LoggerLevel)
	bootstrap.loadGlobalMiddleware()
	bootstrap.LoadRoutes()

	pugEngine := iris.Django("./template", ".html")
	if config.ServerConfig.Env == "development" {
		//测试环境下动态加载
		pugEngine.Reload(true)
	}
	pugEngine.AddFunc("stampToDate", TimestampToDate)
	bootstrap.Application.RegisterView(pugEngine)

	bootstrap.Application.Run(
		iris.Addr(fmt.Sprintf("127.0.0.1:%d", bootstrap.Port)),
		iris.WithoutServerError(iris.ErrServerClosed),
		iris.WithoutBodyConsumptionOnUnmarshal,
	)
}

这样,整个bootstrap.go的代码如下:

package irisweb

import (
	"context"
	"fmt"
	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/middleware/recover"
	"irisweb/config"
	"irisweb/middleware"
	"irisweb/route"
	"time"
)

type Bootstrap struct {
	Application *iris.Application
	Port        int
	LoggerLevel string
}

func New(port int, loggerLevel string) *Bootstrap {
	var bootstrap Bootstrap
	bootstrap.Application = iris.New()
	bootstrap.Port = port
	bootstrap.LoggerLevel = loggerLevel

	return &bootstrap
}

func (bootstrap *Bootstrap) loadGlobalMiddleware() {
	bootstrap.Application.Use(recover.New())
	bootstrap.Application.Use(middleware.Cors)
	bootstrap.Application.Use(middleware.Auth)
}

func (bootstrap *Bootstrap) LoadRoutes() {
	route.Register(bootstrap.Application)
}

func (bootstrap *Bootstrap) Serve() {
	bootstrap.Application.Logger().SetLevel(bootstrap.LoggerLevel)
	bootstrap.loadGlobalMiddleware()
	bootstrap.LoadRoutes()

	pugEngine := iris.Django("./template", ".html")
	if config.ServerConfig.Env == "development" {
		//测试环境下动态加载
		pugEngine.Reload(true)
	}
	pugEngine.AddFunc("stampToDate", TimestampToDate)
	bootstrap.Application.RegisterView(pugEngine)

	bootstrap.Application.Run(
		iris.Addr(fmt.Sprintf("127.0.0.1:%d", bootstrap.Port)),
		iris.WithoutServerError(iris.ErrServerClosed),
		iris.WithoutBodyConsumptionOnUnmarshal,
	)
}

func (bootstrap *Bootstrap) Shutdown() error {
	bootstrap.Application.Shutdown(context.Background())

	return nil
}

func TimestampToDate(in int64, layout string) string {
	t := time.Unix(in, 0)
	return t.Format(layout)
}

至此,中间件就可以正常运作了。

完整的项目示例代码托管在GitHub上,需要查看完整的项目代码可以到github.com/fesiong/goblog 上查看,也可以直接fork一份来在上面做修改。