iris-go 框架構建登陸 API 專案開發過程

snowlyg發表於2020-01-11

iris-go 框架構建登陸 api 專案開發過程

具體專案例項 github.com/snowlyg/IrisAdminApi


很多新手學習 iris-go 的時候,看文件會覺得有些零散。而且英文文件對有些小夥伴來說還是有些吃力,就算用上翻譯軟體有些地方也會翻譯的生澀難以理解。這篇文章主要會詳細的講解一下,我在寫一個小專案的實現過程。本人功力有限,如果有錯誤地方,希望大家友善的指出。

前言
  • 首先需要安裝 golang 環境 。具體安裝教程可以檢視 Go 入門指南
  • 本地安裝 mysql 或者 gcc (sqlite3) 環境 。gcc 下載地址 建議下載解壓版本,安裝版本下載會比較慢。
  • 開啟 GO111MODULE 模式,設定映象
    go env -w GO111MODULE=on
    go env -w GOPROXY=https://goproxy.cn,direct
  • 初始化 go.mod 檔案
    go mod init
  • 安裝 gowatch ,類似 bee run 的一個工具。
    go get github.com/silenceper/gowatch

注意:如果是使用 goland Ide , 需要在 goland 的設定中為每一個專案單獨開啟 go moudels , 並且設定映象地址,如下圖。

iris-go 框架構建登陸 API 專案開發過程

新建專案
  • 在 go/src/ (GOPATH) 目錄下新建資料夾 IrisAdminApi,並新建 main.go 檔案。官方文件例子 https://iris-go.com/start/#installation。
    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))
    }
  • 載入相關包 go mod tidy,此時 go.modgo.sum 會載入包的相關資訊。
    // 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
    )
  • 啟動專案 ,在專案目錄執行 gowatch 或者 go run main.go
  • 輸入 localhost:8080 ,得到如下顯示就表啟動成功了。

Iris-go 專案 casbin + gorm 構建 API 許可權控制實現過程

實現登陸,退出功能
  • 到現在我們只是實現了一個簡單的 web 伺服器,和幾個簡單的介面。接下來我們要來實現網站的基本功能登陸和退出。

  • 這裡我們使用單元測試驅動開發相關介面,這樣做的我們的專案後期的維護會變得相對容易一些。

  • 首先要修改我們的 main.go 檔案,新建一個 NewApp 方法,這個方法返回-個 *iris.Application ,這個是 iris.Application 的一個指標。(具體為什麼是指標,這裡就不詳細講解了。可以去看下 [Go 入門指南]《Go 入門指南》 ),這個方法在單元測試會用到。

    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
    }
  • 在專案目錄下新建測試檔案 base_test.go` ,測試檔案都以 _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
    }
  • 在專案目錄下新建測試檔案 auth_test.go

    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, "登陸成功")
    }
  • 執行單元測試 go test -run TestUserLoginSuccess 單獨執行登陸成功測試。得到如下錯誤:

Iris-go 專案 casbin + gorm 構建 API 許可權控制實現過程

  • 為什麼會報錯,從報錯資訊我們知道本來要得到 200 的狀態碼,結果返回了 404 。因為我們沒有定義登陸的路由。這裡我們使用 gorm 包來管理資料,並使用 jwt 作為介面認證方式。
  • 新建 user 模型,token 模型並實現相關方法。
  • 新建資料庫 iris , tiris 。這裡要注意資料庫的字符集要修改為 utf-8 ,不然會出中文亂碼的情況。
  • 引入了三個新的依賴包
      "github.com/iris-contrib/middleware/jwt"
      "github.com/jameskeane/bcrypt"
      "github.com/jinzhu/gorm"
      _ "github.com/jinzhu/gorm/dialects/mysql"
  • 增加 跨域中介軟體 和 jwt 認證中介軟體
  • 完整程式碼如下
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("HS2JDFKhu7Y1av7b"))

         oauthToken := new(OauthToken)
         oauthToken.Token = tokenString
         oauthToken.UserId = user.ID
         oauthToken.Secret = "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("HS2JDFKhu7Y1av7b")
   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 = "root:wemT5ZNuo074i4FNsTwl4KhfVSvOlBcF@(127.0.0.1:3306)/tiris?charset=utf8&parseTime=True&loc=Local"
  } else {
      conn = "root:wemT5ZNuo074i4FNsTwl4KhfVSvOlBcF@(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))
}
  • 這時候再執行測試命令 go test -run TestUserLoginSuccess 我們會得到如下錯誤:

Iris-go 專案 casbin + gorm 構建 API 許可權控制實現過程

  • 現在已經不再是 404 的錯誤了, 而是返回一個使用者不存在的資訊。原因是雖然我們增加了路由已經一系列的相關程式碼
  • 但是資料庫並沒有使用者資料 ,我們自然無法查詢到任何使用者資訊。
  • 那麼要如何解決這問題呢?
  • 同時你會發現到現在的 main.go 檔案非常龐大臃腫,無論是修改程式碼,還是追蹤錯誤都是非常麻煩。應該如何調整?
  • 前文講到 許可權控制又該如何實現?
重構專案結構
  • 現在我們調整檔案程式碼,劃分專案結構如下
  • 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 // 中介軟體
    • auth.go // 驗證登陸
      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 // 跨域認證
      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 認證
package middleware
import (
    "github.com/iris-contrib/middleware/jwt"
)
/**
 * 驗證 jwt
 * @method JwtHandler
 */
func JwtHandler() *jwt.Middleware {
    var mySecret = []byte("HS2JDFKhu7Y1av7b")
    return jwt.New(jwt.Config{
        ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
            return mySecret, nil
        },
        SigningMethod: jwt.SigningMethodHS256,
    })
}
  • models // 模型
    • base.go // 初始化資料庫,公共方法
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 = "root:wemT5ZNuo074i4FNsTwl4KhfVSvOlBcF@(127.0.0.1:3306)/tiris?charset=utf8&parseTime=True&loc=Local"
    } else {
        conn = "root:wemT5ZNuo074i4FNsTwl4KhfVSvOlBcF@(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模型
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 // 使用者模型
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("HS2JDFKhu7Y1av7b"))
            oauthToken := new(OauthToken)
            oauthToken.Token = tokenString
            oauthToken.UserId = user.ID
            oauthToken.Secret = "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 // 路由

    • 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 // 單元測試

    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))
    }

    Iris-go 專案 casbin + gorm 構建 API 許可權控制實現過程

  • models/user.go 增加建立使用者方法

    func CreateUser() (user *User) {
      salt, _ := bcrypt.Salt(10)
      hash, _ := bcrypt.Hash("password", salt)
      user = &User{
          Username: "username",
          Password: hash,
          Name:     "name",
      }
      if err := Db.Create(user).Error; err != nil {
          color.Red(fmt.Sprintf("CreateUserErr:%s \n ", err))
      }
      return
    }
  • main.go 呼叫方法 models.CreateUser() 程式碼如下

    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()
      })
      models.CreateUser() //建立使用者
      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))
    }
  • 此時我們再次執行單元測試 go test -run TestUserLoginSuccess 得到如下顯示,表示單元測試透過。
    Iris-go 專案 casbin + gorm 構建 API 許可權控制實現過程

  • 到此,登陸的介面基本完成。

  • 但是還有很多問題沒有解決,比如重複啟動專案或者執行測試會得到如下提示,雖然測試還是透過,卻會同時有一個報錯。

  • 原因是 main.gomodels.CreateUser() 方法每次啟動,測試都會重複執行。但是資料已經有的資料雖有會報錯,資料已經存在的錯誤。

  • 要解決的方法也很簡單,直接註釋 models.CreateUser() 方法即可。

  • 同時還有更好的解決方法,判斷資料庫是否存在資料,如果存在就不儲存新資料。

  • 單元測試則可以在每次單元測試後摧毀資料,這樣可以防止多個單元測試的資料相互汙染,導致測試結果和預期結果不符合。
    Iris-go 專案 casbin + gorm 構建 API 許可權控制實現過程

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章