【三】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.go
, global/global.go
,internal/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
启动日志
用户列表
在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中进行调试。
确保代码中路由地址和api接口路由地址一致
apiGroup := Router.Group("u/v1/user")
结果返回
登陆
数据验证
注册中文翻译,使报错信息更用好,直接在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)
调试
测试
自定义验证器
添加手机号码验证
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:"密码"`
}
重新启动并测试
测试结果
登陆
账号密码登陆的密码检测应该由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
}
启动调试
查看日志可以知道是该手机号不存在,但是这时,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
}
再次请求可以看到请求已经成功了,只不过账号或者密码对不上
通过查看数据库,发现密码还没有加密,所以,先把md5校验注释掉,并用数据库已经存在的账号进行登录
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,
}})
启动调试
至此,登陆生成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了
针对某些接口,可能需要不同的权限才能查看对应的内容,比如用户列表只能管理员查看。实现管理员鉴权
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)
解决跨域
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)
测试
给密码登陆加上图片验证码,首先增加传递参数字段
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
}
测试登陆
短信验证码
tips:升级go版本1.18
短信验证码需要用到短信服务商进行短信的发送,我用的是阿里云的短信,这个根据个人喜好选择就行。首先要有一个审核通过的签名
然后还需要一个模板,最重要的就是模板code
这些参数,最好是写在配置文件当中,这样后期更新维护会很方便
在配置文件中添加如下配置,具体值根据自己的配置进行填写
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)
调试
如此,短信验证码发送就完成了,接下来就是在登陆或者注册的时候取到该验证码进行验证即可
用户注册
实现用户的注册接口。定义用户注册参数解析结构体
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)
调试
可以看到注册成功,并且数据库也多了一条新的数据
但是因为前面我没对密码加密,导致现在看到的都是明文。修改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,直接调用注册用户进行用户的添加
可以看到密码已经被加密了
至此,用户api接口层除了更新之外已经全部完成。
作者:丶吃鱼的猫
出处:https://www.cnblogs.com/eatfishcat/p/16037375.html
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接 如有问题, 可站内留言咨询.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)