【三】golang实战之用户api

目录结构

根据用户服务目录结构搭建user-api目录结构

│  .gitignore
│  go.mod
│  LICENSE
│  README.md
├─cmd
│  └─user-api
│          main.go
│
├─configs
├─global
├─initialize
├─internal
│  ├─api
│  ├─handler
│  ├─pkg
│  └─service
└─router

go.mod文件如下

module imooc/user-web

go 1.16

replace imooc/mxshop-api => ../mxshop-api

快速配置

该部分内容和user-service基本一致,所以可以直接将user-service部分的复制过来。需要复制initialize/init.goglobal/global.gointernal/models/config.go文件。然后先执行下

go mod tidy

同步一下包

删除数据库部分的配置,在user-api部分用不到这些内容。调整后,config.go文件内容如下

package models

type LogConfig struct {
	LogPath    string `mapstructure:"logPath"`
	MaxSize    int    `mapstructure:"maxSize"`
	MaxBackups int    `mapstructure:"maxBackups"`
	MaxAge     int    `mapstructure:"maxAge"`
	Level      string `mapstructure:"level"`
}

type ServiceConfig struct {
	Host string `mapstructure:"host"`
	Port int    `mapstructure:"port"`
}

global.go文件内容如下

package global

import (
	"imooc/user-api/internal/models"
)

var (
	LogConfig     *models.LogConfig
	ServiceConfig *models.ServiceConfig
)

func init() {
	LogConfig = &models.LogConfig{}
	ServiceConfig = &models.ServiceConfig{}
}

initialize.go文件内容如下

package initialize

import (
	"fmt"
	"github.com/spf13/viper"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
	"imooc/user-api/global"
	"os"
)

func InitConfig() {
	vp := viper.New()
	vp.AddConfigPath("configs/")
	vp.SetConfigName("config")
	vp.SetConfigType("yml")
	err := vp.ReadInConfig()
	if err != nil {
		panic(fmt.Sprintf("Read config failed:%v", err.Error()))
	}
	err = vp.UnmarshalKey("log", &global.LogConfig)
	if err != nil {
		panic(fmt.Sprintf("Read log config failed:%v", err))
	}
	err = vp.UnmarshalKey("service", &global.ServiceConfig)
	if err != nil {
		panic(fmt.Sprintf("Read service config failed:%v", err))
	}
}

func GetLevel(lvl string) zapcore.Level {
	switch lvl {
	case "debug", "DEBUG":
		return zapcore.DebugLevel
	case "info", "INFO", "": // make the zero value useful
		return zapcore.InfoLevel
	case "warn", "WARN":
		return zapcore.WarnLevel
	case "error", "ERROR":
		return zapcore.ErrorLevel
	case "dpanic", "DPANIC":
		return zapcore.DPanicLevel
	case "panic", "PANIC":
		return zapcore.PanicLevel
	case "fatal", "FATAL":
		return zapcore.FatalLevel
	default:
		return zapcore.InfoLevel
	}
}

func InitLogger() {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder

	lumberJackLogger := &lumberjack.Logger{
		Filename:   global.LogConfig.LogPath,
		MaxSize:    global.LogConfig.MaxSize,
		MaxBackups: global.LogConfig.MaxBackups,
		MaxAge:     global.LogConfig.MaxAge,
		Compress:   false,
	}
	core := zapcore.NewCore(
		zapcore.NewConsoleEncoder(encoderConfig),
		zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(lumberJackLogger)),
		GetLevel(global.LogConfig.Level),
	)
	zap.ReplaceGlobals(zap.New(core, zap.AddCaller()))
}

启动服务

main.go中初始化配置和日志之后,即可创建路由了。

package main

import (
	"fmt"
	"imooc/user-api/initialize"
)

func main() {
	initialize.InitConfig()

	initialize.InitLogger()

	fmt.Println("start user-api ...")

}

如果你的api服务接入多个rpc服务的话,可以在api创建路由文件时,按文件夹来进行创建。该课程中,user-api将只接入user-service服务,所以我直接创建api/user.go文件。

package api

import (
	"github.com/gin-gonic/gin"
)

func GetUserList(ctx *gin.Context) {
	
}

然后创建router/router.go文件进行路由注册功能。我没有按照boby老师的路由注册方式来,我是将全部路由都放到了一个函数中进行注册。按个人习惯吧

package router

import (
	"github.com/gin-gonic/gin"
	"imooc/user-api/internal/api"
)

func RegisterRouter() *gin.Engine {
	Router := gin.Default()
	apiGroup := Router.Group("user")

	apiGroup.GET("list", api.GetUserList)

	return Router
}

然后在主函数中进行路由注册和运行

	addr := fmt.Sprintf("%s:%d", global.ServiceConfig.Host, global.ServiceConfig.Port)
	Router := router.RegisterRouter()
	err := Router.Run(addr)
	if err != nil {
		zap.S().Errorf("User-api running failed: %v", err)
		panic(err)
	}
	zap.S().Infof("User-api running at %s ...", addr)

configs目录创建配置文件config.yml

log:
  logPath: ./imooc-user-api.log
  maxSize: 128
  maxBackups: 10
  maxAge: 30
  level: debug

service:
  host: 0.0.0.0
  port: 9000

启动日志

image-20211218163132622

用户列表

global.go中新增两个变量定义

	UserClient user.UserServiceClient
	UserService *models.ServiceConfig

初始化函数

func init() {
	LogConfig = &models.LogConfig{}
	ServiceConfig = &models.ServiceConfig{}
	UserService = &models.ServiceConfig{}
}

service/user.go中将rpc调用进行封装

package service

import (
	"context"
	user "imooc/mxshop-api/api/user/v0"
	"imooc/user-api/global"
)

func GetUserList(in *user.ListUserRequest) (out *user.ListUserResponse, err error) {
	out, err = global.UserClient.ListUser(context.Background(), in)
	return
}

models/response/response.go中定义返回结构体

package response

import "time"

type UserDetailTO struct {
	Id       int64     `json:"id"`
	NickName string    `json:"name"`
	Mobile   string    `json:"mobile"`
	Birthday time.Time `json:"birthday"`
	Gender   string    `json:"gender"`
}

编写用户列表具体实现

func HandlerGrpcErrorToHttp(err error, c *gin.Context) {
	if err != nil {
		e, ok := status.FromError(err)
		if !ok {
			c.JSON(http.StatusInternalServerError, gin.H{"msg": "未知错误"})
		}
		switch e.Code() {
		case codes.NotFound:
			c.JSON(http.StatusNotFound, gin.H{
			"msg": e.Message(),
			})
		case codes.Internal:
			c.JSON(http.StatusInternalServerError, gin.H{"msg": "内部错误"})
		case codes.InvalidArgument:
			c.JSON(http.StatusBadRequest, gin.H{"msg": "参数错误"})
		}
		return
	}
}

func GetUserList(ctx *gin.Context) {
	out, err := service.GetUserList(&user.ListUserRequest{Page: &common.PageModel{
		Page: -1, PageSize: -1,
	}})
	if err != nil {
		zap.S().Errorf("GetUserList failed: %v", err)
		HandlerGrpcErrorToHttp(err, ctx)
		return
	}
	result := make([]response.UserDetailTO, 0)
	for _, value := range out.Users {
		result = append(result, response.UserDetailTO{
			Id:       value.Id,
			NickName: value.NickName,
			Birthday: time.Time(time.Unix(int64(value.Birthday), 0)),
			Gender:   value.Gender.String(),
			Mobile:   value.Mobile,
		})
	}
	ctx.JSON(http.StatusOK, result)
	return
}

此处,我们默认传递的分页和分页大小是-1, 一般代表返回全部。

注册用户grpc

实现注册用户grpc的接口

func InitUserConnection(host string, port int) *grpc.ClientConn {
	address := fmt.Sprintf("%s:%d", host, port)
	conn, err := grpc.Dial(address, grpc.WithInsecure(),
		grpc.WithUnaryInterceptor(grpcMiddleware.ChainUnaryClient(
			grpcRetry.UnaryClientInterceptor(
				grpcRetry.WithBackoff(grpcRetry.BackoffLinear(time.Second)),
				grpcRetry.WithCodes(codes.Internal, codes.Aborted, codes.Canceled, codes.DeadlineExceeded),
				grpcRetry.WithMax(2),
				grpcRetry.WithPerRetryTimeout(3*time.Second),
			))))
	if err != nil {
		zap.S().Panicf("grpc service:[%s] connection was broken: %v", address, err)
		return nil
	}
	return conn
}

然后再主函数初始化

	zap.S().Infof("init user-service at %s:%d", global.UserService.Host, global.UserService.Port)
	userConn := initialize.InitUserConnection(global.UserService.Host, global.UserService.Port)
	defer func() {
		if err := userConn.Close(); err != nil {
			panic(err)
		}
	}()
	global.UserClient = user.NewUserServiceClient(userConn)

读取配置文件添加解析用户服务

	err = vp.UnmarshalKey("user-service", &global.UserService)
	if err != nil {
		panic(fmt.Sprintf("Read user service configs failed:%v", err))
	}

配置文件增加配置

user-service:
  host: localhost
  port: 8021

启动user-service和user-api服务,然后再yapi中进行调试。

image-20220312002102842

image-20220307224116913

确保代码中路由地址和api接口路由地址一致

	apiGroup := Router.Group("u/v1/user")

结果返回

image-20220312002144851

登陆

数据验证

注册中文翻译,使报错信息更用好,直接在validator文件编写以下代码即可。小写的init函数在运行时会优先执行,这样就不用我们把trans等定义为全局变凉了。

func init() {
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterTagNameFunc(func(fld reflect.StructField) string {
			// 获取自定义tag
            name := fld.Tag.Get("label")
			if name == "" {
                // 获取默认tag
				name = strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
			}
			if name == "-" {
				return ""
			}
			return name
		})

		zhT := zhongwen.New() //中文翻译器
		enT := en.New()       //英文翻译器
		//第一个参数是备用的语言环境,后面的参数是应该支持的语言环境
		uni := ut.New(enT, zhT, enT)
		trans, ok = uni.GetTranslator("zh")
		if !ok {
			fmt.Printf("uni.GetTranslator(zh)")
		}
		_ = zhTranslations.RegisterDefaultTranslations(v, trans)
		return
	}
}

func HandleValidatorError(ctx *gin.Context, err error) {
	errs, ok := err.(validator.ValidationErrors)
	if !ok {
		ctx.JSON(http.StatusOK, gin.H{
			"msg": err.Error(),
		})
	}
	ctx.JSON(http.StatusBadRequest, gin.H{
		"error": removeTopStruct(errs.Translate(trans)),
	})
	return
}

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
	}
	return rsp
}

models/request/request.go中定义登陆字段,注意,我们注册的中文翻译中用到了`label·这个tag,所以如果想返回字段的中文的话,只需要在后面增加label即可

type LoginByPassword struct {
	Mobile   string `json:"mobile" binding:"required" label:"手机号"`
	Password string `json:"password" binding:"required" label:"密码"`
}

实现登陆接口

func LoginByPassword(ctx *gin.Context) {
	info := request.LoginByPassword{}
	if err := ctx.ShouldBind(&info); err != nil {
		utils.HandleValidatorError(ctx, err)
		return
	}
}

注册路由

	apiGroup.POST("login", api.LoginByPassword)

调试

image-20220313154420515

测试

image-20220313191117376

自定义验证器

添加手机号码验证

func ValidateMobile(fl validator.FieldLevel) bool {
	regular := "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$"
	reg := regexp.MustCompile(regular)
	return reg.MatchString(fl.Field().String())
}

给注册的验证器注册翻译

func registerTranslate(tag, msg string) validator.RegisterTranslationsFunc {
	return func(ut ut.Translator) (err error) {
		if err = ut.Add(tag, msg, false); err != nil {
			return
		}
		return
	}
}

封装翻译函数

func translate(ut ut.Translator, fe validator.FieldError) string {
	t, err := ut.T(fe.Tag(), fe.Field(), fe.Param())
	if err != nil {
		return fe.(error).Error()
	}
	return t
}

注册验证器和翻译

func registerCustomerValidateAndTranslation(v *validator.Validate, tag string, fn validator.Func, trans ut.Translator, msg string) {
	if err := v.RegisterValidation(tag, ValidateMobile); err != nil {
		zap.S().Errorf("RegisterValidation validateAccountNumber failed:%v", err)
		panic(err.Error())
	}
	if err := v.RegisterTranslation(tag, trans, registerTranslate(tag, msg), translate); err != nil {
		zap.S().Errorf("RegisterValidation RegisterTranslation failed:%v", err)
		panic(err.Error())
	}
}

在初始化代码中注册验证器

		registerCustomerValidateAndTranslation(v, "mobile", ValidateMobile, trans, "{0}不合法")

LoginByPassword的手机号中添加mobile,tag

type LoginByPassword struct {
	Mobile   string `json:"mobile" binding:"required,mobile" label:"手机号"`
	Password string `json:"password" binding:"required" label:"密码"`
}

重新启动并测试

image-20220313210036351

测试结果

image-20220313210055948

登陆

账号密码登陆的密码检测应该由user-service完成,校验通过需要返回用户的基本信息。所以user-sercice还需要一个login方法

mxshop-api中增加登陆方法

    rpc Login(LoginRequest) returns (UserDetailResponse);

消息

message LoginRequest {
    string mobile = 1;
    string password = 2;
}

去除用户详情中的密码。重新编译

在用户rpc服务实现登陆方法

dao层实现

func (slf *UserServiceImp) CheckPassword(ctx context.Context, in *user.LoginRequest) (out *user.UserDetailResponse, err error) {
	out = &user.UserDetailResponse{}
	value, err := slf.UserDao.GetUserByMobile(in.Mobile)
	if err != nil {
		zap.S().Errorf("GetUserByMobile %s failed: %v", in.Mobile, err)
		return
	}
	if value.Password != utils.Md5Str(in.Password) {
		zap.S().Error("Password not current")
		return
	}
	out.User = slf.transformUserToRpc(value)
	return
}

interface添加方法

	CheckPassword(ctx context.Context, in *user.LoginRequest) (out *user.UserDetailResponse, err error)

handler层实现登陆

func (u *UserService) Login(ctx context.Context, in *user.LoginRequest) (out *user.UserDetailResponse, err error) {
	operator := service.UserServiceInstance()
	out, err = operator.CheckPassword(ctx, in)
	return
}

在api层封装调用函数

func Login(in *user.LoginRequest) (out *user.UserDetailResponse, err error) {
	out, err = global.UserClient.Login(context.Background(), in)
	return
}

完成登陆逻辑

func LoginByPassword(ctx *gin.Context) {
	info := request.LoginByPassword{}
	if err := ctx.ShouldBind(&info); err != nil {
		utils.HandleValidatorError(ctx, err)
		return
	}
	resp, err := service.Login(&user.LoginRequest{Mobile: info.Mobile, Password: info.Password})
	if err != nil {
		utils.HandlerGrpcErrorToHttp(err, ctx)
		return
	}
	if !resp.Success {
		ctx.JSON(http.StatusOK, gin.H{"msg": "账号或密码不正确"})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{"msg": "登陆成功"})
	return
}

启动调试

image-20220313213325674

查看日志可以知道是该手机号不存在,但是这时,rpc层直接返回了err,所以我们需要屏蔽掉该错误

rpc error: code = Unknown desc = record not found

修改roc层登陆,不接受err返回

func (u *UserService) Login(ctx context.Context, in *user.LoginRequest) (out *user.LoginResponse, err error) {
	operator := service.UserServiceInstance()
	out, _ = operator.CheckPassword(ctx, in)
	return
}

再次请求可以看到请求已经成功了,只不过账号或者密码对不上

image-20220313213535022

通过查看数据库,发现密码还没有加密,所以,先把md5校验注释掉,并用数据库已经存在的账号进行登录

image-20220313213947182

jwt

定义读取jwt配置的结构体

type JwtConfig struct {
	SigningKey string `mapstructure:"key"`
	Expire     int    `mapstructure:"expire"`
}

初始化变量增加读取jwt配置的参数

	JwtConfig     *models.JwtConfig
	
	
	JwtConfig = &models.JwtConfig{}

读取配置文件增加读jwt的部分

	err = vp.UnmarshalKey("jwt", &global.JwtConfig)
	if err != nil {
		panic(fmt.Sprintf("Read jwt config faild: %v"))
	}

实现jwt的加解密过程

type Jwt struct {
	SigningKey []byte
}

var (
	TokenExpired     = errors.New("Token is expired")
	TokenNotValidYet = errors.New("Token not active yet")
	TokenMalformed   = errors.New("That's not even a token")
	TokenInvalid     = errors.New("Couldn't handle this token:")
)

func NewJwt() *Jwt {
	return &Jwt{SigningKey: []byte("")}
}

func (j *Jwt) CreateToken(claims request.CustomClaims) (tokenSrt string, err error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenSrt, err = token.SignedString(j.SigningKey)
	return
}

func (j *Jwt) ParseToken(tokenStr string) (claims *request.CustomClaims, err error) {
	token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
		return j.SigningKey, nil
	})
	if err != nil {
		if ve, ok := err.(*jwt.ValidationError); ok {
			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
				err = TokenMalformed
				return
			} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
				err = TokenExpired
				return
			} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
				err = TokenNotValidYet
				return
			} else {
				err = TokenInvalid
				return
			}
		}
	}
	if token == nil {
		err = TokenInvalid
	}
	return
}

func (j *Jwt) RefreshToken(tokenStr string) (string, error) {

	jwt.TimeFunc = func() time.Time {
		return time.Unix(0, 0)
	}
	token, err := jwt.ParseWithClaims(tokenStr, &request.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return j.SigningKey, nil
	})
	if err != nil {
		return "", err
	}
	if claims, ok := token.Claims.(*request.CustomClaims); ok && token.Valid {
		jwt.TimeFunc = time.Now
		claims.StandardClaims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix()
		return j.CreateToken(*claims)
	}
	return "", TokenInvalid
}

登陆成功返回token

	j := middleware.NewJwt()
	claims := request.CustomClaims{
		ID:          resp.User.Id,
		Name:        resp.User.NickName,
		AuthorityId: int32(resp.User.Role),
		StandardClaims: jwt.StandardClaims{
			NotBefore: time.Now().Unix(),
			ExpiresAt: time.Now().Unix() + int64(global.JwtConfig.Expire),
			Issuer:    "imooc",
		},
	}
	token, err := j.CreateToken(claims)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "登陆失败"})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{"msg": "登陆成功", "data": gin.H{
		"token":    token,
		"id":       resp.User.Id,
		"username": resp.User.Username,
		"role":     resp.User.Role,
		"expired":  (time.Now().Unix() + int64(global.JwtConfig.Expire)) * 1000,
	}})

启动调试

image-20220314004510993

至此,登陆生成token已经完成了,下面就是接口访问时的token校验了,此步骤极为简单,只需要使用gin的中间件注册一个token校验即可

先实现token校验中间件

func ValidateAuth(ctx *gin.Context) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		token := ctx.Request.Header.Get("Authorization")
		if token == "" {
			ctx.JSON(http.StatusUnauthorized, gin.H{"msg": "请登录"})
			ctx.Abort()
			return
		}
		j := NewJwt()
		claims, err := j.ParseToken(token)
		if err != nil {
			if err == TokenExpired {
				ctx.JSON(http.StatusUnauthorized, gin.H{"msg": "授权已过期"})
				ctx.Abort()
				return
			}
			ctx.JSON(http.StatusUnauthorized, gin.H{"msg": "未登录"})
			ctx.Abort()
			return
		}
		ctx.Set("claims", claims)
		ctx.Set("userId", claims.ID)
		ctx.Next()
	}
}

添加校验

	validate := middleware.ValidateAuth()

	apiGroup.GET("list", validate, api.GetUserList)

然后启动,可以看到此时已经需要token了

image-20220314005252806

针对某些接口,可能需要不同的权限才能查看对应的内容,比如用户列表只能管理员查看。实现管理员鉴权

func AdminValidate() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		loginUser := utils.GetUser(ctx)
		if loginUser.AuthorityId != int32(user.Role_Admin) {
			ctx.JSON(http.StatusForbidden, gin.H{"msg": "无操作权限"})
			ctx.Abort()
			return
		}
		ctx.Next()
	}
}

其中获取登陆用户的部分抽离成了一个函数,因为其他接口可能会使用到

func GetUser(ctx *gin.Context) *request.CustomClaims {
	if claims, ok := ctx.Get("claims"); ok {
		return claims.(*request.CustomClaims)
	}
	return nil
}

注册管理员权限函数

	apiGroup.GET("list", validate, middleware.AdminValidate(), api.GetUserList)

image-20220315221704861

解决跨域

func Cors() gin.HandlerFunc {
	return func(c *gin.Context) {
		method := c.Request.Method
		c.Header("Access-Control-Allow-Origin", "*")
		c.Header("Access-Control-Allow-Headers", "Content-Type, AccessToken, X-CSRF-Token, Authorization, Token, x-token")
		c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH, PUT")
		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Methods, Content-Type")
		c.Header("Access-Control-Allow-Credentials", "true")
		if method == "OPTIONS" {
			c.AbortWithStatus(http.StatusNoContent)
		}
	}
}


	apiGroup := Router.Group("u/v1/user").Use(middleware.Cors())

图片验证码

采用Go进阶37:重构我的base64Captcha图形验证码项目来实现图形验证码

实现获取图形验证码的路由

func GetCaptcha(ctx *gin.Context) {
	driver := base64Captcha.DefaultDriverDigit
	cp := base64Captcha.NewCaptcha(driver, store)
	id, b64s, err := cp.Generate()
	if err != nil {
		zap.S().Errorf("Generate captcha failed: %v", err)
		ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "生成图形验证码错误"})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{"data": gin.H{"captchaId": id, "picPath": b64s}})
}

注册新的路由地址

	baseGroup := Router.Group("u/v1/base")
	baseGroup.GET("captcha", api.GetCaptcha)

测试

image-20220319104743429

image-20220319104837996

给密码登陆加上图片验证码,首先增加传递参数字段

	CaptchaId string `json:"captchaId" binding:"required" label:"验证码Id"`
	Captcha   string `json:"captcha" binding:"required,min=5,max=5" label:"图片验证码"`

在登陆逻辑中增加验证码验证

	info := request.LoginByPassword{}
	if err := ctx.ShouldBind(&info); err != nil {
		utils.HandleValidatorError(ctx, err)
		return
	}

	if !store.Verify(info.CaptchaId, info.Captcha, false) {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"captcha": "验证码错误",
		})
		return
	}
	resp, err := service.Login(&user.LoginRequest{Mobile: info.Mobile, Password: info.Password})
	if err != nil {
		zap.S().Errorf("Login failed: %v", err)
		utils.HandlerGrpcErrorToHttp(err, ctx)
		return
	}

测试登陆

image-20220319105432031

短信验证码

tips:升级go版本1.18

短信验证码需要用到短信服务商进行短信的发送,我用的是阿里云的短信,这个根据个人喜好选择就行。首先要有一个审核通过的签名

image-20220319112940608

然后还需要一个模板,最重要的就是模板code

image-20220319113115452

这些参数,最好是写在配置文件当中,这样后期更新维护会很方便

在配置文件中添加如下配置,具体值根据自己的配置进行填写

redis:
  host: xxx.xxx.xxx.xxx
  port: xxxx
sms:
  code: xxx
  key: xxxx
  secret: xxxx
  region: xxxx
  domain: xxxxxxxxxxx
  expire: xxx
  name: xxxxx
  version: xxxxxxxx

定义解析配置文件的结构体

type RedisConfig struct {
	Host string `mapstructure:"host"`
	Port int    `mapstructure:"port"`
}

type SmsConfig struct {
	Template string `mapstructure:"code"`
	Key      string `mapstructure:"key"`
	Secret   string `mapstructure:"secret"`
	Expire   int    `mapstructure:"expire"`
	Domain   string `mapstructure:"domain"`
	Region   string `mapstructure:"region"`
	Name     string `mapstructure:"name"`
	Version  string `mapstructure:"version"`
}

在global中定义全局变量

	SmsConfig     *models.SmsConfig
	RedisConfig   *models.RedisConfig


	SmsConfig = &models.SmsConfig{}
	RedisConfig = &models.RedisConfig{}

在初始化中添加读取配置

	err = vp.UnmarshalKey("redis", &global.RedisConfig)
	if err != nil {
		panic(any(fmt.Sprintf("Read redis config faild: %v", err)))
	}
	err = vp.UnmarshalKey("sms", &global.SmsConfig)
	if err != nil {
		panic(any(fmt.Sprintf("Read sms config faild: %v", err)))
	}

然后就是代码实现发送验证码

func GenerateSmsCode(witdh int) string {
	//生成width长度的短信验证码
	numeric := [10]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	r := len(numeric)
	rand.Seed(time.Now().UnixNano())

	var sb strings.Builder
	for i := 0; i < witdh; i++ {
		fmt.Fprintf(&sb, "%d", numeric[rand.Intn(r)])
	}
	return sb.String()
}

func SendSms(ctx *gin.Context) {
	sendSmsForm := request.SendSms{}
	if err := ctx.ShouldBind(&sendSmsForm); err != nil {
		utils.HandleValidatorError(ctx, err)
		return
	}

	client, err := dysmsapi.NewClientWithAccessKey(global.SmsConfig.Region, global.SmsConfig.Key, global.SmsConfig.Secret)
	if err != nil {
		zap.S().Errorw("NewClientWithAccessKey failed", "err", err)
		ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "发送短信验证码失败"})
		return
	}
	smsCode := GenerateSmsCode(6)
	req := requests.NewCommonRequest()
	req.Method = "POST"
	req.Scheme = "https" // https | http
	req.Domain = global.SmsConfig.Domain
	req.Version = global.SmsConfig.Version
	req.ApiName = "SendSms"
	req.QueryParams["RegionId"] = global.SmsConfig.Region
	req.QueryParams["PhoneNumbers"] = sendSmsForm.Mobile
	req.QueryParams["SignName"] = global.SmsConfig.Name
	req.QueryParams["TemplateCode"] = global.SmsConfig.Template
	req.QueryParams["TemplateParam"] = "{\"code\":" + smsCode + "}"
	_, err = client.ProcessCommonRequest(req)
	if err != nil {
		zap.S().Errorw("ProcessCommonRequest failed", "err", err)
		ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "发送短信验证码失败"})
		return
	}
    
	rdb := redis.NewClient(&redis.Options{
		Addr: fmt.Sprintf("%s:%d", global.RedisConfig.Host, global.RedisConfig.Port),
	})
	rdb.Set(context.Background(), sendSmsForm.Mobile, smsCode, time.Duration(global.SmsConfig.Expire)*time.Second)

	ctx.JSON(http.StatusOK, gin.H{
		"msg": "发送成功",
	})
}

注册路由

	baseGroup.POST("sms", api.SendSms)

调试

image-20220319115902038

image-20220319115922212

如此,短信验证码发送就完成了,接下来就是在登陆或者注册的时候取到该验证码进行验证即可

用户注册

实现用户的注册接口。定义用户注册参数解析结构体

type UserRegister struct {
	Mobile   string `json:"mobile" binding:"required,mobile" label:"手机号"`
	Password string `json:"password" binding:"required,min=6,max=16" label:"密码"`
	Code     string `json:"code" binding:"required" label:"短信验证码"`
}

封装grpc的注册接口

func Register(in *user.RegisterUserRequest) (out *user.RegisterUserResponse, err error) {
	out, err = global.UserClient.RegisterUser(context.Background(), in)
	return
}

实现注册用户并登录的api接口

func Register(ctx *gin.Context) {
	info := request.UserRegister{}
	if err := ctx.ShouldBind(&info); err != nil {
		utils.HandleValidatorError(ctx, err)
		return
	}
    
	rdb := redis.NewClient(&redis.Options{
		Addr: fmt.Sprintf("%s:%d", global.RedisConfig.Host, global.RedisConfig.Port),
	})
	value, err := rdb.Get(context.Background(), info.Mobile).Result()
	if err == redis.Nil {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"code": "验证码错误",
		})
		return
	}
	if value != info.Code {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"code": "验证码错误",
		})
		return
	}
    
	resp, err := service.Register(&user.RegisterUserRequest{Mobile: info.Mobile, Password: info.Password, Username: info.Mobile})
	if err != nil {
		zap.S().Errorf("register failed: %v", err)
		utils.HandlerGrpcErrorToHttp(err, ctx)
		return
	}
	j := middleware.NewJwt()
	claims := request.CustomClaims{
		ID:          resp.User.Id,
		Name:        resp.User.NickName,
		AuthorityId: int32(resp.User.Role),
		StandardClaims: jwt.StandardClaims{
			NotBefore: time.Now().Unix(),
			ExpiresAt: time.Now().Unix() + int64(global.JwtConfig.Expire),
			Issuer:    "imooc",
		},
	}
	token, err := j.CreateToken(claims)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "登陆失败"})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{"msg": "注册成功", "data": gin.H{
		"token":    token,
		"id":       resp.User.Id,
		"username": resp.User.Username,
		"role":     resp.User.Role,
		"expired":  (time.Now().Unix() + int64(global.JwtConfig.Expire)) * 1000,
	}})
	return
}

注册接口路由

	apiGroup.POST("register", api.Register)

调试

image-20220319120458424

可以看到注册成功,并且数据库也多了一条新的数据

image-20220319120527349

但是因为前面我没对密码加密,导致现在看到的都是明文。修改user-service,增加一个md5加密方法

func Md5Str(str string) string {
	h := md5.New()
	h.Write([]byte(str))
	return hex.EncodeToString(h.Sum(nil))
}

然后将保存数据修改为

		Password: utils.Md5Str(in.GetPassword()),

需要注意的是,检查登陆的地方也需要同步修改

	if value.Password != utils.Md5Str(in.Password) 

重新启动user-service,直接调用注册用户进行用户的添加

image-20220317214825917

可以看到密码已经被加密了

至此,用户api接口层除了更新之外已经全部完成。

posted @ 2022-03-21 23:43  丶吃鱼的猫  阅读(380)  评论(0编辑  收藏  举报