Yalda

哈库呐玛塔塔

嗨,我是张志方,一名 iOS 开发者。


六、中间件token鉴权和app升级功能

本篇文章主要为项目添加中间件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文件大小
    

    创建modelmodels/app_version.go ```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
```go
...
db.AutoMigrate(&User{},&AppVersion{})
...

先来完成创建app版本功能。现在使用简单的html页面实现,后期做后台管理功能再整合进去。 创建html文件,放在templates目录下
templates/appversion.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <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
}

更新网页做好了,后台需要做两件事,加载appversion.html文件,实现/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)
}
最近的文章

nginx部署应用

首先需要在云服务器平台购买一台云服务器,然后使用ssh登录,在terminal中安装相应的软件与架构。以下是在Ubuntu服务器上从头开始部署一个Go应用程序并使用Nginx进行反向代理的具体步...…

继续阅读

更早的文章

五、登录注册接口开发和JWT

用户模型已经构建完了,可以愉快的写接口开发了,来编写第一个接口,登录/注册接口首先定义接口routers/routers.go...apiv1.POST("auth",v1.AuthStore)...…

继续阅读
-->