验证器与登录的基本逻辑
分页逻辑传入处理
利用 context.getDefaultQuery(key, defaultValue
)快速取出url参数中的query参数:
// 生成 grpc 的 client 并调用接口
userSrvClient := proto.NewUserClient(userConn)
// 通过上下文 gin.Context 获取请求参数
// 若能找到对应的请求参数,则返回传入的请求参数,若不存在,则返回默认值
pn := ctx.DefaultQuery("pn", "0")
pnInt, _ := strconv.Atoi(pn)
pSize := ctx.DefaultQuery("psize", "10")
pSizeInt, _ := strconv.Atoi(pSize)
rsp, err := userSrvClient.GetUserList(context.Background(), &proto.PageInfo{
Pn: uint32(pnInt),
PSize: uint32(pSizeInt),
})
之后这样访问就可以指定参数,若不指定,则默认是第一页展示10个
http://127.0.0.1:8021/u/v1/user/list?pn=0&psize=1
用户登录逻辑错误处理及验证器配置
目录:
user-web
api
user.go
forms
user.go
global
global.go
validate
validators.go (验证器的位置)
基础错误处理和简单验证器
一、 在 user.go 中建立方法,方法参数为 ctx *gin.Context
api/user.go:
// 用户登录模块
func PassWordLogin(c *gin.Context) {
}
二、在 form 目录中添加对应的结构
form/user.go
package forms
// 这里要注意 binding 内部的参数不可以加空格
type PassWordLoginForm struct {
Mobile string `form:"mobile" json:"mobile" binding:"required"`
PassWord string `form:"password" json:"password" binding:"required,min=3,max=10"`
}
三、配置错误处理
- 在全局变量中配置 Trans
global/global.go
package global
import (
ut "github.com/go-playground/universal-translator"
)
// 全局变量
var (
// 用于进行错误处理
Trans ut.Translator
)
- 在初始化中添加验证器的初始化内容
initialize/validator.go
package initialize
import (
"fmt"
"reflect"
"strings"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"mxshop-api/user-web/global"
)
// 下面这一切主要也是为了 global.Trans 的使用
func InitTrans(locale string) (err error) {
// 定制化 gin 中的 validator 引擎
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册 json tag 的自定义方案
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
zhT := zh.New()
enT := en.New()
// 第一个参数是备用语言环境,后面是支持的语言环境
uni := ut.New(enT, zhT, enT)
global.Trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}
switch locale {
case "en":
en_translations.RegisterDefaultTranslations(v, global.Trans)
case "zh":
zh_translations.RegisterDefaultTranslations(v, global.Trans)
default:
en_translations.RegisterDefaultTranslations(v, global.Trans)
}
return
}
return
}
- 在 main.go 中配置初始化验证器
package main
import (
"fmt"
"go.uber.org/zap"
"mxshop-api/user-web/global"
"mxshop-api/user-web/initialize"
)
func main() {
// 调用配置文件伛
initialize.InitConfig()
// 调用自己的 initlize 创建 Router
Router := initialize.Routers()
//port := 8021
// 配置日志级别,自定义方法
initialize.InitLogger()
// 添加::::初始化翻译。验证器工作,配置翻译为中文
initialize.InitTrans("zh")
// 打印日志
// 这里的 S() 和 L() 方法的主要用途是:在底层创建安全的日志情况,否则我们自己写的话需要自己考虑加锁的问题
zap.L().Debug(fmt.Sprintf("启动服务器,端口:%d", global.ServerConfig.Port))
if err := Router.Run(fmt.Sprintf(":%d", global.ServerConfig.Port)); err != nil {
zap.L().Panic("启动失败:", zap.Error(err))
}
}
- 核心代码逻辑编写:
api/user.go
// 在最后返回错误时调用,用来将返回中的对象名去掉
func removeTopStruct(fields map[string]string) map[string]string {
rsp := map[string]string{}
for field, err := range fields {
rsp[field[strings.Index(field, ".")+1:]] = err // 将map中的 key 中的 . 前面的信息去掉
}
return rsp
}
// 用户登录模块
func PassWordLogin(c *gin.Context) {
passwordLoginForm := forms.PassWordLoginForm{}
if err := c.ShouldBind(&passwordLoginForm); err != nil {
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
}
c.JSON(http.StatusBadRequest, gin.H{
"msg": removeTopStruct(errs.Translate(global.Trans)),
})
return
}
}
- 配置http访问路径
router/user.go
package router
import (
"github.com/gin-gonic/gin"
"mxshop-api/user-web/api"
)
func InitUserRouter(Router *gin.RouterGroup) {
// 这样就需要 /user/list 才可以进行访问了
UserRouter := Router.Group("user")
{
UserRouter.GET("list", api.GetUserList)
UserRouter.POST("pwd_login", api.PassWordLogin) // 新配置的路径
}
}
- 由于主逻辑中,错误处理信息有点长,所以这里选择使用抽取的方式缩减代码
api/user.go
func HandleValidatorError(c *gin.Context, err error) {
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
}
c.JSON(http.StatusBadRequest, gin.H{
"msg": removeTopStruct(errs.Translate(global.Trans)),
})
return
}
// 用户登录模块
func PassWordLogin(c *gin.Context) {
passwordLoginForm := forms.PassWordLoginForm{}
if err := c.ShouldBind(&passwordLoginForm); err != nil {
HandleValidatorError(c, err)
}
}
复杂验证器-手机号验证器自定义配置
在 validate 包中定义 validators.go 文件,用来存放自定义的验证器:
validators.go
package validator
import (
"regexp"
"github.com/go-playground/validator/v10"
)
func ValidateMobile(fl validator.FieldLevel) bool {
// 利用正则判断是否验证通过
mobile := fl.Field().String()
// 使用 正则 判断字符串是否合法
ok, _ := regexp.MatchString(`^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$`, mobile)
if !ok {
// ok 为false。代表正则匹配失败,后续整体返回 false 代表验证失败
return false
}
return true
}
在 main 中将创建的验证器注册到 gin 中:
import (
"github.com/go-playground/validator/v10"
myValidator "mxshop-api/user-web/validator"
)
func main() {
...
Router := initialize.Routers()
// 初始化翻译。验证器工作,配置翻译为中文
if err := initialize.InitTrans("zh"); err != nil {
panic(err)
}
// 获取验证器的对象,这里会将获取的 Engine 转换为 validator.Validate 对象,若转换成功,ok 会返回 true
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("mobile", myValidator.ValidateMobile)
}
...
}
将验证逻辑注入到对应的变量中:
在 required 后面添加 mobile 参数
forms/user.go
package forms
// 这里要注意 binding 内部的参数不可以加空格
type PassWordLoginForm struct {
Mobile string `form:"mobile" json:"mobile" binding:"required,mobile"`
PassWord string `form:"password" json:"password" binding:"required,min=3,max=10"`
}
修改 main 中的注册内容,令其可以翻译为中文:
// 获取验证器的对象,这里会将获取的 Engine 转换为 validator.Validate 对象,若转换成功,ok 会返回 true
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("mobile", myValidator.ValidateMobile)
// 错误翻译器注册:
v.RegisterTranslation("mobile", global.Trans, func(ut ut.Translator) error {
return ut.Add("mobile", "{0} must have a value", true)
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("mobile", fe.Field())
return t
})
}
登录功能的核心逻辑编写
无需其他接口配合,仅需这一个逻辑,逻辑为,该接口接收前端参数,进行拨号连接并生成NewUserClient对象,利用该对象在数据库进行比对。
api/user.go:
// 用户登录模块
func PassWordLogin(c *gin.Context) {
// 绑定请求参数
passwordLoginForm := forms.PassWordLoginForm{}
if err := c.ShouldBind(&passwordLoginForm); err != nil {
HandleValidatorError(c, err)
}
// 将请求参数转发给 rpc 服务器
//ip := "127.0.0.1"
//port := 50051
// 拨号连接用户 GRPC 服务
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", global.ServerConfig.UserSrvInfo.Host, global.ServerConfig.UserSrvInfo.Port), grpc.WithInsecure())
if err != nil {
zap.L().Error("[PassWordLogin] 连接 【用户服务失败】",
zap.String("msg", err.Error()))
}
// 生成 grpc 的 client 并调用接口
userSrvClient := proto.NewUserClient(userConn)
if rsp, err := userSrvClient.GetUserByMobile(context.Background(), &proto.MobileRequest{
Mobile: passwordLoginForm.Mobile,
}); err != nil {
if e, ok := status.FromError(err); ok {
switch e.Code() {
case codes.NotFound:
c.JSON(http.StatusBadRequest, map[string]string{
"mobile": "用户不存在",
})
default:
c.JSON(http.StatusInternalServerError, map[string]string{
"mobile": "登录失败",
})
}
return
}
} else {
// TODO 这里只能验证找到了用户,后面继续验证密码是否正确的功能
// 这里是调用的 Srv 中的服务进行登录逻辑的判断,传入的 第二个参数是需要的参数,这个参数的对应取值在之前已经取到
// 这里仅仅演示简单的登录密码验证机制,完全的登录逻辑在之后展示
if passRsp, passErr := userSrvClient.CheckPassWord(context.Background(), &proto.PassWordCheckInfo{
Password: passwordLoginForm.PassWord,
EncryptedPassword: rsp.Password,
}); passErr != nil {
c.JSON(http.StatusInternalServerError, map[string]string{
"msg": "登录失败",
})
} else {
// 这里代表未发生其他错误,继续验证密码
if passRsp.Success {
c.JSON(http.StatusOK, map[string]string{
"msg": "登录成功",
})
} else {
// 这里代表登录密码验证错误
c.JSON(http.StatusBadRequest, map[string]string{
"msg": "密码错误",
})
}
}
}