一.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 mainimport ("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 中配置服务器端允许跨域),下面上案例说明:
路由
配置 路由时,可以项目迭代,存在多个版本的api,格式如下:
package routersimport ("goshop/controllers/api""github.com/gin-gonic/gin"
)//设置api路由
func ApiRoutersInit(r *gin.Engine) {//多版本apiapiRouters := 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)}
}
控制器代码
各个不同的功能模块可以在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 apiimport ("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 UserInfob, _ := 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 Articleb, _ := 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接口权限验证
关于接口的安全验证
关于接口安全验证的解决方案有很多:
可以用 Session 来实现安全验证
对请求接口的参数进行签名,来实现接口的签名验证
使用 JWT 实现接口的验证
...
基于 Session 的安全验证
Session 存储在服务器,用户用户比较少的话是一种简单的安全验证机制,但是涉及到跨域的话需要进行一些配置,用户量非常非常大的话会耗费一定的服务器资源,关于 cookie 和 session 跨域可以参考: 解决vue请求gin框架接口cros跨域cookie和session失效的问题
对请求参数进行加密的签名验证
涉及公钥、私钥、签名等,比如支付相关功能接口
JWT
JWT 全称 JSON Web Token,是目前比较流行的另一种跨域身份验证解决方案。也是被很多人用坏的一种安全验证机制
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 intjwt.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)
}
}
具体使用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 modelsimport ("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 签名并获得完整的编码后的字符串 tokentokenStr, err := tokenObj.SignedString(jwtKey)if err != nil {return "", err}return tokenStr, nil
}func GetToken(tokenData string, uid int) (int, error) {//获取tokentokenStr := strings.Split(tokenData, " ")[1]//校验tokentoken, 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) {//获取tokentokenData := 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校验


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);
})
关于 Jwt 的一些问题
JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询
数据库的次数。
JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某
个 token,或者更改 token 的权限,也就是说,一旦 JWT 签发了,在到期之前就会始终有
效,除非服务器部署额外的逻辑。
JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限,为了减
少盗用,JWT 的有效期应该设置得比较短,对于一些比较重要的权限,使用时应该再次对
用户进行认证
为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输
[上一节][golang gin框架] 35.Gin 商城项目- 用户中心制作以及订单列表数据渲染(分页展示,订单状态,筛选订单 搜索订单,订单详情, 以及后台订单管理功能实现逻辑 )