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

点关注,不迷路

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342