前期准备
- GoLand :2024.1.1
下载官网:https://www.jetbrains.com/zh-cn/go/download/other.html
- Postman:
下载官网:https://www.postman.com/downloads/
效果图(使用Postman
)
- Google:
- QQ:
And so on…
Just you can try!
项目结构
本项目基于nunu基础上实现(github地址:https://github.com/go-nunu/nunu),Nunu是一个基于Golang的应用脚手架,它的名字来自于英雄联盟中的游戏角色,一个骑在雪怪肩膀上的小男孩。和努努一样,该项目也是站在巨人的肩膀上,它是由Golang生态中各种非常流行的库整合而成的,它们的组合可以帮助你快速构建一个高效、可靠的应用程序。拥有以下功能:
从nunu官方按照规范安装好之后:
基本操作流程
- 用户提交邮箱(email) 以请求 验证码(code)。
- 服务器生成验证码并发送到用户邮箱。
- 用户输入收到的验证码和邮箱进行登录(login)。
- 服务器验证验证码和邮箱。
- 如果验证成功,用户登录成功(sucess);否则,返回错误信息(error)。
代码实现
1.internal/model/user.go和config/local.yml
注意:config和internal在同一级目录下
咱们先定义一个表结构,然后去连接数据库,创建对应映射的表,存储咱们的userid
和email
,验证码(code)是临时的,保存在cache
里就好,不需要落库。
package modelimport ("time""gorm.io/gorm"
)type User struct {Id string `gorm:"primarykey"`Email string `gorm:"not null"`CreatedAt time.TimeUpdatedAt time.TimeDeletedAt gorm.DeletedAt `gorm:"index"`
}func (u *User) TableName() string {return "users"
}
建议直接从右边状态栏里直接连接mysql数据库:
对应的SQL建表语句:
create table users
(id varchar(255) not nullprimary key,email varchar(255) not null,created_at timestamp not null,updated_at timestamp not null,deleted_at timestamp null,constraint emailunique (email),constraint idunique (id)
);
另外还需要在config包下修改local.yml数据库连接配置信息:
库名为刚才所添加表的所在库名哦!
2.api/v1/user.go
package v1type LoginResponseData struct {AccessToken string `json:"accessToken"`
}type SendVerificationCodeRequest struct {Email string `json:"email"`
}type LoginByVerificationCodeRequest struct {Email string `json:"email"`Code string `json:"code"`
}
这段Go代码定义了三个结构体:
LoginResponseData
:表示登录成功后的响应数据,包含一个AccessToken字段,用于标识用户的访问令牌。SendVerificationCodeRequest
:表示发送验证代码请求的数据结构,包含一个Email字段,用于指定要发送验证代码的邮箱地址。LoginByVerificationCodeRequest
:表示通过验证代码登录的请求数据结构,包含一个Email字段和一个Code字段,分别用于指定邮箱地址和收到的验证代码。
3.internal/repository/user.go
GetByEmail
函数通过邮箱地址从数据库中获取用户信息。
- 参数:
ctx context.Context
表示上下文信息,email string
表示要查询的邮箱地址。 - 返回值:
*model.User
表示查询到的用户信息,error
表示错误信息。 - 该函数首先根据邮箱地址查询数据库中是否存在该用户,如果查询成功,则返回用户信息;如果查询失败,则返回错误信息。
CreateUserByEmail
函数通过邮箱地址创建一个新的用户。
- 参数:
ctx context.Context
表示上下文信息,email string
表示要创建的用户的邮箱地址。 - 返回值:
*model.User
表示创建的用户信息,error表示错误信息。 - 该函数首先生成一个唯一的用户ID,然后使用邮箱地址创建一个新的用户实例,并设置创建时间和更新时间为当前时间。
- 接着,将新用户实例插入到数据库中,如果插入成功,则返回新创建的用户信息;如果插入失败,则返回错误信息。
package repositoryimport ("context""errors""fmt""time""emerge-ai-core/common/utils""emerge-ai-core/internal/model""gorm.io/gorm"
)type UserRepository interface {GetByEmail(ctx context.Context, email string) (*model.User, error)CreateUserByEmail(ctx context.Context, email string) (*model.User, error)
}func NewUserRepository(r *Repository,
) UserRepository {return &userRepository{Repository: r,}
}type userRepository struct {*Repository
}func (r *userRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) {var user model.Userif err := r.DB(ctx).Where("email = ?", email).First(&user).Error; err != nil {return nil, err}return &user, nil
}// CreateUserByEmail creates a user by email
func (r *userRepository) CreateUserByEmail(ctx context.Context, email string) (*model.User, error) {now := time.Now()user := &model.User{Id: utils.GenerateUUID(),Email: email,CreatedAt: now,UpdatedAt: now,}if err := r.DB(ctx).Create(user).Error; err != nil {return nil, fmt.Errorf("failed to create user by email: %v", err)}return user, nil
}
4.internal/service/email.go和internal/service/user.go
user.go:
- 定义了一个名为
UserService
的接口,其中包含一个GenerateTokenByUserEmail
方法,用于生成用户的令牌。实现该接口的是userService
结构体,它通过NewUserService
函数进行实例化。GenerateTokenByUserEmail
方法首先通过userRepo
获取用户信息,如果用户不存在,则创建新用户,并使用jwt.GenToken
方法生成令牌。
package serviceimport ("context""errors""time"v1 "emerge-ai-core/api/v1""emerge-ai-core/internal/model""emerge-ai-core/internal/repository""github.com/patrickmn/go-cache""golang.org/x/crypto/bcrypt""gorm.io/gorm"
)type UserService interface {GenerateTokenByUserEmail(ctx context.Context, email string) (string, error)
}func NewUserService(service *Service,userRepo repository.UserRepository,
) UserService {return &userService{userRepo: userRepo,Service: service,}
}type userService struct {userRepo repository.UserRepositoryemailService EmailService*Service
}// GenerateTokenByUserEmail generates a token for a user
func (s *userService) GenerateTokenByUserEmail(ctx context.Context, email string) (string, error) {// get user by emailuser, err := s.userRepo.GetByEmail(ctx, email)if err != nil {if errors.Is(err, gorm.ErrRecordNotFound) {// is new user create useruser, err = s.userRepo.CreateUserByEmail(ctx, email)if err != nil {return "", err}} else {return "", err}}// generate tokentoken, err := s.jwt.GenToken(user.Id, time.Now().Add(time.Hour*24*1))if err != nil {return "", err}return token, nil
}
email.go:
- 提供了一个电子邮件服务,用于发送和验证用户邮箱中的验证代码。
package serviceimport ("context""fmt""math/rand""net/smtp""time""github.com/jordan-wright/email""github.com/patrickmn/go-cache"
)var (// cache for storing verification codes// 缓存中的验证代码将在创建后5分钟内有效,且每隔10分钟进行一次清理。verificationCodeCache = cache.New(5*time.Minute, 10*time.Minute)
)type EmailService interface {SendVerificationCode(ctx context.Context, to string) errorVerifyVerificationCode(email string, code string) bool
}type emailService struct {
}func NewEmailService() EmailService {return &emailService{}
}// SendVerificationCode sends a verification code to the user's email
func (e *emailService) SendVerificationCode(ctx context.Context, to string) error {code := generateVerificationCode()err := e.sendVerificationCode(to, code)if err != nil {return err}// store the verification code in the cache for later verificationverificationCodeCache.Set(to, code, cache.DefaultExpiration)return nil
}// sendVerificationCode 发送验证代码到指定的邮箱。
// 参数 to: 邮件接收人的邮箱地址。
// 参数 code: 需要发送的验证代码。
// 返回值 error: 发送过程中遇到的任何错误。
func (e *emailService) sendVerificationCode(to string, code string) error {// 创建一个新的邮件实例em := email.NewEmail()em.From = "Xxxxxxx <xxxxxxxxxx@qq.com>"em.To = []string{to}em.Subject = "Verification Code"// 设置邮件的HTML内容em.HTML = []byte(`<h1>Verification Code</h1><p>Your verification code is: <strong>` + code + `</strong></p>`)// 发送邮件(这里使用QQ进行发送邮件验证码)err := em.Send("smtp.qq.com:587", smtp.PlainAuth("", "xxxxxxxxxx@qq.com", "这里填写的是授权码", "smtp.qq.com"))if err != nil {return err // 如果发送过程中有错误,返回错误信息}return nil // 邮件发送成功,返回nil
}// 随机生成一个6位数的验证码。
func generateVerificationCode() string {rand.Seed(time.Now().UnixNano())code := fmt.Sprintf("%06d", rand.Intn(1000000))return code
}// VerifyVerificationCode verifies the verification code sent to the user
func (e *emailService) VerifyVerificationCode(email string, code string) bool {// debug codeif code == "123456" {return true}// retrieve the verification code from the cachecachedCode, found := verificationCodeCache.Get(email)// 如果没有找到验证码或者验证码过期,返回falseif !found {return false}// compare the cached code with the provided codeif cachedCode != code {return false}return true
}
注意:这里需要SMTP
协议知识,并且要想获取到授权码,一般要去所在邮箱官方进行申请,这里以QQ为例:
-
电脑端打开QQ邮箱,点击
设置
。
-
点击
账号
。 -
往下滑,找到
POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务
,我这里已经开启了服务。
-
即可获取到授权码!
5.internal/handler/user.go
- 处理用户通过验证代码登录的HTTP请求
package handlerimport ("net/http""emerge-ai-core/api/v1""emerge-ai-core/internal/model""emerge-ai-core/internal/service""github.com/gin-gonic/gin""go.uber.org/zap"
)type UserHandler struct {*HandleruserService service.UserServiceemailService service.EmailService
}func NewUserHandler(handler *Handler, userService service.UserService, emailService service.EmailService) *UserHandler {return &UserHandler{Handler: handler,userService: userService,emailService: emailService,}
}// SendVerificationCode send verification code
func (h *UserHandler) SendVerificationCode(ctx *gin.Context) {var req v1.SendVerificationCodeRequestif err := ctx.ShouldBindJSON(&req); err != nil {v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, err.Error())return}if err := h.emailService.SendVerificationCode(ctx, req.Email); err != nil {v1.HandleError(ctx, http.StatusInternalServerError, v1.ErrInternalServerError, err.Error())return}v1.HandleSuccess(ctx, nil)
}// LoginByVerificationCode by verification code
func (h *UserHandler) LoginByVerificationCode(ctx *gin.Context) {var req v1.LoginByVerificationCodeRequestif err := ctx.ShouldBindJSON(&req); err != nil {v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, err.Error())return}// check verification codeif !h.emailService.VerifyVerificationCode(req.Email, req.Code) {v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil)return}token, err := h.userService.GenerateTokenByUserEmail(ctx, req.Email)if err != nil {v1.HandleError(ctx, http.StatusUnauthorized, v1.ErrUnauthorized, err.Error())return}v1.HandleSuccess(ctx, v1.LoginResponseData{AccessToken: token,})
}
6.internal/server/http.go
- 创建一个以
/v1
为前缀的路由分组v1
,然后在该分组下创建子分组/public
。在/public
子分组下定义了两个POST
请求的路由,分别对应/send-verification-code
和/login
,并绑定相应的处理函数。
package serverimport (apiV1 "emerge-ai-core/api/v1""emerge-ai-core/docs""emerge-ai-core/internal/handler""emerge-ai-core/internal/middleware""emerge-ai-core/pkg/jwt""emerge-ai-core/pkg/log""emerge-ai-core/pkg/server/http""github.com/gin-gonic/gin""github.com/spf13/viper"swaggerfiles "github.com/swaggo/files"ginSwagger "github.com/swaggo/gin-swagger"
)func NewHTTPServer(logger *log.Logger,conf *viper.Viper,jwt *jwt.JWT,userHandler *handler.UserHandler,chatHandler *handler.ChatHandler,
) *http.Server {gin.SetMode(gin.DebugMode)s := http.NewServer(gin.Default(),logger,http.WithServerHost(conf.GetString("http.host")),http.WithServerPort(conf.GetInt("http.port")),)...v1 := s.Group("/v1"){publicRouter := v1.Group("/public"){// POST /v1/public/send-verification-codepublicRouter.POST("/send-verification-code", userHandler.SendVerificationCode)// POST /v1/public/loginpublicRouter.POST("/login", userHandler.LoginByVerificationCode)}}return s
}
Postman测试
同效果图
- Google:
- QQ:
And so on…
Just you can try!