使用 gin + gorm + seesion 完成 Laravel 的 make:auth

aen233發表於2019-09-11

go初學者,這篇是筆記,記錄和分享go入門實踐的歷程。
初學一門語言實踐自然從blog開始,個人打算是按照本站laravel教程入門實戰來搭個sample類微博小站。現在才開頭哈。
基礎文件讀完,差不多臉熟了一下go的語法,實踐環節要選框架,我沒多想就是gin+gorm,原因很簡單啊,本站文件裡就有iris、gin、gorm的這3個文件,iris和gin比起來我更傾向gin,因為iris看起來是個大而全的框架,laravel就是個大而全的框架了,學習go我想從小而精的gin開始。

以下是這幾天學習的路程步驟^_^

  1. gin+gorm搭簡單的mvc
  2. html模板
  3. session 登陸,自定義Auth中介軟體
  4. 密碼使用bcrypt加密驗證
  5. 分頁列表
  6. 結構最佳化,api介面和web動作分組
  7. 部署到阿里雲伺服器

gin+gorm搭簡單的mvc

當前目錄結構

這個步驟下來,檔案目錄結構為:

go-sample/
├── apis
│   ├── user.go
│   └── welcome.go
├── models
│   ├── init.go
│   └── user.go
├── router
│   └── router.go
├── main.go

新建專案,我的專案名叫go-sample
新建main.go,開啟gin的文件,複製“快速開始”的程式碼到這個檔案

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // 在 0.0.0.0:8080 上監聽並服務
}

這時候已經可以跑起來了

# 執行 main.go 並在瀏覽器上訪問 0.0.0.0:8080/ping
$ go run main.go

接下來要拆分程式碼了,新建route目錄和apis目錄,這個apis目錄其實就是controller層了,翻了好幾個go專案,叫apis的比較多,我也叫這個吧
新建 router/router.go

package router

import (
    "github.com/gin-gonic/gin"
    "go-sample/apis"
)

// SetupRouter index
func SetupRouter() *gin.Engine {
    r := gin.Default()
    r.GET("/welcome", apis.Welcome)
    return r
}

新建 apis/welcome.go

package apis

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

func Welcome(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "code": 1,
        "data": "hello-world",
    })
}

main.go改成

package main

import (
    "go-sample/router"
)

func main() {
    r := router.SetupRouter()
    r.Run(":3000") // 預設在 0.0.0.0:8080 上監聽並服務
}

這時候訪問 127.0.0.1:3000/welcome

使用gin + gorm + seesion 完成 laravel 的 make:auth

然後是gorm,新建models目錄
新建檔案 models/init.go

package models

import (
    "fmt"
    _ "github.com/go-sql-driver/mysql" //載入mysql
    "github.com/jinzhu/gorm"
)

var DB *gorm.DB

func Setup() {
    var err error
    // var user userModel.User
    DB, err = gorm.Open("mysql", "root:12345678@/iu56?charset=utf8&parseTime=True&loc=Local&timeout=10ms")

    if err != nil {
        fmt.Printf("mysql connect error %v", err)
    }

    if DB.Error != nil {
        fmt.Printf("database error %v", DB.Error)
    }
    AutoMigrateAll()
}

func AutoMigrateAll() {
    DB.AutoMigrate(&User{})
}

新建檔案 models/user.go

package models

import (
    "github.com/jinzhu/gorm"
)

type User struct {
    gorm.Model
    Name     string `json:"name"`
    Password string `json:"password"`
    Email    string `json:"email"`
    Gender   string `json:"gender"`
}

main.go改成

package main

import (
    "go-sample/models"
    "go-sample/router"
)

func init() {
    models.Setup()
}

func main() {
    r := router.SetupRouter()
    r.Run(":3000") // 預設在 0.0.0.0:8080 上監聽並服務
}

router/router.go改成

package router

import (
    "github.com/gin-gonic/gin"
    "go-sample/apis"
)

func SetupRouter() *gin.Engine {
    r := gin.Default()
    r.GET("/welcome",apis.Welcome)

    user := r.Group("/user")
    {
        user.GET("/index", apis.UserIndex)
    }

    return r
}

新建 apis/user.go

package apis

import (
    "github.com/gin-gonic/gin"
    "go-sample/models"
    "net/http"
)

func UserIndex(c *gin.Context) {
    var user models.User
    result := models.DB.Take(&user).Value  //開頭就寫最簡單的,獲取一條記錄,不指定排序

    c.JSON(http.StatusOK, gin.H{
        "code": 1,
        "data": result,
    })
}

這時候訪問127.0.0.1:3000/user/index

使用gin + gorm + seesion 完成 laravel 的 make:auth

知識點

  1. 要在$GOPATH/src 下新建專案
  2. 要加深對package的理解

session 登陸,自定義Auth中介軟體

當前目錄結構

這個步驟下來,檔案目錄結構如下:

go-sample/
├── apis
│   ├── auth.go
│   └── user.go
│   └── welcome.go
├── models
│   ├── init.go
│   └── user.go
├── pkg
│   ├── session.go
│   └── password.go
├── router
│   └── router.go
├── main.go

新建了pkg目錄,封裝的第三方包都在這裡,session和password都在這裡

使用cookie儲存session,session中介軟體

新建檔案 pkg/session.go

package pkg

import (
    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/gin"
    "go-sample/models"
    "net/http"
)

// gin session key
const KEY = "AEN233"

// 使用 Cookie 儲存 session
func EnableCookieSession() gin.HandlerFunc {
    store := cookie.NewStore([]byte(KEY))
    return sessions.Sessions("SAMPLE", store)
}

// session中介軟體
func AuthSessionMiddle() gin.HandlerFunc {
    return func(c *gin.Context) {
        session := sessions.Default(c)
        sessionValue := session.Get("userId")
        if sessionValue == nil {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Unauthorized",
            })
            c.Abort()
            return
        }
        // 設定簡單的變數
        c.Set("userId", sessionValue.(uint))

        c.Next()
        return
    }
}

// 註冊和登陸時都需要儲存seesion資訊
func SaveAuthSession(c *gin.Context, id uint) {
    session := sessions.Default(c)
    session.Set("userId", id)
    session.Save()
}

// 退出時清除session
func ClearAuthSession(c *gin.Context) {
    session := sessions.Default(c)
    session.Clear()
    session.Save()
}

func HasSession(c *gin.Context) bool {
    session := sessions.Default(c)
    if sessionValue := session.Get("userId"); sessionValue == nil {
        return false
    }
    return true
}

func GetSessionUserId(c *gin.Context) uint {
    session := sessions.Default(c)
    sessionValue := session.Get("userId")
    if sessionValue == nil {
        return 0
    }
    return sessionValue.(uint)
}

func GetUserSession(c *gin.Context) map[string]interface{} {

    hasSession := HasSession(c)
    userName := ""
    if hasSession {
        userId := GetSessionUserId(c)
        userName = models.UserDetail(userId).Name
    }
    data := make(map[string]interface{})
    data["hasSession"] = hasSession
    data["userName"] = userName
    return data
}

密碼使用x/crypto/bcrypt加密比對

新建檔案 pkg/password.go

package pkg

import "golang.org/x/crypto/bcrypt"

// 密碼加密
func Encrypt(source string) (string, error) {
    hashPwd, err := bcrypt.GenerateFromPassword([]byte(source), bcrypt.DefaultCost)
    return string(hashPwd), err
}

// 密碼比對 (傳入未加密的密碼即可)
func Compare(hashedPassword, password string) error {
    return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

註冊、登陸、退出介面

新建檔案 apis.go,主要是註冊、登陸、退出、獲取當前使用者

package apis

import (
    "github.com/gin-gonic/gin"
    "go-sample/models"
    "go-sample/pkg"
    "net/http"
)

func Login(c *gin.Context) {
    name := c.Request.FormValue("name")
    password := c.Request.FormValue("password")

    if hasSession := pkg.HasSession(c); hasSession == true {
        c.String(200, "使用者已登陸")
        return
    }

    user := models.UserDetailByName(name)

    if err := pkg.Compare(user.Password, password); err != nil {
        c.String(401, "密碼錯誤")
        return
    }

    pkg.SaveAuthSession(c, user.ID)

    c.String(200, "登入成功")
}

func Logout(c *gin.Context) {
    if hasSession := pkg.HasSession(c); hasSession == false {
        c.String(401, "使用者未登陸")
        return
    }
    pkg.ClearAuthSession(c)
    c.String(200, "退出成功")
}

func Register(c *gin.Context) {
    var user models.User
    user.Name = c.Request.FormValue("name")
    user.Email = c.Request.FormValue("email")

    if hasSession := pkg.HasSession(c); hasSession == true {
        c.String(200, "使用者已登陸")
        return
    }

    if existUser := models.UserDetailByName(user.Name); existUser.ID != 0 {
        c.String(200, "使用者名稱已存在")
        return
    }

    if c.Request.FormValue("password") != c.Request.FormValue("password_confirmation") {
        c.String(200, "密碼不一致")
        return
    }

    if pwd, err := pkg.Encrypt(c.Request.FormValue("password")); err == nil {
        user.Password = pwd
    }

    models.AddUser(&user)

    pkg.SaveAuthSession(c, user.ID)

    c.String(200, "註冊成功")

}

func Me(c *gin.Context) {
    currentUser := c.MustGet("userId").(uint)
    c.JSON(http.StatusOK, gin.H{
        "code": 1,
        "data": currentUser,
    })
}

model中封裝方法

models/user.go 新增方法

// 使用者註冊時新增
func AddUser(user *User) {
    DB.Create(&user)
    return
}

func UserDetailByName(name string) (user User) {
    DB.Where("name = ?", name).First(&user)
    return
}

func UserDetailByEmail(email string) (user User) {
    DB.Where("email = ?", email).First(&user)
    return
}

func UserDetail(id uint) (user User) {
    DB.Where("id = ?", id).First(&user)
    return
}

路由中加入Auth中介軟體

修改 router/router.go

package router

import (
    "github.com/gin-gonic/gin"
    "go-sample/apis"
    "go-sample/pkg"
)

func SetupRouter() *gin.Engine {
    r := gin.Default()

    user := r.Group("/user")
    {
        user.GET("/index", apis.UserIndex)
    }

    // use session router
    sr := r.Group("/", pkg.EnableCookieSession())
    {

        sr.GET("/welcome", apis.Welcome)
        sr.GET("/login", apis.Login)
        sr.POST("/register", apis.Register)
        sr.GET("/logout", apis.Logout)

        authorized := sr.Group("/auth", pkg.AuthSessionMiddle())
        {
            authorized.GET("/me", apis.Me)
        }
    }

    return r
}

HTML 渲染,api介面和web動作分組

當前目錄結構

這個步驟下來,檔案目錄結構如下:

go-sample/
├── actions
│   ├── auth.go
│   └── welcome.go
├── apis
│   ├── auth.go
│   └── user.go
│   └── welcome.go
├── assets
│   ├── app.css
│   └── app.js
├── models
│   ├── init.go
│   └── user.go
├── pkg
│   ├── session.go
│   └── password.go
├── router
│   └── router.go
├── views
│   ├── layouts
│   │   ├── head.html
│   │   └── nav.html
│   └── page
│   │   ├── login.html
│   │   └── register.html
│   │   └── welcome.html
├── main.go

新加了actions目錄、assets目錄、views目錄
actions目錄和api目錄邏輯處理一致,返回不同,一個是介面,一個是web動作。
aseets目錄放置js和css。
views目錄放置頁面,和laravel類似,有一個layouts目錄放置公共模板。

go-template使用

todo

路由修改

router/router.go修改為:

package router

import (
    "github.com/gin-gonic/gin"
    "go-sample/actions"
    "go-sample/apis"
    "go-sample/pkg"
    "net/http"
)

func SetupRouter() *gin.Engine {
    r := gin.Default()

    pageGroup(r)  // 將web頁面路由提出來

    user := r.Group("/user")
    {
        user.GET("/index", apis.UserIndex)
    }

    // use session router
    sr := r.Group("/", pkg.EnableCookieSession())
    {

        sr.GET("/welcome", apis.Welcome)
        sr.GET("/login", apis.Login)
        sr.POST("/register", apis.Register)
        sr.GET("/logout", apis.Logout)

        authorized := sr.Group("/auth", pkg.AuthSessionMiddle())
        {
            authorized.GET("/me", apis.Me)
        }
    }

    return r
}

func pageGroup(r *gin.Engine) *gin.Engine {
    r.LoadHTMLGlob("views/**/*")
    // use session router
    sr := r.Group("/page", pkg.EnableCookieSession())
    {
        sr.StaticFS("/assets", http.Dir("assets"))
        sr.GET("/welcome", actions.Welcome)
        sr.GET("/login_page", actions.LoginPage)
        sr.GET("/register_page", actions.RegisterPage)
        sr.POST("/login", actions.Login)
        sr.POST("/register", actions.Register)
        sr.POST("/logout", actions.Logout)

        authorized := sr.Group("/auth", pkg.AuthSessionMiddle())
        {
            authorized.GET("/me", actions.Me)
        }
    }
    return r
}

分頁列表

修改apis/user.go

package apis

import (
    "github.com/gin-gonic/gin"
    "go-sample/models"
    "math"
    "net/http"
    "strconv"
)
func UserIndex(c *gin.Context) {
    page, _ := strconv.Atoi(c.Query("page"))
    size, _ := strconv.Atoi(c.Query("size"))

    data := models.GetUsers(page, size)

    meta := make(map[string]interface{})
    total, _ := models.GetUserTotal()
    meta["total"] = total
    meta["current_page"] = page
    meta["per_page"] = size
    meta["last_page"] = math.Ceil(float64(total/size)) + 1

    c.JSON(http.StatusOK, gin.H{
        "code": 1,
        "data": data,
        "meta": meta,
    })
}

models/user.go 新增方法

func GetUserTotal() (int, error) {
    var count int
    if err := DB.Model(&User{}).Count(&count).Error; err != nil {
        return 0, err
    }

    return count, nil
}

func GetUsers(page int, size int) (users []User) {
    // limit定位每頁大小, Offset定位偏移的查詢位置.並且先進行條件篩選,最後做分頁操作.
    DB.Offset((page - 1) * size).Limit(size).Find(&users)
    return
}

訪問127.0.0.1:3000/user/index?page=1&size=15

使用 gin + gorm + seesion 完成 Laravel 的 make:auth

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

相關文章