Bootstrap

[golang gin框架] 36.Gin 商城项目-RESTful API 设计指南,允许Cros跨域 ,提供api接口实现前后端分离,以及JWT的使用

一.RESTful API 设计指南

请看:Restful API 的接口规范

二.Gin 中配置服务器端允许跨域

github官方地址: https://github.com/gin-contrib/cors
main.go文件中配置跨域请求

代码如下:

在使用cors时,需要 引入该插件,先:
import (
"github.com/gin-contrib/cors"
)
然后在main.go下运行命令 : go mod tidy, 即可
package main

import (
    "fmt"
    "github.com/gin-contrib/sessions"
    _ "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-contrib/sessions/redis"
    "github.com/gin-gonic/gin"
    "gopkg.in/ini.v1"
    "goshop/models"
    "goshop/routers"
    "html/template"
    "github.com/gin-contrib/cors"
    "os"
    "path/filepath"
    "strings"
    "time"
)

func main() {
    //初始化路由,会设置默认中间件:engine.Use(Logger(), Recovery()),可以使用gin.New()来设置路由
    r := gin.Default()
    //配置gin允许跨域请求
    //默认配置
    //r.Use(cors.Default())
    r.Use(cors.New(cors.Config{  //自定义配置
        AllowMethods:     []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"},  //允许的方法
        AllowHeaders:     []string{"Origin", "Content-Length", "Content-Type", "Authorization"},  //header允许山高月小
        AllowCredentials: false,
        MaxAge:           12 * time.Hour,  //有效时间
        ExposeHeaders:    []string{"Content-Length"},
        AllowOriginFunc: func(origin string) bool {  //允许的域
            return true  //所有
        },
    }))
    r.run()
}

三.api接口实现前后端分离

在前后端分离模式下的项目,需要使用api接口来实现数据的交互,一般接口使用 RESTful API模式设计,当前端请求接口时,有时候会发生跨域问题,需要服务器方面进行跨域配置(见 二.Gin 中配置服务器端允许跨域),下面上案例说明:
  1. 路由

配置 路由时,可以项目迭代,存在多个版本的api,格式如下:
package routers

import (
    "goshop/controllers/api"
    "github.com/gin-gonic/gin"
)

//设置api路由
func ApiRoutersInit(r *gin.Engine) {
    //多版本api
    apiRouters := r.Group("/v1")
    {
        //获取导航列表
        apiRouters.GET("/navList", api.NavController{}.Navlist)
        //登录操作
        apiRouters.POST("/doLogin", api.UserController{}.DoLogin)
        //编辑文章操作
        apiRouters.PUT("/editArticle", api.ArticleController{}.EditArticle)
        //删除评论操作
        apiRouters.DELETE("/deleteComment", api.CommentController{}.DeleteComment)
    }

    api2Routers := r.Group("/v2")
    {
        //获取导航列表
        api2Routers.GET("/navList", api.NavController{}.Navlist)
        //登录操作
        api2Routers.POST("/doLogin", api.UserController{}.DoLogin)
        //编辑文章操作
        api2Routers.PUT("/editArticle", api.ArticleController{}.EditArticle)
        //删除评论操作
        api2Routers.DELETE("/deleteComment", api.CommentController{}.DeleteComment)
   }
}
  1. 控制器代码

各个不同的功能模块可以在controllers/api/下创建不同的控制器,比如:
导航相关api: 在controllers/api/下创建NavController.go控制器,里面存放导航相关api(获取顶部导航,中部导航...)
用户相关api: 在controllers/api/下创建UserController.go控制器,里面存放用户相关api(获取用户信息,登录,登出,...)
文章相关api: 在controllers/api/下创建ArticleController.go控制器,里面存放文章相关api(获取文章列表,文章详情,...)
评论相关api: 在controllers/api/下创建CommentController.go控制器,里面存放用户评论相关api(获取用户评论列表,增加评论,删除评论,...)
下面就增删改查各举一个案例:

导航相关控制器

获取导航列表
package api

import (
    "encoding/json"
    "goshop/models"
    "github.com/gin-gonic/gin"
    "net/http"
)

type V1Controller struct{}

//获取导航列表
func (con V1Controller) Navlist(c *gin.Context) {
    navList := []models.Nav{}
    models.DB.Find(&navList)
    c.JSON(http.StatusOK, gin.H{
        "navList": navList,
    })
}

返回的json数据如下:

{
    "navList": [
        {
            "id": 1,
            "title": "商城1",
            "link": "http://www.xxx.com",
            "position": 2,
            "is_opennew": 2,
            "relation": "36,35",
            "sort": 10,
            "status": 1,
            "add_time": 1592919226,
            "goods_items": null
       },
       ...
    ]
}

用户相关控制器

用户登录操作:
POST方式
注意: api请求有两种请求格式:
1. form-data 表单格式,服务器需使用 c.PostForm获取数据
2. Content-Type: application/json格式,服务器需使用 c.GetRawData()获取数据
案例如下:
//api 当前端发送请求类型为:Content-Type: application/json,时,c.PostForm没法获取,需要通过c.GetRawData() 获取
//Content-Type: application/json; 发过来的数据需要通过c.GetRawData() 获取
//用户相关结构体: 在实际项目中,可以在models下创建相关结构体
type UserInfo struct {
    Username string `form:"username" json:"username"`
    Password string `form:"password" json:"password"`
}

//Content-Type: application/json; 发过来的数据需要通过c.GetRawData() 获取
func (con V1Controller) DoLogin(c *gin.Context) {
    var userInfo UserInfo
    b, _ := c.GetRawData() //从 c.Request.Body 读取请求数据
    err := json.Unmarshal(b, &userInfo)
    if err != nil {
        c.JSON(200, gin.H{
            "err": err.Error(),
        })
    } else {
        c.JSON(200, gin.H{
            "userInfo": userInfo,
        })
    }
}

//form-data 表单格式,服务器需使用c.PostForm获取数据
func (con V1Controller) DoLoginPost(c *gin.Context) {
    //实例化user结构体
    userInfo := models.User{}
    //获取请求的数据
    username:= c.PostForm("username")
    if username == "" {
        c.JSON(200, gin.H{
            "err": "用户名不能为空",
        })
    } else {
        c.JSON(200, gin.H{
            "username": username,
        })
    }
}

文章相关控制器

修改文章数据:
//文章结构体: 在项目中可以在models下面创建结构体
type Article struct {
    Title   string `form:"title" json:"title"`
    Content string `form:"content" json:"content"`
}

//编辑
//Content-Type: application/json,发过来的数据需要通过c.GetRawData() 获取
func (con V1Controller) EditArticle(c *gin.Context) {
    var article Article
    b, _ := c.GetRawData() //从 c.Request.Body 读取请求数据
    err := json.Unmarshal(b, &article)
    if err != nil {
        c.JSON(200, gin.H{
            "err": err.Error(),
        })
    } else {
        c.JSON(200, gin.H{
            "article": article,
        })
    }
}

评论相关控制器

删除相关评论
//删除
func (con V1Controller) DeleteNav(c *gin.Context) {
    id := c.Query("id")
    //执行删除逻辑操作
    c.JSON(200, gin.H{
        "message": "删除数据成功",
        "id":      id,
    })
}

四.JWT接口权限验证

  1. 关于接口的安全验证

关于接口安全验证的解决方案有很多:
  • 可以用 Session 来实现安全验证

  • 对请求接口的参数进行签名,来实现接口的签名验证

  • 使用 JWT 实现接口的验证

  • ...

  1. 基于 Session 的安全验证

Session 存储在服务器,用户用户比较少的话是一种简单的安全验证机制,但是涉及到跨域的话需要进行一些配置,用户量非常非常大的话会耗费一定的服务器资源,关于 cookie 和 session 跨域可以参考: 解决vue请求gin框架接口cros跨域cookie和session失效的问题
  1. 对请求参数进行加密的签名验证

涉及公钥、私钥、签名等,比如支付相关功能接口
  1. JWT

JWT 全称 JSON Web Token,是目前比较流行的另一种跨域身份验证解决方案。也是被很多人用坏的一种安全验证机制
  1. Golang 中使用 JWT 实现接口的安全验证

这里使用 https://github.com/dgrijalva/jwt-go模块,使用步骤如下 :

(1).下载引入模块

import 中引入 github.com/dgrijalva/jwt-go,然后在main.go目录下运行go mod tidy 即可
import ( 
    "fmt"
    "strings"
    "time"
    "github.com/gin-gonic/gin"
    "github.com/dgrijalva/jwt-go"
)

(2).生成 Jwt Token

1).自定义一个结构体
首先需要 自定义一个结构体,这个结构体需要继承 jwt.StandardClaims 结构体,这个结构体也可以 自定义结构体属性,自定义的属性用于 Jwt 传值
type MyClaims struct {
    Uid int
    jwt.StandardClaims
}
2).定义生成结构体的私钥 key 以及过期时间
var jwtKey = []byte("123456")
var expireTime = time.Now().Add(24 * time.Hour).Unix()
3).实例化自定义的结构体,创建 token
myClaimsObj := MyClaims{
    12, // 生成 token 的时候传值
    jwt.StandardClaims{
        ExpiresAt: expireTime, Issuer: "userinfo", // 签发人
}, }
// 使用指定的签名方法创建签名对象
tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaimsObj)
// 使用指定的 secret 签名并获得完整的编码后的字符串 token
tokenStr, _ := tokenObj.SignedString(jwtkey)

(3).验证 Jwt Token

1).Gin 中获取客户端穿过来的值 token 值
注意:
1.服务器生成的token传给客户端后,客户端保存在 Authorization
2.服务端生成的token校验方法使用的是 OAuth2.0,故客户端请求服务器时,Authorization的TYPE应该为OAuth2.0
tokenData := c.Request.Header.Get("Authorization")
tokenString := strings.Split(tokenData, " ")[1]
2).服务端定义一个方法验证 token
func ParseToken(tokenString string) (*jwt.Token, *MyClaims, error) {
    s := &MyClaims{}
    token, err := jwt.ParseWithClaims(tokenString, s, func(token *jwt.Token)(i interface{}, err error) {
    return jwtkey, nil
})

gin.Info(token, s)
return token, s, err
}
3).验证完整代码
tokenData := c.Ctx.Input.Header("Authorization")
tokenString := strings.Split(tokenData, " ")[1]
if tokenString == "" {
    fmt.Println("权限不足")
} else {
    token, claims, err := ParseToken(tokenString)
if err != nil || !token.Valid {
    fmt.Println("权限不足")
} else {
    fmt.Println("验证通过")
    fmt.Println(claims.Uid)
}
}
  1. 具体使用JWT案例

以用户登录后,获取收货地址为例(在这里为了方便,请求都以GET方式),具体步骤:
1.客户端请求路由login,获取服务端生成的token并保存到 Authorization
2.客户端请求路由addressList,获取用户收货地址; 注意:客户端一定要把 Authorization 传给服务端校验,并且TYPE= OAuth2.0

(1).路由

在routers/apiRouters.go下增加以下路由
//登录操作(生成token)
apiRouters.GET("/login", api.UserController{}.Login)
//获取收货地址(校验token)
apiRouters.GET("/addressList", api.UserController{}.AddressList)

(2).服务端生成token

在models文件下,创建MyClaims.go,封装一个MyClaims结构体,创建方法:设置token,获取token的方法
package models

import (
    "github.com/dgrijalva/jwt-go"
    "strings"
    "time"
)

//定义key和过期时间
var jwtKey = []byte("www.xxx.comx") //byte类型的切片
var expireTime = time.Now().Add(24 * time.Hour).Unix()

//自定义一个结构体,这个结构体需要继承 jwt.StandardClaims 结构体
type MyClaims struct {
    Uid int //自定义的属性 用于不同接口传值
    jwt.StandardClaims
}

//设置token
func SetToken(uid int) (string, error) {
    //实例化 存储token的结构体
    myClaimsObj := MyClaims{
        uid, //自定义参数: 可自行传值
        jwt.StandardClaims{
            ExpiresAt: expireTime, //过期时间
            Issuer:    "www.xxx.com",
        },
    }

    // 使用指定的签名方法创建签名对象
    tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaimsObj)
    // 使用指定的 secret 签名并获得完整的编码后的字符串 token
    tokenStr, err := tokenObj.SignedString(jwtKey)
    if err != nil {
        return "", err
    }

    return tokenStr, nil
}

func GetToken(tokenData string, uid int) (int, error) {
    //获取token
    tokenStr := strings.Split(tokenData, " ")[1]
    //校验token
    token, myClaims, err := ParseToken(tokenStr)

    if err != nil || !token.Valid { //校验失败
        return 0, err
    } else {
        return myClaims.Uid, nil
    }
}

//验证token是否合法
func ParseToken(tokenStr string) (*jwt.Token, *MyClaims, error) {
    myClaims := &MyClaims{}
    token, err := jwt.ParseWithClaims(tokenStr, myClaims, func(token *jwt.Token) (i interface{}, err error) {
        return jwtKey, nil
    })
    return token, myClaims, err
}
在controllers/api/UserController.go下创建login,addressList方法,并调用models.Myclaims结构体中的GetToken,SetToken来获取以及校验token
//登录操作:获取token
func (con V1Controller) Login(c *gin.Context) {
    tokenStr, err := models.SetToken(11)
    if err != nil {
        c.JSON(200, gin.H{
            "message": "生成token失败重试",
            "success": false,
        })
        return
    }
    c.JSON(200, gin.H{
        "message": "获取token成功",
        "token":   tokenStr,
        "success": true,
    })
}

//获取收货地址
func (con V1Controller) AddressList(c *gin.Context) {
    //获取token
    tokenData := c.Request.Header.Get("Authorization")
    if len(tokenData) <= 0 {
        c.JSON(http.StatusOK, gin.H{
            "message": "token传入错误长度不合法",
            "success": false,
        })
    }

    uid, err := models.GetToken(tokenData, 11)
    if err != nil {  //校验失败
        c.JSON(http.StatusOK, gin.H{
            "message": err,
            "success": false,
        })
    }

    //校验成功
    c.JSON(http.StatusOK, gin.H{
        "uid": uid,
        "success": true,
    })
}

(3).PostMan校验

  1. Vue React Angular 使用 Axios 访问基于 Jwt 的接口

var token = localStorage.getItem('token');
this.$http.get("http://localhost:8080/api/addressList", {
    headers: {
    'Authorization': 'Bearer ' + token, }
    }).then(function (response) {
        console.log(response);
}).catch(function (error) {
    console.log(error);
})
  1. 关于 Jwt 的一些问题

  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询

数据库的次数。

  • JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某

个 token,或者更改 token 的权限,也就是说,一旦 JWT 签发了,在到期之前就会始终有

效,除非服务器部署额外的逻辑。

  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限,为了减

少盗用,JWT 的有效期应该设置得比较短,对于一些比较重要的权限,使用时应该再次对

用户进行认证

  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输

[上一节][golang gin框架] 35.Gin 商城项目- 用户中心制作以及订单列表数据渲染(分页展示,订单状态,筛选订单 搜索订单,订单详情, 以及后台订单管理功能实现逻辑 )

[下一节][golang gin框架] 37.ElasticSearch 全文搜索引擎的使用

;