此文记录在Golang
开发遇到的问题或注意事项
一、dchest/captcha
使用
dchest/captcha
是Golang
中的一个用于生成验证码图片的库。
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
),这个时候需要前后端同时做一些配置。
服务端配置
配置运行跨域,
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 }
配置
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;
注意事项
注意:localhost
和127.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)
}
}