zoukankan      html  css  js  c++  java
  • 【Gin-API系列】请求和响应参数的检查绑定(二)

    参数设计

    一套合格的API的服务需要规范的输入请求和标准的输出响应格式。
    为了更规范的设计,也是为了代码的可读性和扩展性,我们需要对Http请求和响应做好模型设计。

    • 请求

    根据【Gin-API系列】需求设计和功能规划(一)请求案例的设计,
    我们在ip参数后面再增加一个参数oid来表示模型ID,只返回需要的模型model

     // 考虑到后面会有更多的 API 路由设计,本路由可以命名为 SearchIp
    
    type ReqGetParaSearchIp struct {
    	Ip  string        
    	Oid configure.Oid 
    }
    
    type ReqPostParaSearchIp struct {
    	Ip  string        
    	Oid configure.Oid 
    }
    
    const (
    	OidHost   Oid = "HOST"
    	OidSwitch Oid = "SWITCH"
    )
    
    var OidArray = []Oid{OidHost, OidSwitch}
    
    • 响应

    API的响应都需要统一格式,并维护各字段的文档解释

    type ResponseData struct {
    	Page     int64         `json:"page"`  // 分页显示,页码
    	PageSize int64         `json:"page_size"`  // 分页显示,页面大小
    	Size     int64         `json:"size"`  // 返回的元素总数
    	Total    int64         `json:"total"`  // 筛选条件计算得到的总数,但可能不会全部返回
    	List     []interface{} `json:"list"`
    }
    
    type Response struct {
    	Code    configure.Code `json:"code"`  // 响应码
    	Message string         `json:"message"` // 响应描述
    	Data    ResponseData   `json:"data"`  // 最终返回数据
    }
    
    • 请求IP合法性检查

    我们需要对search_ip接口的请求参数长度、格式、错误IP做检查。
    其中,错误IP一般指的是127.0.0.1这种回环IP,或者是网关、重复的局域网IP等

    func CheckIp(ipArr []string, low, high int) error {
    	if low > len(ipArr) || len(ipArr) > high {
    		return errors.New(fmt.Sprintf("请求IP数量超过限制"))
    	}
    	for _, ip := range ipArr {
    		if !network.MatchIpPattern(ip) {
    			return errors.New(fmt.Sprintf("错误的IP格式:%s", ip))
    		}
    		if network.ErrorIpPattern(ip) {
    			return errors.New(fmt.Sprintf("不支持的IP:%s", ip))
    		}
    	}
    	return nil
    }
    

    utils.network 文件

    // IP地址格式匹配  "010.99.32.88" 属于正常IP
    func MatchIpPattern(ip string) bool {
    	//pattern := `^((2[0-4]d|25[0-5]|[01]?dd?).){3}(2[0-4]d|25[0-5]|[01]?dd?)$`
    	//reg := regexp.MustCompile(pattern)
    	//return reg.MatchString(ip)
    	if net.ParseIP(ip) == nil {
    		return false
    	}
    	return true
    }
    
    // 排查错误的IP
    func ErrorIpPattern(ip string) bool {
    	errorIpMapper := map[string]bool{
    		"192.168.122.1": true,
    		"192.168.250.1": true,
    		"192.168.255.1": true,
    		"192.168.99.1":  true,
    		"192.168.56.1":  true,
    		"10.10.10.1":    true,
    	}
    	errorIpPrefixPattern := []string{"127.0.0.", "169.254.", "11.1.", "10.176."}
    	errorIpSuffixPattern := []string{".0.1"}
    	if _, ok := errorIpMapper[ip]; ok {
    		return true
    	}
    	for _, p := range errorIpPrefixPattern {
    		if strings.HasPrefix(ip, p) {
    			return true
    		}
    	}
    	for _, p := range errorIpSuffixPattern {
    		if strings.HasSuffix(ip, p) {
    			return true
    		}
    	}
    	return false
    }
    
    • 代码实现

    为了通用性设计,我们将main函数的func(c *gin.Context)独立定义成一个函数SearchIpHandlerWithGet
    考虑到API的扩展和兼容,我们将对API的实现区分版本,在route文件夹中新建v1文件夹作为第一版本代码实现。
    同时,我们将search_ip归类为sdk集合,存放于v1

    var SearchIpHandlerWithGet = func(c *gin.Context) {
    	ipStr := c.DefaultQuery("ip", "")
    	response := route_response.Response{
    		Code:configure.RequestSuccess,
    		Data: route_response.ResponseData{List: []interface{}{}},
    	}
    	if ipStr == "" {
    		response.Code, response.Message = configure.RequestParameterMiss, "缺少请求参数ip"
    		c.JSON(http.StatusOK, response)
    		return
    	}
    	ipArr := strings.Split(ipStr, ",")
    	if err := route_request.CheckIp(ipArr, 1, 10); err != nil {
    		response.Code, response.Message = configure.RequestParameterRangeError, err.Error()
    		c.JSON(http.StatusOK, response)
    		return
    	}
    	hostInfo := map[string]interface{}{
    		"10.1.162.18": map[string]string{
    			"model": "主机", "IP": "10.1.162.18",
    		},
    	}
    	response.Data = route_response.ResponseData{
    		Page:     1,
    		PageSize: 1,
    		Size:     1,
    		Total:    1,
    		List:     []interface{}{hostInfo, },
    	}
    	c.JSON(http.StatusOK, response)
    	return
    }
    
    • 结果验证
    D:> curl http://127.0.0.1:8080?ip=''
    {"code":4002,"message":"错误的IP格式:''","data":{"page":0,"page_size":0,"size":0,"total":0,"list":[]}}
    
    D:> curl http://127.0.0.1:8080?ip=
    {"code":4005,"message":"缺少请求参数ip","data":{"page":0,"page_size":0,"size":0,"total":0,"list":[]}}
    
    D:> curl http://127.0.0.1:8080?ip="10.1.1.1"
    {"code":0,"message":"","data":{"page":1,"page_size":1,"size":1,"total":1,"list":[{"10.1.162.18":{"IP":"10.1.162.18","model":"主机"}}]}}
    

    Gin.ShouldBind参数绑定

    • 为了使请求参数的可读性和扩展性更强,我们使用ShouldBind函数来对请求进行参数绑定和校验

    ShouldBind 支持将Http请求内容绑定到Gin Struct结构体,
    目前支持JSONXMLFORM请求格式绑定(请看前面定义的ReqParaSearchIp)。

    • 使用方法
    // ipStr := c.DefaultQuery("ip", "")
     
    var req route_request.ReqParaSearchIp
    if err := c.ShouldBindQuery(&req); err != nil {
        response.Code, response.Message = configure.RequestParameterTypeError, err.Error()
        c.JSON(http.StatusOK, response)
        return
    }
    ipStr := req.Ip
    if ipStr == "" {
        response.Code, response.Message = configure.RequestParameterMiss, "缺少请求参数ip"
        c.JSON(http.StatusOK, response)
        return
    }
    
    • 注意事项

    GET请求的struct使用form解析
    POST请求的使用JSON解析,Content-Type使用application/json

    type ReqParaSearchIp struct {
        Ip string         `form:"ip"`
        Oid configure.Oid `form:"oid"`
    }
    
    type ReqPostParaSearchIp struct {
        Ip string         `json:"ip"`
        Oid configure.Oid `json:"oid"`
    }
    
    • 自定义校验器

    使用了ShouldBind之后我们就可以使用第三方校验器来协助校验参数了。
    还记得我们前面的参数校验吗,逻辑很简单,代码却很繁琐。
    接下来,我们将使用validator.v10来做自定义校验器。

    先完成validator.v10的初始化绑定

    // DefaultValidator 验证器
    type DefaultValidator struct {
    	once     sync.Once
    	validate *validator.Validate
    }
    
    var _ binding.StructValidator = &DefaultValidator{}
    
    // ValidateStruct 如果接收到的类型是一个结构体或指向结构体的指针,则执行验证。
    func (v *DefaultValidator) ValidateStruct(obj interface{}) error {
    	if kindOfData(obj) == reflect.Struct {
    		v.lazyinit()
    		if err := v.validate.Struct(obj); err != nil {
    			return err
    		}
    	}
    	return nil
    }
    
    // Engine 返回支持`StructValidator`实现的底层验证引擎
    func (v *DefaultValidator) Engine() interface{} {
    	v.lazyinit()
    	return v.validate
    }
    
    func (v *DefaultValidator) lazyinit() {
    	v.once.Do(func() {
    		v.validate = validator.New()
    		v.validate.SetTagName("validate")
    
    		//自定义验证器 初始化
    		for valName, valFun := range validatorMapper {
    			if err := v.validate.RegisterValidation(valName, valFun); err != nil {
    				fmt.Println(err)
    				os.Exit(1)
    			}
    		}
    	})
    }
    
    func kindOfData(data interface{}) reflect.Kind {
    	value := reflect.ValueOf(data)
    	valueType := value.Kind()
    
    	if valueType == reflect.Ptr {
    		valueType = value.Elem().Kind()
    	}
    	return valueType
    }
    
    func InitValidator() {
    	binding.Validator = new(DefaultValidator)
    }
    

    自定义参数验证器

    // 自定义参数验证器名称
    const (
    	ValNameCheckOid string = "check_oid"
    )
    
    // 自定义参数验证器字典
    var validatorMapper = map[string]func(field validator.FieldLevel) bool{
    	ValNameCheckOid: CheckOid,
    }
    
    // 自定义参数验证器函数
    func CheckOid(field validator.FieldLevel) bool {
    	oid := configure.Oid(field.Field().String())
    	for _, id := range configure.OidArray {
    		if oid == id {
    			return true
    		}
    	}
    	return false
    }
    

    使用参数验证器

    type ReqGetParaSearchIp struct {
    	Ip  string        `form:"ip" validate:"required"`
    	Oid configure.Oid `form:"oid" validate:"required,check_oid"`
    }
    
    type ReqPostParaSearchIp struct {
    	Ip  string        `json:"ip" validate:"required"`
    	Oid configure.Oid `json:"oid" validate:"required,check_oid"`
    }
    

    ShouldBind绑定

    var SearchIpHandlerWithGet = func(c *gin.Context) {
    	response := route_response.Response{
    		Code:configure.RequestSuccess,
    		Data: route_response.ResponseData{List: []interface{}{}},
    	}
    	var params route_request.ReqGetParaSearchIp
    	if err := c.ShouldBindQuery(&params); err != nil {
    		code, msg := params.ParseError(err)
    		response.Code, response.Message = code, msg
    		c.JSON(http.StatusOK, response)
    		return
    	}
    	ipArr := strings.Split(params.Ip, ",")
    	if err := route_request.CheckIp(ipArr, 1, 10); err != nil {
    		response.Code, response.Message = configure.RequestParameterRangeError, err.Error()
    		c.JSON(http.StatusOK, response)
    		return
    	}
    	hostInfo := map[string]interface{}{
    		"10.1.162.18": map[string]string{
    			"model": "主机", "IP": "10.1.162.18",
    		},
    	}
    	response.Data = route_response.ResponseData{
    		Page:     1,
    		PageSize: 1,
    		Size:     1,
    		Total:    1,
    		List:     []interface{}{hostInfo, },
    	}
    	c.JSON(http.StatusOK, response)
    	return
    }
    
    

    main函数初始化

    func main() {
    	route := gin.Default()
    	route_request.InitValidator()
    	route.GET("/", v1_sdk.SearchIpHandlerWithGet)
    	if err := route.Run("127.0.0.1:8080"); err != nil {
    		fmt.Println(err)
    		os.Exit(1)
    	}
    }
    
    • 实现效果展示
    D:> curl "http://127.0.0.1:8080?ip=10.1.1.1&oid=HOST"
    {"code":0,"message":"","data":{"page":1,"page_size":1,"size":1,"total":1,"list":[{"10.1.162.18":{"IP":"10.1.162.18","model":"主机"}}]}}
    
    D:> curl "http://127.0.0.1:8080?ip=10.1.1.1&oid=SWITCH"
    {"code":0,"message":"","data":{"page":1,"page_size":1,"size":1,"total":1,"list":[{"10.1.162.18":{"IP":"10.1.162.18","model":"主机"}}]}}
    
    D:> curl "http://127.0.0.1:8080?ip=10.1.1.1&oid=XX"
    {"code":4002,"message":"请求参数 Oid 需要传入 object id","data":{"page":0,"page_size":0,"size":0,"total":0,"list":[]}}
    
    D:> curl "http://127.0.0.1:8080?ip=10.1.1&oid=HOST"
    {"code":4002,"message":"错误的IP格式:10.1.1","data":{"page":0,"page_size":0,"size":0,"total":0,"list":[]}}
    
    

    Github 代码

    请访问 Gin-IPs 或者搜索 Gin-IPs

  • 相关阅读:
    Redis 之 数据持久化、主从复制、哨兵、集群
    Linux 之 MySQL(mariadb) 主从复制
    python 面试题
    Linux 之 nginx相关
    Linux 之redis 的安装及使用
    Linux 之 安装虚拟环境virtualenvwrapper
    Vue使用Element-ui走马灯功能动态改变图片和容器大小
    文本信息抽取的方法
    python3 将文本用utf-8编码方式写入txt文件
    一个比celery更简单的python异步模块rq
  • 原文地址:https://www.cnblogs.com/lxmhhy/p/13385482.html
Copyright © 2011-2022 走看看