Wangjili
文章58
标签12
分类9
Go开发

Go开发

此文记录在Golang开发遇到的问题或注意事项

一、dchest/captcha使用

dchest/captchaGolang中的一个用于生成验证码图片的库。

1.1、生成验证码图片/tool/captcha.go

package tool

import (
	"bytes"
	"fmt"
	"net/http"
	"time"

	"github.com/dchest/captcha"
	"github.com/gin-contrib/sessions"
	"github.com/gin-gonic/gin"
)


// 生成图片
func Captcha(c *gin.Context, length ...int) {
	l := captcha.DefaultLen
	w, h := 107, 36
	if len(length) == 1 {
		l = length[0]
	}
	if len(length) == 2 {
		w = length[1]
	}
	if len(length) == 3 {
		h = length[2]
	}
	captchaId := captcha.NewLen(l)
	session := sessions.Default(c)
	session.Set("captcha", captchaId)
	_ = session.Save()
	_ = Serve(c.Writer, c.Request, captchaId, ".png", "zh", false, w, h)
}

// 验证图片
func CaptchaVerify(c *gin.Context, code string) bool {
	session := sessions.Default(c)
	if captchaId := session.Get("captcha"); captchaId != nil {
		session.Delete("captcha")
		_ = session.Save()
		fmt.Println(captchaId, code)
		if captcha.VerifyString(captchaId.(string), code) {
			return true
		} else {
			return false
		}
	} else {
		return false
	}
}
func Serve(w http.ResponseWriter, r *http.Request, id, ext, lang string, download bool, width, height int) error {
	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
	w.Header().Set("Pragma", "no-cache")
	w.Header().Set("Expires", "0")

	var content bytes.Buffer
	fmt.Println(id)
	switch ext {
	case ".png":
		w.Header().Set("Content-Type", "image/png")
		_ = captcha.WriteImage(&content, id, width, height)
	case ".wav":
		w.Header().Set("Content-Type", "audio/x-wav")
		_ = captcha.WriteAudio(&content, id, lang)
	default:
		return captcha.ErrNotFound
	}

	if download {
		w.Header().Set("Content-Type", "application/octet-stream")
	}
	http.ServeContent(w, r, id+ext, time.Time{}, bytes.NewReader(content.Bytes()))
	return nil
}

1.2、前端使用图片的方法

需要在请求中使用如下的方法将响应数据转为图片

export function getImg(url) {
    return new Promise((resolve, reject) => {
        axios({
            
            method: "GET",
            url: url,  // 服务端生成图片的地址 /user/captcha
            baseURL: httpUrl,  // 服务端根地址 http://127.0.0.1:8080
            // headers: {
            //     "Content-Type":"image/png"
            // }
            responseType:"arraybuffer"  // buffer格式
        })
            .then((res) => {
            // 下面这一步是必须的,通过将服务端返回的数据转为图片格式
            const data= "data:image/jpeg;base64," +
                btoa(
                    new Uint8Array(res.data).reduce(
                        (data, byte) => data + String.fromCharCode(byte),
                        ""
                    )
                );

                    resolve(data) 
            })
            .catch((err) => {
                reject(err);
            });
    });
}

最后在html页面中使用如下代码嵌入进去,下面的data就是上面请求的resolve(data)

<img src={data} />

二、跨域传递cookie

首先cookie默认只能在同域中传递,也就是前后端只有在同一个端口下,cookie才能互相传递。(当前后端部署到线上时,需要它们的根域名相同)。
前后端分离的开发中,由于开发的端口号不同(服务端:8080客户端:3000),这个时候需要前后端同时做一些配置

服务端配置

  1. 配置运行跨域,gin举例:/routers/routers.go

    package routers
    
    import (
    	"xao_oauth/middlewares"
    	"xao_oauth/settings"
    
    	"github.com/gin-contrib/cors"
    	"github.com/gin-gonic/gin"
    )
    
    func InitRoute() (r *gin.Engine) {
    	r = gin.Default()
    
    	config := cors.DefaultConfig()
    	config.AllowCredentials = true  // 允许跨域请求的时候携带cookie
    	config.AllowOrigins = []string{"http://127.0.0.1:3000"}  // 设置了 AllowCredentials 的时候,这一项需要写具体的客户端地址,多个用逗号分开。
    	r.Use(cors.New(config))
    	// r.NoRoute(middlewares.HandleNotFound)
    	// r.NoMethod(middlewares.HandleNotFound)
    	// r.Use(middlewares.ErrHandler(), middlewares.Session(settings.Conf.SessionConfig))
    	// UserRouter(r)
    	return
    }
  2. 配置session, /middlewares/session.go

    package middlewares
    
    import (
    	"xao_oauth/settings"
    
    	"github.com/gin-contrib/sessions"
    	"github.com/gin-contrib/sessions/cookie"
    	"github.com/gin-gonic/gin"
    )
    
    // 中间件 处理session
    
    func Session(cfg *settings.SessionConfig) gin.HandlerFunc {
    	store := cookie.NewStore([]byte(cfg.Secret))
    	store.Options(sessions.Options{
    		MaxAge: cfg.MaxAge,
    		Path:   cfg.Path,
    		// SameSite: http.SameSiteDefaultMode,
    		HttpOnly: true,
    		Domain:   "http://127.0.0.1",
    		// Secure:   false,
    	})
    
    	return sessions.Sessions(cfg.Key, store)
    }

    在具体创建cookie-key的代码中,在每次创建完,需要调用相对应的session.Save()才会成功创建。例子如下中的注释那一行 /tool/captcha.go这里仅举例,根据自己的业务需求编写,这个例子是我实现验证码图片的代码。

    package tool
    
    import (
    	"bytes"
    	"fmt"
    	"net/http"
    	"time"
    
    	"github.com/dchest/captcha"
    	"github.com/gin-contrib/sessions"
    	"github.com/gin-gonic/gin"
    )
    
    func Captcha(c *gin.Context, length ...int) {
    	l := captcha.DefaultLen
    	w, h := 107, 36
    	if len(length) == 1 {
    		l = length[0]
    	}
    	if len(length) == 2 {
    		w = length[1]
    	}
    	if len(length) == 3 {
    		h = length[2]
    	}
    	captchaId := captcha.NewLen(l)
    	session := sessions.Default(c)
    	session.Set("captcha", captchaId)
    	_ = session.Save()  // 只有调用了此函数,才会成功创建captcha这个cookie
    	_ = Serve(c.Writer, c.Request, captchaId, ".png", "zh", false, w, h)
    }
    func CaptchaVerify(c *gin.Context, code string) bool {
    	session := sessions.Default(c)
    	if captchaId := session.Get("captcha"); captchaId != nil {
    		session.Delete("captcha")
    		_ = session.Save()
    		fmt.Println(captchaId, code)
    		if captcha.VerifyString(captchaId.(string), code) {
    			return true
    		} else {
    			return false
    		}
    	} else {
    		return false
    	}
    }
    func Serve(w http.ResponseWriter, r *http.Request, id, ext, lang string, download bool, width, height int) error {
    	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
    	w.Header().Set("Pragma", "no-cache")
    	w.Header().Set("Expires", "0")
    
    	var content bytes.Buffer
    	fmt.Println(id)
    	switch ext {
    	case ".png":
    		w.Header().Set("Content-Type", "image/png")
    		_ = captcha.WriteImage(&content, id, width, height)
    	case ".wav":
    		w.Header().Set("Content-Type", "audio/x-wav")
    		_ = captcha.WriteAudio(&content, id, lang)
    	default:
    		return captcha.ErrNotFound
    	}
    
    	if download {
    		w.Header().Set("Content-Type", "application/octet-stream")
    	}
    	http.ServeContent(w, r, id+ext, time.Time{}, bytes.NewReader(content.Bytes()))
    	return nil
    }

客户端配置

使用axios请求数据时,全家配置如下代码即可。

axios.defaults.withCredentials = true;

注意事项

注意localhost127.0.0.1是不同的域,如果用localhost:3000去请求127.0.0.1:3000是跨域的。这个坑我踩了多次了。
当服务端是用127.0.0.1启动时,并且跨域的AllowOrigin配置为127.0.0.1时,在这种情况下,如果客户端用localhost去请求127.0.0.1的数据,这个时候数据会请求成功,但是服务端返回的cookie是不会保存在客户端的。切记!!!

三、Gin使用注意事项

这一章记录关于Golang服务端开发框架Gin相关的注意事项。

1.中间件使用

如果要在中间件中断执行,需要使用ctx.Abort()相关的内置函数,使用return语句只会中止当前中间件继续执行,不会影响后续的中间件。

2.数据校验

gin中,通常需要对前端传过来的数据进行校验,一般通过结构体struct绑定binding实现。

// 下面结构体中的 msg 就是校验数据不通过时,返回给前端的错误信息
type UserInfo struct{
	Name string `json:"name" binding:"required" msg:"用户名校验失败"`
}

下面演示在实际代码中,如果校验数据出错,如何返回给前端。

func (c *gin.Context){
	var user UserInfo
	err := c.ShouldBindJSON(&user)
	if err != nil {
		getObj := reflect.TypeOf(&user)
		// 将err接口断言为具体类型
		if errs,ok := err.(validator.ValidationErrors);ok{
			// 断言成功
			for _,e := range errs{
				// 循环每一个错误信息
				// 根据报错字段名,获取结构体的具体字段
				if f,ok1:= getObj.Elem().FieldByName(e.Field());ok1{
					msg:=f.Tag.Get("msg")
					fmt.Println(msg)  // 此处可以拿到对应的msg
				}
			}
		}
	}
}

// 将上述错误处理封装为一个函数
// 获取结构体中的msg参数
func GetValidMsg(err error,obj any) string{
	getObj := reflect.TypeOf(obj)
	// 将err接口断言为具体类型
	if errs,ok := err.(validator.ValidationErrors);ok{
		// 断言成功
		for _,e := range errs{
			// 循环每一个错误信息
			// 根据报错字段名,获取结构体的具体字段
			if f,ok1:= getObj.Elem().FieldByName(e.Field());ok1{
				msg:=f.Tag.Get("msg")					
				return msg  // fmt.Println(msg)  // 此处可以拿到对应的msg
			}
		}
	}
	return ""
}

3.自定义校验

在实际应用开发中,往往gin自带的数据校验规则不能满足我们的需求,这个时候可以自定义校验规则。示例如下

type User struct{
	Name string `json:"name" binding:"required,sign" msg:"用户名校验失败"`  // sign 为自定义校验属性名
}

func signValid(fl validator.FieldLevel) bool{
	var nameList []string = []string{"张三","里斯","王五"}
	for _,nameStr :+ range nameList{
		name := fl.Field().Interface().(string)
		if name == nameStr{
			return false
		}
	}
	return true
}


func main(){
	if v,ok := binding.Validator.Engine().(*validator.Validate);ok{
		v.RegisterValidator("sign",signValid)
	}
}
本文作者:Wangjili
本文链接:https://blog.wangjili.cn/2023/01/10/goserver/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可