一.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 中配置服务器端允许跨域),下面上案例说明:
路由
配置 路由时,可以项目迭代,存在多个版本的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)
}
}
控制器代码
各个不同的功能模块可以在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接口权限验证
关于接口的安全验证
关于接口安全验证的解决方案有很多:
可以用 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 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)
}
}
具体使用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校验
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 商城项目- 用户中心制作以及订单列表数据渲染(分页展示,订单状态,筛选订单 搜索订单,订单详情, 以及后台订单管理功能实现逻辑 )