图片上传
- 大小限制
- 白名单机制
- 文件名重复判断
package image_api
import (
"fast_gin/global"
"fast_gin/utils/find"
"fast_gin/utils/md5"
"fast_gin/utils/random"
"fast_gin/utils/res"
"fmt"
"github.com/gin-gonic/gin"
"os"
"path"
"path/filepath"
"strings"
)
var whiteList = []string{
".jpg",
".jpeg",
".png",
".webp",
}
func (ImageApi) UploadView(c *gin.Context) {
fileHeader, err := c.FormFile("file")
if err != nil {
res.FailWithMsg("请选择文件", c)
return
}
// 大小限制
if fileHeader.Size > global.Config.Upload.Size*1024*1024 {
res.FailWithMsg("上传文件过大", c)
return
}
// 后缀判断
ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
if !find.InList(whiteList, ext) {
res.FailWithMsg("上传文件后缀非法", c)
return
}
// 处理文件名重复
// uploads/images/xx.jpg
// uploads/images/xx_7hf.jpg
fp := path.Join("uploads", global.Config.Upload.Dir, fileHeader.Filename)
for {
_, err1 := os.Stat(fp)
if os.IsNotExist(err1) {
break
}
// 文件存在
// 算上传的图片和本身的图片是不是一样的,如果是一样的,那就直接返回之前的地址
uploadFile, _ := fileHeader.Open()
oldFile, _ := os.Open(fp)
uploadFileHash := md5.MD5WithFile(uploadFile)
oldFileHash := md5.MD5WithFile(oldFile)
if uploadFileHash == oldFileHash {
// 上传的图片,名称和内容都是一样的
res.Ok("/"+fp, "上传成功", c)
return
}
// 上传的图片,名称是一样的,但是内容不一样
fileNameNotExt := strings.TrimSuffix(fileHeader.Filename, ext)
newFileName := fmt.Sprintf("%s_%s%s", fileNameNotExt, random.RandStr(3), ext)
fp = path.Join("uploads", global.Config.Upload.Dir, newFileName)
}
c.SaveUploadedFile(fileHeader, fp)
res.Ok("/"+fp, "上传成功", c)
}
生成图片验证码
可以使用这个第三方库
https://github.com/mojocn/base64Captcha?tab=readme-ov-file
重点讲一下图片验证码后续的验证流程
生成图片验证码的时候,返回一串随机字符串和图片本身的base64编码
后端在内存中存储随机字符串对应正确的图片验证码
前端把图片渲染出来,需要验证的时候,就把用户输入的图片验证码和那一串随机字符串传给后端
验证的时候,根据随机字符串去匹配图片验证码是否正确
package captcha_api
import (
"fast_gin/utils/captcha"
"fast_gin/utils/res"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"github.com/sirupsen/logrus"
)
type GenerateResponse struct {
CaptchaID string `json:"captchaID"`
Captcha string `json:"captcha"`
}
func (CaptchaApi) GenerateView(c *gin.Context) {
var driver = base64Captcha.DriverString{
Width: 200,
Height: 60,
NoiseCount: 2,
ShowLineOptions: 4,
Length: 4,
Source: "0123456789",
}
cp := base64Captcha.NewCaptcha(&driver, captcha.CaptchaStore)
id, b64s, _, err := cp.Generate()
if err != nil {
logrus.Errorf("图片验证码生成失败 %s", err)
res.FailWithMsg("图片验证码生成失败", c)
return
}
res.OkWithData(GenerateResponse{
CaptchaID: id,
Captcha: b64s,
}, c)
}
apifox显示base64的图片
//pm是apifox的内置对象,通过这个对象,可以获取到请求结果(response)
//通过.json()函数,获取到响应体中返回的json数据
let res = pm.response.json()
//定义一个模板,这个模板存的是
const template = `<html><img src="{{imgTemplate}}" /></html>`;
//构建img标签能识别的base64 url,注:如果接口返回的base64 url没有【data:image/png;base64,】则需要拼接进去,否则出不来图片。
//因接口返回的是一个数组,这里打印打一张图片
let img= res.data.captcha;
//console.log(img) //打印
// 设置 visualizer 数据。
//template:模板,上面const定义的template。
// {imgTemplate: img},imgTemplate对应的是template中src的值;img指的是上述定义的img base64 url
pm.visualizer.set(template, {
imgTemplate: img
})
登录,登出
校验用户名密码,然后颁发token
package user_api
import (
"fast_gin/global"
"fast_gin/middleware"
"fast_gin/models"
"fast_gin/utils/captcha"
"fast_gin/utils/jwts"
"fast_gin/utils/pwd"
"fast_gin/utils/res"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
type LoginRequest struct {
Username string `json:"username" binding:"required" label:"用户名"`
Password string `json:"password" binding:"required" label:"密码"`
CaptchaID string `json:"captchaID"`
CaptchaCode string `json:"captchaCode"`
}
func (UserApi) LoginView(c *gin.Context) {
cr := middleware.GetBind[LoginRequest](c)
if global.Config.Site.Login.Captcha {
if cr.CaptchaID == "" || cr.CaptchaCode == "" {
res.FailWithMsg("请输入图片验证码", c)
return
}
if !captcha.CaptchaStore.Verify(cr.CaptchaID, cr.CaptchaCode, true) {
res.FailWithMsg("图片验证码验证失败", c)
return
}
}
var user models.UserModel
err := global.DB.Take(&user, "username = ?", cr.Username).Error
if err != nil {
res.FailWithMsg("用户名或密码错误", c)
return
}
if !pwd.CompareHashAndPassword(user.Password, cr.Password) {
res.FailWithMsg("用户名或密码错误", c)
return
}
token, err := jwts.SetToken(jwts.Claims{
UserID: user.ID,
RoleID: user.RoleID,
})
if err != nil {
logrus.Errorf("生成token失败 %s", err)
res.FailWithMsg("登录失败", c)
return
}
res.OkWithData(token, c)
return
}
注销
将token放入redis黑名单,过期时间为token的失效时间
package redis_ser
import (
"context"
"fast_gin/global"
"fast_gin/utils/jwts"
"fmt"
"github.com/sirupsen/logrus"
"time"
)
func Logout(token string) {
claims, err := jwts.CheckToken(token)
if err != nil {
return
}
key := fmt.Sprintf("logout_%s", token)
sub := claims.ExpiresAt.Sub(time.Now())
_, err = global.Redis.Set(context.Background(), key, "", sub).Result()
if err != nil {
logrus.Error(err)
}
}
func HasLogout(token string) (ok bool) {
key := fmt.Sprintf("logout_%s", token)
_, err := global.Redis.Get(context.Background(), key).Result()
if err == nil {
return true
}
return false
}
用户列表
- 分页查询
- 模糊匹配
- 关键字查询
package user_api
import (
"fast_gin/global"
"fast_gin/middleware"
"fast_gin/models"
"fast_gin/utils/res"
"github.com/gin-gonic/gin"
)
func (UserApi) UserListView(c *gin.Context) {
var cr = middleware.GetBind[models.PageInfo](c)
var list = make([]models.UserModel, 0)
query := global.DB.Where("")
if cr.Key != "" {
query.Where("username like ?", "%"+cr.Key+"%")
}
offset := (cr.Page - 1) * cr.Limit
global.DB.Where(query).Limit(cr.Limit).Offset(offset).Order(cr.Order).Find(&list)
var count int64
global.DB.Where(query).Count(&count)
res.OkWithList(list, count, c)
}
通用列表查询
package common
import (
"fast_gin/global"
"fast_gin/models"
"fmt"
"gorm.io/gorm"
)
type QueryOption struct {
models.PageInfo
Likes []string
Where *gorm.DB
Preloads []string
Debug bool
}
func QueryList[T any](model T, option QueryOption) (list []T, count int64, err error) {
list = make([]T, 0)
// 自己身上的
query := global.DB.Where(model)
// 模糊匹配
if option.Key != "" {
if len(option.Likes) != 0 {
likeQuery := global.DB.Where("")
for _, column := range option.Likes {
likeQuery.Or(fmt.Sprintf("%s like ?", column), fmt.Sprintf("%%%s%%", option.Key))
}
query.Where(likeQuery)
}
}
// 预加载
for _, preload := range option.Preloads {
query = query.Preload(preload)
}
// 分页
if option.Page <= 0 {
option.Page = 1
}
if option.Limit <= 0 {
option.Limit = -1
}
offset := (option.Page - 1) * option.Limit
if option.Order == "" {
option.Order = "created_at desc"
}
db := global.DB.Where("")
if option.Debug {
db = db.Debug()
}
db.Where(query).Limit(option.Limit).Offset(offset).Order(option.Order).Find(&list)
db.Model(model).Where(query).Count(&count)
return
}
如何使用
func (UserApi) UserListView(c *gin.Context) {
var cr = middleware.GetBind[models.PageInfo](c)
list, count, _ := common.QueryList(models.UserModel{}, common.QueryOption{
PageInfo: cr,
Likes: []string{"username", "nickname"},
Debug: true,
})
res.OkWithList(list, count, c)
}