Go實戰(zhàn)項目【六】中間件token鑒權(quán)和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的內(nèi)容是自定義的荷載,可以根據(jù)里面的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正確缔杉,可以進行后續(xù)的操作。設(shè)置用戶的ID和手機號搁料,供后續(xù)方法使用
                c.Set("ID",claims.ID)
                //c.Set("Mobile",claims.Mobile)
                c.Next()
            }
        }
    }
}

從請求的header中獲取token或详,如果沒有的話直接返回錯誤信息系羞,并終結(jié)此次請求。否則的話對token進行校驗霸琴。當token無誤后椒振,進行下一步的操作。

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

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

        }
...

創(chuàng)建了apiv1Token路由組杠人,凡是這個路由組中的路由都需要使用token。

APP版本升級

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

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

首先客戶端請求更新接口横浑,上傳以下請求參數(shù):

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

1剔桨,根據(jù)客戶端上傳的版本號參數(shù),先與后臺記錄的最新版本號相比較徙融。如果小于最新版本號則記錄選擇更新洒缀。

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

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

根據(jù)流程职车,設(shè)計app更新的數(shù)據(jù)表

版本更新表

字段名 類型 描述 備注
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下載地址 安卓可以做應(yīng)用內(nèi)升級瘫俊,iOS跳轉(zhuǎn)應(yīng)用商店
app_size int apk文件大小 apk文件大小

創(chuàng)建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{})
...

先來完成創(chuàng)建app版本功能。現(xiàn)在使用簡單的html頁面實現(xiàn)悴灵,后期做后臺管理功能再整合進去扛芽。

創(chuàng)建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文件的保存路徑庐船,就是配置文件設(shè)置的   如: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)               //檢查文件權(quán)限
    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
}

更新網(wǎng)頁做好了嘲更,后臺需要做兩件事
1筐钟,加載appversion.html文件
2,實現(xiàn)/manage/appversion這個接口赋朦。

routers/routers.go

...
func InitRouter() *gin.Engine {
...
    r.LoadHTMLGlob("templates/*")    //渲染模版
    appManage := r.Group("/manage/") //后續(xù)做后臺管理頁面
    {
        appManage.GET("appversion", v1.GetAppVersionIndex) //app版本升級網(wǎng)頁文件
        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頁面(暫時這樣寫篓冲,后續(xù)的話完成后臺管理頁面)
func GetAppVersionIndex(c *gin.Context)  {
    c.HTML(e.SUCCESS,"appversion.html",gin.H{
        "title": "App版本升級",
    })
}

//創(chuàng)建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

        //檢查文件路徑,這里面做了包括創(chuàng)建文件夾宠哄,檢查權(quán)限等操作
        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
        }

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

    //對參數(shù)做校驗
    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
    }

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

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

models/app_version.go

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

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

//數(shù)據(jù)庫查詢鉤子壹将,在數(shù)據(jù)庫查詢之后執(zhí)行的方法
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鑒權(quán)中間件
        {
            apiv1Token.POST("version", v1.GetAppVersion) //app版本更新
        }
...

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

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

    appVersion := models.GetVersion()       //獲取數(shù)據(jù)庫中最新的版本信息
    //要返回的數(shù)據(jù)
    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     //返回應(yīng)用商店地址

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

點關(guān)注暴区,不迷路

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市辛臊,隨后出現(xiàn)的幾起案子仙粱,更是在濱河造成了極大的恐慌,老刑警劉巖彻舰,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伐割,死亡現(xiàn)場離奇詭異,居然都是意外死亡淹遵,警方通過查閱死者的電腦和手機口猜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門负溪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來透揣,“玉大人,你說我怎么就攤上這事川抡》妫” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵崖堤,是天一觀的道長侍咱。 經(jīng)常有香客問我,道長密幔,這世上最難降的妖魔是什么楔脯? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮胯甩,結(jié)果婚禮上昧廷,老公的妹妹穿的比我還像新娘堪嫂。我一直安慰自己,他們只是感情好木柬,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布皆串。 她就那樣靜靜地躺著,像睡著了一般眉枕。 火紅的嫁衣襯著肌膚如雪恶复。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天速挑,我揣著相機與錄音谤牡,去河邊找鬼。 笑死姥宝,一個胖子當著我的面吹牛拓哟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播伶授,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼断序,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了糜烹?” 一聲冷哼從身側(cè)響起违诗,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎疮蹦,沒想到半個月后诸迟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡愕乎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年阵苇,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片感论。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡绅项,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出比肄,到底是詐尸還是另有隱情快耿,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布芳绩,位于F島的核電站掀亥,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏妥色。R本人自食惡果不足惜搪花,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧撮竿,春花似錦丁稀、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惑折,卻和暖如春授账,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惨驶。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工白热, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人粗卜。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓屋确,卻偏偏與公主長得像,于是被迫代替她去往敵國和親续扔。 傳聞我的和親對象是個殘疾皇子攻臀,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355