API authentication flow

Specific project examples https://github.com/snowlyg/IrisAdminApi Related articles: iris + casbin from unfamiliarity to the process of learning to use it

When many novices learn iris-go, they find the documentation a bit fragmented. Moreover, English documents are still a bit difficult for some friends. Even if they use translation software, some places will be translated in a jerky and difficult-to-understand manner. This article will mainly explain in detail the implementation process of a small project I am writing. My skills are limited, if there are any mistakes, I hope everyone can kindly point them out.

Preface
  • First you need to install the golang environment. For specific installation tutorials, please view Go Getting Started Guide
  • Install mysql or gcc (sqlite3) environment locally. gcc download address It is recommended to download the decompressed version. The installation version will be slower to download.
  • Turn on GO111MODULE mode and set up mirroring
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
  • Initialize go.mod file
go mod init
  • Install gowatch, a tool similar to bee run.
go get github.com/silenceper/gowatch

Note: If you are using goland Ide, you need to enable go moudels for each project separately in goland settings, and set the mirror address, as shown below.

image.png

New project
package main
import (
	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/middleware/logger"
	"github.com/kataras/iris/v12/middleware/recover"
)
func main(){
	app := iris.New()
	app.Logger().SetLevel("debug")
	//可选的, 增加两个内置的处理程序
	// 一个可以让程序从任意的 http-relative panics 中恢复过来,
	// 一个可以记录日志到终端。
	app.Use(recover.New())
	app.Use(logger.New())
	// Method:   GET
	// Resource: http://localhost:8080
	app.Handle("GET", "/", func(ctx iris.Context) {
		ctx.HTML("<h1>Welcome</h1>")
	})
	// same as app.Handle("GET", "/ping", [...])
	// Method:   GET
	// Resource: http://localhost:8080/ping
	app.Get("/ping", func(ctx iris.Context) {
		ctx.WriteString("pong")
	})
	// Method:   GET
	// Resource: http://localhost:8080/hello
	app.Get("/hello", func(ctx iris.Context) {
		ctx.JSON(iris.Map{"message": "Hello Iris!"})
	})
	// http://localhost:8080
	// http://localhost:8080/ping
	// http://localhost:8080/hello
	app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
}
  • Load the related package go mod tidy, at this time go.mod and go.sum will load the relevant information of the package.
// go.mod
module IrisAdmin
go 1.13
require (
	github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
	github.com/ajg/form v1.5.1 // indirect
	github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 // indirect
	github.com/google/go-querystring v1.0.0 // indirect
	github.com/imkira/go-interpol v1.1.0 // indirect
	github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
	github.com/kataras/iris/v12 v12.1.4
	github.com/mattn/go-colorable v0.1.4 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.1 // indirect
	github.com/moul/http2curl v1.0.0 // indirect
	github.com/onsi/ginkgo v1.11.0 // indirect
	github.com/onsi/gomega v1.8.1 // indirect
	github.com/sergi/go-diff v1.1.0 // indirect
	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
	github.com/smartystreets/goconvey v1.6.4 // indirect
	github.com/valyala/fasthttp v1.8.0 // indirect
	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
	github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect
	github.com/yudai/gojsondiff v1.0.0 // indirect
	github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
	github.com/yudai/pp v2.0.1+incompatible // indirect
)
  • Start the project and run gowatch or go run main.go in the project directory.
  • Enter http://localhost:8080. If you get the following display, it means the startup is successful.

image.png

Implement login and logout functions
  • So far we have only implemented a simple web server and a few simple interfaces. Next we have to implement the basic functions of logging in and out of the website.
  • Here we use unit testing to drive the development of relevant interfaces. This will make the later maintenance of our project relatively easier.
  • First, we need to modify our main.go file and create a NewApp method. This method returns an *iris.Application, which is a pointer to iris.Application. (The specific reason why it is a pointer will not be explained in detail here. You can read [Go Getting Started Guide]https://learnku.com/docs/the-way-to-go), this method will be used in unit testing.
package main
import (
	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/middleware/logger"
	"github.com/kataras/iris/v12/middleware/recover"
)
func main(){
	app := NewApp()
	// http://localhost:8080
	// http://localhost:8080/ping
	// http://localhost:8080/hello
	app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
}
func NewApp() *iris.Application {
	app := iris.New()
	app.Logger().SetLevel("debug")
	// Optionally, add two built'n handlers
	// that can recover from any http-relative panics
	// and log the requests to the terminal.
	app.Use(recover.New())
	app.Use(logger.New())
	// Method:   GET
	// Resource: http://localhost:8080
	app.Handle("GET", "/", func(ctx iris.Context) {
		ctx.HTML("<h1>Welcome</h1>")
	})
	// same as app.Handle("GET", "/ping", [...])
	// Method:   GET
	// Resource: http://localhost:8080/ping
	app.Get("/ping", func(ctx iris.Context) {
		ctx.WriteString("pong")
	})
	// Method:   GET
	// Resource: http://localhost:8080/hello
	app.Get("/hello", func(ctx iris.Context) {
		ctx.JSON(iris.Map{"message": "Hello Iris!"})
	})
	return app
}
  • Create a new test file base_test.go` in the project directory. The test files all end with _test.go.
package main
import (
	"flag"
	"os"
	"testing"
	"github.com/gavv/httpexpect"
	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/httptest"
)
const baseUrl = "/v1/admin/"
const loginUrl = baseUrl + "login" // 登陆接口地址
var (
	app   *iris.Application 
)
//单元测试基境
func TestMain(m *testing.M) {
	// 初始化app
	app = NewApp()
	flag.Parse()
	exitCode := m.Run()
	os.Exit(exitCode)
}
// 单元测试 login 方法
func login(t *testing.T, Object interface{}, StatusCode int, Status bool, Msg string) (e *httpexpect.Expect) {
	e = httptest.New(t, app, httptest.Configuration{Debug: true})
	e.POST(loginUrl).WithJSON(Object).Expect().Status(StatusCode).JSON().Object().Values().Contains(Status, Msg)
	return
}
  • Create a new test file auth_test.go in the project directory.
package main
import (
   "testing"
 "github.com/kataras/iris/v12")
//登陆成功
func TestUserLoginSuccess(t *testing.T) {
   oj := map[string]string{
      "username": "username",
  "password": "password",
  }
   login(t, oj, iris.StatusOK, true, "登陆成功")
}
  • Run the unit test go test -run TestUserLoginSuccess to execute the login success test separately. Got the following error:

image.png

  • Why an error is reported? From the error message, we know that we were supposed to get a status code of 200, but 404 was returned. Because we have not defined the login route. Here we use the gorm package to manage data and use jwt as the interface authentication method.
  • Create a new user model, token model and implement related methods.
  • Create new databases iris and tiris. It should be noted here that the character set of the database must be changed to utf-8, otherwise Chinese garbled characters will appear. -Introduced three new dependency packages
	"github.com/iris-contrib/middleware/jwt"
	"github.com/jameskeane/bcrypt"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
  • Added cross-domain middleware and jwt authentication middleware -The complete code is as follows
package main

import (
   "errors"
 "fmt" "net/http" "os" "strconv" "strings" "time"
 "github.com/fatih/color" "github.com/kataras/iris/v12/context"
 "github.com/kataras/iris/v12"
 "github.com/iris-contrib/middleware/cors" "github.com/iris-contrib/middleware/jwt" "github.com/jameskeane/bcrypt" "github.com/jinzhu/gorm"  _ "github.com/jinzhu/gorm/dialects/mysql"
 "github.com/kataras/iris/v12/middleware/logger" "github.com/kataras/iris/v12/middleware/recover"  _ "github.com/mattn/go-sqlite3"
)

var Db *gorm.DB
var err error
var dirverName string
var conn string

// 用户数据模型
type User struct {
   gorm.Model

  Name     string `gorm:"not null VARCHAR(191)"`
  Username string `gorm:"unique;VARCHAR(191)"`
  Password string `gorm:"not null VARCHAR(191)"`
}

// 接口返回数据对想
type Response struct {
   Status bool `json:"status"` //接口状态 true ,false  Msg    interface{} `json:"msg"` // 接口信息
  Data   interface{} `json:"data"` //接口数据
}

// token 数据模型
type OauthToken struct {
   gorm.Model

  Token     string `gorm:"not null default '' comment('Token') VARCHAR(191)"`
  UserId    uint `gorm:"not null default '' comment('UserId') VARCHAR(191)"`
  Secret    string `gorm:"not null default '' comment('Secret') VARCHAR(191)"`
  ExpressIn int64 `gorm:"not null default 0 comment('是否是标准库') BIGINT(20)"`
  Revoked   bool
}

// 创建 token
func (ot *OauthToken) OauthTokenCreate() (response Token) {
   Db.Create(ot)
   response = Token{ot.Token}

   return
}

type Token struct {
   Token string `json:"access_token"`
}

// 判断数据库是否返回 ErrRecordNotFound ,如果是说明数据库没有相关记录。
func IsNotFound(err error) {
   if ok := errors.Is(err, gorm.ErrRecordNotFound); !ok && err != nil {
      color.Red(fmt.Sprintf("error :%v \n ", err))
   }
}

// 根据用户名查询用户
func UserAdminCheckLogin(username string) *User {
   user := new(User)
   IsNotFound(Db.Where("username = ?", username).First(user).Error)

   return user
}

// 登陆处理程序
func UserLogin(ctx iris.Context) {
   aul := new(User)
   if err := ctx.ReadJSON(&aul); err != nil {
      ctx.StatusCode(iris.StatusOK)
      _, _ = ctx.JSON(Response{Status: false, Msg: nil, Data: "请求参数错误"})
      return
  }

   ctx.StatusCode(iris.StatusOK)
   response, status, msg := CheckLogin(aul.Username, aul.Password)
   _, _ = ctx.JSON(Response{Status: status, Msg: response, Data: msg})
   return

}

// 检查登陆用户,并生成登陆凭证 token
func CheckLogin(username, password string) (response Token, status bool, msg string) {
   user := UserAdminCheckLogin(username)
   if user.ID == 0 {
      msg = "用户不存在"
  return
  } else {
      if ok := bcrypt.Match(password, user.Password); ok {

         token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
            "exp": time.Now().Add(time.Hour * time.Duration(1)).Unix(),
  "iat": time.Now().Unix(),
  })
         tokenString, _ := token.SignedString([]byte("replace-with-env-jwt-secret"))

         oauthToken := new(OauthToken)
         oauthToken.Token = tokenString
         oauthToken.UserId = user.ID
         oauthToken.Secret = "replace-with-env-oauth-secret"
  oauthToken.Revoked = false
  oauthToken.ExpressIn = time.Now().Add(time.Hour * time.Duration(1)).Unix()
         oauthToken.CreatedAt = time.Now()

         response = oauthToken.OauthTokenCreate()
         status = true
  msg = "登陆成功"

  return

  } else {
         msg = "用户名或密码错误"
  return
  }
   }
}

// 作废token
func UpdateOauthTokenByUserId(userId uint) (ot *OauthToken) {
   Db.Model(ot).Where("revoked = ?", false).
      Where("user_id = ?", userId).
      Updates(map[string]interface{}{"revoked": true})
   return
}

// 登出用户
func UserAdminLogout(userId uint) bool {
   ot := UpdateOauthTokenByUserId(userId)
   return ot.Revoked
}

// 登出
func UserLogout(ctx iris.Context) {
   aui := ctx.Values().GetString("auth_user_id")
   id, _ := strconv.Atoi(aui)
   UserAdminLogout(uint(id))

   ctx.StatusCode(http.StatusOK)
   _, _ = ctx.JSON(Response{true, nil, "退出"})
}

//获取程序运行环境
// 根据程序运行路径后缀判断
//如果是 test 就是测试环境
func isTestEnv() bool {
   files := os.Args
   for _, v := range files {
      if strings.Contains(v, "test") {
         return true
  }
   }
   return false
}

// 接口跨域处理
func CrsAuth() context.Handler {
   return cors.New(cors.Options{
      AllowedOrigins:   []string{"*"}, // allows everything, use that to change the hosts.
  AllowedMethods:   []string{"PUT", "PATCH", "GET", "POST", "OPTIONS", "DELETE"},
  AllowedHeaders:   []string{"*"},
  ExposedHeaders:   []string{"Accept", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization"},
  AllowCredentials: true,
  })

}

// 获取 access_token 信息
func GetOauthTokenByToken(token string) (ot *OauthToken) {
   ot = new(OauthToken)
   Db.Where("token =  ?", token).First(&ot)
   return
}

/**
 * 验证 jwt * @method JwtHandler */
 func JwtHandler() *jwt.Middleware {
   var mySecret = []byte("replace-with-env-jwt-secret")
   return jwt.New(jwt.Config{
      ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
         return mySecret, nil
  },
  SigningMethod: jwt.SigningMethodHS256,
  })
}

func AuthToken(ctx context.Context) {
   value := ctx.Values().Get("jwt").(*jwt.Token)
   token := GetOauthTokenByToken(value.Raw) //获取 access_token 信息
  if token.Revoked || token.ExpressIn < time.Now().Unix() {
      //_, _ = ctx.Writef("token 失效,请重新登录") // 输出到前端
  ctx.StatusCode(http.StatusUnauthorized)
      ctx.StopExecution()
      return
  } else {
      ctx.Values().Set("auth_user_id", token.UserId)
   }

   ctx.Next()
}

func NewApp() *iris.Application {

   dirverName = "mysql"
  if isTestEnv() { //如果是测试使用测试数据库
  conn = "mysql_user:mysql_password@(127.0.0.1:3306)/tiris?charset=utf8&parseTime=True&loc=Local"
  } else {
      conn = "mysql_user:mysql_password@(127.0.0.1:3306)/iris?charset=utf8&parseTime=True&loc=Local"
  }

   //初始化数据库
  Db, err = gorm.Open(dirverName, conn)
   if err != nil {
      color.Red(fmt.Sprintf("gorm open 错误: %v", err))
   }

   app := iris.New()
   app.Logger().SetLevel("debug")

   app.Use(recover.New())
   app.Use(logger.New())

   // 路由集使用跨域中间件 CrsAuth() 
   // 允许 Options 方法 AllowMethods(iris.MethodOptions)
main := app.Party("/", CrsAuth()).AllowMethods(iris.MethodOptions)
   {
      v1 := main.Party("/v1")
      {
         v1.Post("/admin/login", UserLogin)
         v1.PartyFunc("/admin", func(admin iris.Party) {
            admin.Use(JwtHandler().Serve, AuthToken) //登录验证
  admin.Get("/logout", UserLogout).Name = "退出"
  })
      }
   }

   return app
}

func main() {
   app := NewApp()
   app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
}
  • At this time, if we execute the test command go test -run TestUserLoginSuccess we will get the following error:

image.png

  • Now it is no longer a 404 error, but returns a message that the user does not exist. The reason is that although we have added routing, there is already a series of related codes
  • But the database does not have user data, so we naturally cannot query any user information.
  • So how to solve this problem?
  • At the same time, you will find that the current main.go file is very large and bloated, and it is very troublesome to modify the code or track errors. How should it be adjusted?
  • As mentioned earlier, how should permission control be implemented?
Restructure the project
  • Now we adjust the file code and divide the project structure as follows
  • controllers // controllers
    • access.go
	package controllers
import (
	"net/http"
	"strconv"
	"IrisAdmin/models"
	"github.com/kataras/iris/v12"
)
// 登陆处理程序
func UserLogin(ctx iris.Context) {
	aul := new(models.User)
	if err := ctx.ReadJSON(&aul); err != nil {
		ctx.StatusCode(iris.StatusOK)
		_, _ = ctx.JSON(models.Response{Status: false, Msg: nil, Data: "请求参数错误"})
		return
	}
	ctx.StatusCode(iris.StatusOK)
	response, status, msg := models.CheckLogin(aul.Username, aul.Password)
	_, _ = ctx.JSON(models.Response{Status: status, Msg: response, Data: msg})
	return
}
// 登出
func UserLogout(ctx iris.Context) {
	aui := ctx.Values().GetString("auth_user_id")
	id, _ := strconv.Atoi(aui)
	models.UserAdminLogout(uint(id))
	ctx.StatusCode(http.StatusOK)
	_, _ = ctx.JSON(models.Response{true, nil, "退出"})
}
  • middleware // middleware
    • auth.go // Verify login
package middleware
import (
	"net/http"
	"time"
	"IrisAdmin/models"
	"github.com/iris-contrib/middleware/jwt"
	"github.com/kataras/iris/v12/context"
)
func AuthToken(ctx context.Context) {
	value := ctx.Values().Get("jwt").(*jwt.Token)
	token := models.GetOauthTokenByToken(value.Raw) //获取 access_token 信息
	if token.Revoked || token.ExpressIn < time.Now().Unix() {
		//_, _ = ctx.Writef("token 失效,请重新登录") // 输出到前端
		ctx.StatusCode(http.StatusUnauthorized)
		ctx.StopExecution()
		return
	} else {
		ctx.Values().Set("auth_user_id", token.UserId)
	}
	ctx.Next()
}
  • crs.go // Cross-domain authentication
package middleware
import (
	"github.com/iris-contrib/middleware/cors"
	"github.com/kataras/iris/v12/context"
)
func CrsAuth() context.Handler {
	return cors.New(cors.Options{
		AllowedOrigins:   []string{"*"}, // allows everything, use that to change the hosts.
		AllowedMethods:   []string{"PUT", "PATCH", "GET", "POST", "OPTIONS", "DELETE"},
		AllowedHeaders:   []string{"*"},
		ExposedHeaders:   []string{"Accept", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization"},
		AllowCredentials: true,
	})
}
  • jwt.go // token authentication
package middleware
import (
	"github.com/iris-contrib/middleware/jwt"
)
/**
 * 验证 jwt
 * @method JwtHandler
 */
func JwtHandler() *jwt.Middleware {
	var mySecret = []byte("replace-with-env-jwt-secret")
	return jwt.New(jwt.Config{
		ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
			return mySecret, nil
		},
		SigningMethod: jwt.SigningMethodHS256,
	})
}
  • models // models
    • base.go // Initialize database, public method
package models
import (
	"errors"
	"fmt"
	"os"
	"strings"
	"github.com/fatih/color"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	_ "github.com/mattn/go-sqlite3"
)
var Db *gorm.DB
var err error
var dirverName string
var conn string
/**
*设置数据库连接
*@param diver string
 */
func Register() {
	dirverName = "mysql"
	if isTestEnv() { //如果是测试使用测试数据库
		conn = "mysql_user:mysql_password@(127.0.0.1:3306)/tiris?charset=utf8&parseTime=True&loc=Local"
	} else {
		conn = "mysql_user:mysql_password@(127.0.0.1:3306)/iris?charset=utf8&parseTime=True&loc=Local"
	}
	//初始化数据库
	Db, err = gorm.Open(dirverName, conn)
	if err != nil {
		color.Red(fmt.Sprintf("gorm open 错误: %v", err))
	}
}
func IsNotFound(err error) {
	if ok := errors.Is(err, gorm.ErrRecordNotFound); !ok && err != nil {
		color.Red(fmt.Sprintf("error :%v \n ", err))
	}
}
//获取程序运行环境
// 根据程序运行路径后缀判断
//如果是 test 就是测试环境
func isTestEnv() bool {
	files := os.Args
	for _, v := range files {
		if strings.Contains(v, "test") {
			return true
		}
	}
	return false
}
// 接口返回数据对想
type Response struct {
	Status bool        `json:"status"` //接口状态 true ,false
	Msg    interface{} `json:"msg"`    // 接口信息
	Data   interface{} `json:"data"`   //接口数据
}
  • token.go // token model
package models
import "github.com/jinzhu/gorm"
// token 数据模型
type OauthToken struct {
	gorm.Model
	Token     string `gorm:"not null default '' comment('Token') VARCHAR(191)"`
	UserId    uint   `gorm:"not null default '' comment('UserId') VARCHAR(191)"`
	Secret    string `gorm:"not null default '' comment('Secret') VARCHAR(191)"`
	ExpressIn int64  `gorm:"not null default 0 comment('是否是标准库') BIGINT(20)"`
	Revoked   bool
}
// 创建 token
func (ot *OauthToken) OauthTokenCreate() (response Token) {
	Db.Create(ot)
	response = Token{ot.Token}
	return
}
type Token struct {
	Token string `json:"access_token"`
}
// 获取 access_token 信息
func GetOauthTokenByToken(token string) (ot *OauthToken) {
	ot = new(OauthToken)
	Db.Where("token =  ?", token).First(&ot)
	return
}
  • user.go // User model
package models
import (
	"time"
	"github.com/iris-contrib/middleware/jwt"
	"github.com/jameskeane/bcrypt"
	"github.com/jinzhu/gorm"
)
// 用户数据模型
type User struct {
	gorm.Model
	Name     string `gorm:"not null VARCHAR(191)"`
	Username string `gorm:"unique;VARCHAR(191)"`
	Password string `gorm:"not null VARCHAR(191)"`
}
// 根据用户名查询用户
func UserAdminCheckLogin(username string) *User {
	user := new(User)
	IsNotFound(Db.Where("username = ?", username).First(user).Error)
	return user
}
// 检查登陆用户,并生成登陆凭证 token
func CheckLogin(username, password string) (response Token, status bool, msg string) {
	user := UserAdminCheckLogin(username)
	if user.ID == 0 {
		msg = "用户不存在"
		return
	} else {
		if ok := bcrypt.Match(password, user.Password); ok {
			token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
				"exp": time.Now().Add(time.Hour * time.Duration(1)).Unix(),
				"iat": time.Now().Unix(),
			})
			tokenString, _ := token.SignedString([]byte("replace-with-env-jwt-secret"))
			oauthToken := new(OauthToken)
			oauthToken.Token = tokenString
			oauthToken.UserId = user.ID
			oauthToken.Secret = "replace-with-env-oauth-secret"
			oauthToken.Revoked = false
			oauthToken.ExpressIn = time.Now().Add(time.Hour * time.Duration(1)).Unix()
			oauthToken.CreatedAt = time.Now()
			response = oauthToken.OauthTokenCreate()
			status = true
			msg = "登陆成功"
			return
		} else {
			msg = "用户名或密码错误"
			return
		}
	}
}
// 作废token
func UpdateOauthTokenByUserId(userId uint) (ot *OauthToken) {
	Db.Model(ot).Where("revoked = ?", false).
		Where("user_id = ?", userId).
		Updates(map[string]interface{}{"revoked": true})
	return
}
// 登出用户
func UserAdminLogout(userId uint) bool {
	ot := UpdateOauthTokenByUserId(userId)
	return ot.Revoked
}
  • routers // routing
    • router.go
package routers
import (
	"IrisAdmin/controllers"
	"IrisAdmin/middleware"
	"github.com/kataras/iris/v12"
)
func Register(app *iris.Application) {
	// 路由集使用跨域中间件 CrsAuth()
	// 允许 Options 方法 AllowMethods(iris.MethodOptions)
	main := app.Party("/", middleware.CrsAuth()).AllowMethods(iris.MethodOptions)
	{
		v1 := main.Party("/v1")
		{
			v1.Post("/admin/login", controllers.UserLogin)
			v1.PartyFunc("/admin", func(admin iris.Party) {
				admin.Use(middleware.JwtHandler().Serve, middleware.AuthToken) //登录验证
				admin.Get("/logout",  controllers.UserLogout).Name = "退出"
			})
		}
	}
}
  • auth_test.go // unit test
package main
import (
	"testing"
	"github.com/kataras/iris/v12"
)
//登陆成功
func TestUserLoginSuccess(t *testing.T) {
	oj := map[string]string{
		"username": "username",
		"password": "password",
	}
	login(t, oj, iris.StatusOK, true, "登陆成功")
}
// 输入不存在的用户名登陆
func TestUserLoginWithErrorName(t *testing.T) {
	oj := map[string]string{
		"username": "err_user",
		"password": "password",
	}
	login(t, oj, iris.StatusOK, false, "用户不存在")
}
// 输入错误的登陆密码
func TestUserLoginWithErrorPwd(t *testing.T) {
	oj := map[string]string{
		"username": "username",
		"password": "admin",
	}
	login(t, oj, iris.StatusOK, false, "用户名或密码错误")
}
// 输入登陆密码格式错误
func TestUserLoginWithErrorFormtPwd(t *testing.T) {
	oj := map[string]string{
		"username": "username",
		"password": "123",
	}
	login(t, oj, iris.StatusOK, false, "密码格式错误")
}
// 输入登陆密码格式错误
func TestUserLoginWithErrorFormtUserName(t *testing.T) {
	oj := map[string]string{
		"username": "df",
		"password": "123",
	}
	login(t, oj, iris.StatusOK, false, "用户名格式错误")
}
  • base_test.go
package main
import (
	"flag"
	"os"
	"testing"
	"github.com/gavv/httpexpect"
	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/httptest"
)
const baseUrl = "/v1/admin/" // 接口地址
const loginUrl = baseUrl + "login" // 登陆接口地址
var (
	app   *iris.Application 
)
//单元测试基境
func TestMain(m *testing.M) {
	// 初始化app
	app = NewApp()
	flag.Parse()
	exitCode := m.Run()
	os.Exit(exitCode)
}
// 单元测试 login 方法
func login(t *testing.T, Object interface{}, StatusCode int, Status bool, Msg string) (e *httpexpect.Expect) {
	e = httptest.New(t, app, httptest.Configuration{Debug: true})
	e.POST(loginUrl).WithJSON(Object).Expect().Status(StatusCode).JSON().Object().Values().Contains(Status, Msg)
	return
}
  • main.go
package main
import (
	"IrisAdmin/models"
	"IrisAdmin/routers"
	"github.com/kataras/iris/v12"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"github.com/kataras/iris/v12/middleware/logger"
	"github.com/kataras/iris/v12/middleware/recover"
	_ "github.com/mattn/go-sqlite3"
)
func NewApp() *iris.Application {
	models.Register() // 数据库初始化
	models.Db.AutoMigrate(
		&models.User{},
		&models.OauthToken{},
	)
	iris.RegisterOnInterrupt(func() {
		_ = models.Db.Close()
	})
	app := iris.New()
	app.Logger().SetLevel("debug") //设置日志级别
	app.Use(recover.New())
	app.Use(logger.New())
	routers.Register(app) // 注册路由
	return app
}
func main() {
	app := NewApp()
	app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
}

image.png

  • At this point, the login interface is basically completed.
  • However, there are still many problems that have not been solved. For example, if you repeatedly start the project or execute the test, you will get the following prompt. Although the test still passes, an error will be reported at the same time.
  • The reason is that every time the models.CreateUser() method in main.go is started, the test will be executed repeatedly. However, although the data already exists, an error will be reported, and the data already exists.
  • The solution is also very simple, just annotate the models.CreateUser() method directly.
  • At the same time, there is a better solution to determine whether data exists in the database and not save new data if it exists.
  • Unit testing can destroy data after each unit test, which can prevent the data of multiple unit tests from contaminating each other, causing the test results to be inconsistent with the expected results. image.png