Go實戰項目【六】中間件token鑒權和app升級功能

Middleware中間件

使用中間件可以對api接口訪問前后做處理。例如token校驗。接下來使用gin框架的中間件功能。
middleware/token.go

package middleware

import (
    "api/pkg/e"
    "api/pkg/util"
    "github.com/gin-gonic/gin"
    "time"
)

func TokenVer() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("authorization")   //從請求的header中獲取toekn字符串

        if token == "" {
            util.ResponseWithJson(e.ERROR_AUTH_TOKEN,"",c)
            c.Abort()
            return
        }else {
            claims, err := util.ParseToken(token)               //token校驗,claims的內容是自定義的荷載,可以根據里面的id取出用戶信息
            if err != nil {                                     //token校驗失敗,返回錯誤信息
                util.ResponseWithJson(e.ERROR_AUTH_CHECK_TOKEN_FAIL,"",c)
                c.Abort()
                return
            }else if time.Now().Unix() > claims.ExpiresAt {     //token過期,返回錯誤信息
                util.ResponseWithJson(e.ERROR_AUTH_CHECK_TOKEN_TIMEOUT,"",c)
                c.Abort()
                return
            }else {                 //token正確,可以進行后續的操作。設置用戶的ID和手機號,供后續方法使用
                c.Set("ID",claims.ID)
                //c.Set("Mobile",claims.Mobile)
                c.Next()
            }
        }
    }
}

從請求的header中獲取token,如果沒有的話直接返回錯誤信息,并終結此次請求。否則的話對token進行校驗。當token無誤后,進行下一步的操作。

接下來需要在路由文件中使用中間件方法
routers/routers.go

...
func InitRouter() *gin.Engine {
...
    apiv1 := r.Group("/api/v1/")    //路由分組,apiv1代表v1版本的路由組
    {
        ...
        apiv1Token := apiv1.Group("token/") //創建使用token中間件的路由組
        apiv1Token.Use(middleware.TokenVer())   //使用token鑒權中間件
        {

        }
...

創建了apiv1Token路由組,凡是這個路由組中的路由都需要使用token。

APP版本升級

APP版本更新升級功能的重要性不言而喻。這里把APP版本升級計劃成兩種方式,強制更新和選擇更新。

整體思路如下:
用戶在后臺需要更新app版本號、iOS最低可兼容的版本、安卓最低可兼容的版本、app升級文案、app下載地址、上傳安卓更新的apk文件。

首先客戶端請求更新接口,上傳以下請求參數:

  • 客戶端類型(iOS或安卓)
  • 客戶端版本號

1,根據客戶端上傳的版本號參數,先與后臺記錄的最新版本號相比較。如果小于最新版本號則記錄選擇更新。

2,再與后臺記錄的最低可兼容版本相比較,如果小于最低可兼容版本,則記錄為強制更新。

由于iOS只能在AppStore下載更新(非企業賬號)。所以對于iOS客戶端返回地址為AppStore的地址,由客戶端打開這個地址更新即可。

根據流程,設計app更新的數據表

版本更新表

字段名 類型 描述 備注
id int 自增長 ID 主鍵
version varchar(10) app最新版本號 格式:1.0.0
ios_min_version varchar(10) iOS端最低支持的版本號 格式:1.0.0
android_min_version varchar(10) 安卓端最低支持的版本號 格式:1.0.0
desc varchar(255) 升級文案 app端展示端升級文案
app_url varchar(255) app下載地址 安卓可以做應用內升級,iOS跳轉應用商店
app_size int apk文件大小 apk文件大小

創建model
models/app_version.go

package models

import "github.com/jinzhu/gorm"

type AppVersion struct {
    gorm.Model
    Version string      `gorm:"type:varchar(10);not null"`  //不為空
    IosMinVersion string    `gorm:"type:varchar(10)"`
    AndriodMinVersion string `gorm:"type:varchar(10)"`
    Desc string         `gorm:"type:varchar(255)"`
    AppUrl string       `gorm:"type:varchar(255)"`
    AppSize int
}

別忘了添加到自動遷移
models/models.go

...
db.AutoMigrate(&User{},&AppVersion{})
...

先來完成創建app版本功能。現在使用簡單的html頁面實現,后期做后臺管理功能再整合進去。

創建html文件,放在templates目錄下
templates/appversion.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
</head>
<body>

<form id="form" method="post" action="/manage/appversion" enctype="multipart/form-data">
    App版本號:<br>
    <input type="text" name="version" placeholder="格式:1.0.0">
    <br>
    iOS最低支持版本號:<br>
    <input type="text" name="ios_min_version" placeholder="格式:1.0.0">
    <br>
    安卓最低支持版本號:<br>
    <input type="text" name="android_min_version" placeholder="格式:1.0.0">
    <br>
    版本更新說明:<br>
    <textarea name="desc" rows="10" cols="30" placeholder="">

    </textarea>
    <br>
    app下載地址(僅限安卓)注意:如果上傳了apk文件,則此欄無效,下載地址與apk文件必須完成一個:<br>
    <input type="text" name="app_url">
    <br>
    <input type="file" name="apk_file">

    <br><br>
    <input type="submit" value="Submit">
</form>

</body>

</html>

一個很簡單的html文件,只有post上傳功能。
因為需要上傳apk功能,所以對apk上傳封裝一下
pkg/upload/apk.go

package upload

import (
    "api/pkg/file"
    "api/pkg/logging"
    "api/pkg/setting"
    "fmt"
    "os"
    "strings"
    "time"
)

//獲取apk文件的保存路徑,就是配置文件設置的   如:upload/apks/
func GetApkFilePath() string {
    return setting.AppSetting.ApkSavePath
}

//獲取apk文件完整訪問URL 如:http://127.0.0.1:8000/upload/apks/20190730/******.apk
func GetApkFullUrl(name string) string {
    return setting.AppSetting.ImagePrefixUrl + "/" +GetApkFilePath() + GetApkDateName() + name
}

//日期文件夾      如:20190730/
func GetApkDateName() string {
    t := time.Now()
    return fmt.Sprintf("%d%02d%02d/",t.Year(),t.Month(),t.Day())
}

//獲取apk文件在項目中的目錄  如:runtime/upload/apks/
func GetApkFullPath() string {
    return  setting.AppSetting.RuntimeRootPath + GetApkFilePath()
}

//檢查文件后綴,是否屬于配置中允許的后綴名
func CheckApkExt(fileName string) bool {
    ext := file.GetExt(fileName)

    if strings.ToLower(ext) == strings.ToLower(setting.AppSetting.ApkAllowExt) {
        return  true
    }

    return false
}

//檢查apk文件
func CheckApk(src string)error  {
    dir,err := os.Getwd()
    if err != nil {
        logging.Warn("pkg/upload/apk.go文件CheckApk方法os.Getwd出錯",err)
        return fmt.Errorf("os.Getwd err: %v", err)
    }

    err = file.IsNotExistMkDir(dir + "/" + src)     //如果不存在則新建文件夾
    if err != nil {
        logging.Warn("pkg/upload/apk.go文件CheckApk方法file.IsNotExistMkDir出錯",err)
        return fmt.Errorf("file.IsNotExistMkDir err: %v", err)
    }

    perm := file.CheckPermission(src)               //檢查文件權限
    if perm == true {
        logging.Warn("pkg/upload/apk.go文件CheckApk方法file.CheckPermission出錯",err)
        return fmt.Errorf("file.CheckPermission Permission denied src: %s", src)
    }

    return nil
}

更新網頁做好了,后臺需要做兩件事
1,加載appversion.html文件
2,實現/manage/appversion這個接口。

routers/routers.go

...
func InitRouter() *gin.Engine {
...
    r.LoadHTMLGlob("templates/*")    //渲染模版
    appManage := r.Group("/manage/") //后續做后臺管理頁面
    {
        appManage.GET("appversion", v1.GetAppVersionIndex) //app版本升級網頁文件
        appManage.POST("appversion", v1.CreateAppVersion)  //app版本升級api接口
    }

    return r
}

routers/v1/app_version.go

package v1

import (
    "api/pkg/e"
    "github.com/gin-gonic/gin"
)

//打開版本升級的html頁面(暫時這樣寫,后續的話完成后臺管理頁面)
func GetAppVersionIndex(c *gin.Context)  {
    c.HTML(e.SUCCESS,"appversion.html",gin.H{
        "title": "App版本升級",
    })
}

//創建app版本升級
func CreateAppVersion(c *gin.Context) {
    var appVersion models.AppVersion

    appVersion.Version = c.PostForm("version")          //新版本app版本號
    appVersion.IosMinVersion = c.PostForm("ios_min_version")    //iOS最低可兼容的版本
    appVersion.AndriodMinVersion = c.PostForm("android_min_version")    //安卓最低可兼容的版本
    appVersion.Desc = c.PostForm("desc")                        //app升級文案
    appVersion.AppUrl = c.PostForm("app_url")               //app下載地址

    //獲取上傳的apk文件
    apkFile,_ := c.FormFile("apk_file")

    //如果有上傳的apk文件
    if apkFile != nil {
        //判斷文件格式是否正確
        if ! upload.CheckApkExt(apkFile.Filename) {
            util.ResponseWithJson(e.ERROR,"apk文件格式不正確",c)
            return
        }

        //把上傳的文件移動到指定目錄
        savePath := upload.GetApkFilePath()     //保存的目錄 upload/apks/
        dataPath := upload.GetApkDateName()     //日期的目錄 20190730/
        fullPath := upload.GetApkFullPath() + dataPath  //圖片在項目中的目錄 runtime/upload/apks/20190730/
        src := fullPath + apkFile.Filename      //圖片在項目中的位置 runtime/upload/apks/****.apk

        //檢查文件路徑,這里面做了包括創建文件夾,檢查權限等操作
        if err := upload.CheckApk(fullPath); err != nil{
            util.ResponseWithJson(e.ERROR,"apk文件有問題",c)
            return
        }

        //使用c.SaveUploadedFile()把上傳的文件移動到指定到位置
        if err := c.SaveUploadedFile(apkFile, src); err != nil {
            util.ResponseWithJson(e.ERROR,"上傳apk失敗",c)
            return
        }

        //設置結構體的值
        appVersion.AppSize = int(apkFile.Size)  //獲取并設置apk文件大小
        appVersion.AppUrl = savePath + dataPath + apkFile.Filename  //數據庫中保存apk文件的路徑
    }

    //對參數做校驗
    valid := validation.Validation{}
    valid.Required(appVersion.Version,"version").Message("版本號必須填寫")
    valid.MinSize(appVersion.Desc,1,"minVersion").Message("升級文案最少1個字符")
    valid.Required(appVersion.AppUrl,"appUrl").Message("app升級地址必須填寫")
    if isOk := checkValidation(&valid, c); isOk == false {  //校驗不通過
        return
    }

    //數據庫創建數據
    if err := appVersion.CreateAppVersion(); err != nil {
        util.ResponseWithJson(e.ERROR,"保存版本信息失敗",c)
        return
    }

    //返回正確的數據
    util.ResponseWithJson(e.SUCCESS,appVersion,c)
}

models/app_version.go

...
//數據庫操作創建app升級版本
func (appVersion *AppVersion)CreateAppVersion()error  {
    err := db.Create(appVersion).Error
    return err
}

//獲取最新版本的信息
func GetVersion() *AppVersion {
    var appVersion AppVersion
    db.Last(&appVersion)
    return &appVersion
}

//數據庫查詢鉤子,在數據庫查詢之后執行的方法
func (appVersion *AppVersion)AfterFind() {
    appVersion.AppUrl = setting.AppSetting.ImagePrefixUrl + "/" + appVersion.AppUrl //拼接完整的apk的url地址
}

這樣就完成了升級app版本功能
如果是上傳的apk文件,還需要讓用戶能訪問到這個apk文件
routers/routers.go

...
func InitRouter() *gin.Engine {
...
    /*
        當訪問 $HOST/upload/apks 時,將會讀取到 項目/runtime/upload/apks 下的文件
        這樣就能讓外部訪問到圖片資源了
    */
    r.StaticFS(setting.AppSetting.ApkSavePath, http.Dir(setting.AppSetting.RuntimeRootPath+setting.AppSetting.ApkSavePath))

    return r
}

下面進行app獲取升級信息的接口,新增
routers/routers.go

...
        apiv1Token.Use(middleware.TokenVer())   //使用token鑒權中間件
        {
            apiv1Token.POST("version", v1.GetAppVersion) //app版本更新
        }
...

這里是把GetAppVersion路由放到了需要token鑒權的中間件中了,所以請求這個接口需要token
routers/v1/app_version.go

...
//獲取最新版本的app版本號
func GetAppVersion(c *gin.Context){
    //客戶端上傳的參數
    appID := c.PostForm("app_id")       //客戶端上傳的app_id參數,=1是iOS客戶端,=2是Android客戶端
    version := c.PostForm("version")    //客戶端上傳的安裝的app版本號

    appVersion := models.GetVersion()       //獲取數據庫中最新的版本信息
    //要返回的數據
    var responseData = gin.H{
        "needUpdate":0,
        "apkUrl":appVersion.AppUrl,
        "desc":appVersion.Desc,
        "version":appVersion.Version,
        "appSize":appVersion.AppSize,
    }

    //如果是iOS
    if appID == "1" {
        responseData["apkUrl"] = setting.AppSetting.AppStoreUrl     //返回應用商店地址

        a,b,c := VersionOrdinal(version),VersionOrdinal(appVersion.Version),VersionOrdinal(appVersion.IosMinVersion)
        //先比較是否是最新版本
        if a < b {
            responseData["needUpdate"] = 1  //不是最新版本提示可選升級
        }
        //再比較是否是最低支持的版本號
        if a < c {
            responseData["needUpdate"] = 2  //需要強制升級
        }
    }

    //如果是安卓
    if appID == "2" {
        a,b,c := VersionOrdinal(version),VersionOrdinal(appVersion.Version),VersionOrdinal(appVersion.AndriodMinVersion)
        //先比較是否是最新版本
        if a < b {
            responseData["needUpdate"] = 1  //不是最新版本提示可選升級
        }
        //再比較是否是最低支持的版本號
        if a < c {
            responseData["needUpdate"] = 2  //需要強制升級
        }
    }

    util.ResponseWithJson(e.SUCCESS,responseData,c)
}

//用于比較兩個字符串版本號的大小
func VersionOrdinal(version string) string {
    // ISO/IEC 14651:2011
    const maxByte = 1<<8 - 1
    vo := make([]byte, 0, len(version)+8)
    j := -1
    for i := 0; i < len(version); i++ {
        b := version[i]
        if '0' > b || b > '9' {
            vo = append(vo, b)
            j = -1
            continue
        }
        if j == -1 {
            vo = append(vo, 0x00)
            j = len(vo) - 1
        }
        if vo[j] == 1 && vo[j+1] == '0' {
            vo[j+1] = b
            continue
        }
        if vo[j]+1 > maxByte {
            panic("VersionOrdinal: invalid version")
        }
        vo = append(vo, b)
        vo[j]++
    }
    return string(vo)
}

點關注,不迷路

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。