如何在go项目中实现发送邮箱验证码、邮箱+验证码登录
前期准备
- 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!
相关文章:
如何在go项目中实现发送邮箱验证码、邮箱+验证码登录
前期准备 GoLand :2024.1.1 下载官网:https://www.jetbrains.com/zh-cn/go/download/other.html Postman: 下载官网:https://www.postman.com/downloads/ 效果图(使用Postman) Google: QQ: And …...
Docker 部署 Nginx 实现一个极简的 负载均衡
背景: Nginx是异步框架的网页服务器,其常用作反向代理(负载均衡器)。在一般的小项目中, 服务器不多, 如果不考虑使用服务注册与发现, 使用Nginx 可以容易实现负载均衡。 在特此写一个快速入门 Nginx 的技术贴, 使用 Docker 部署 Nginx, 实现一个极简的加权轮询负载均…...
Java刷题总结(面试)
1、String类 String不可变 java 中String是 immutable的,也就是不可变,一旦初始化,其引用指向的内容是不可变的。 也就是说,String str “aa”;str“bb”;第二句不是改变“aa”所存储地址的内容…...
ipad air6电容笔推荐,2024十大高性价比电容笔排行榜!
电容笔作为ipad的最佳拍档,为学生党和打工人带来了极大的便利,二者搭配效率真的大大提升,但是,如何选购一支适合自己的电容笔呢?作为一个对数码设备非常感兴趣并且有一定了解的人,我根据自己多年的使用经…...
Java Memorandum
Java Memorandum 1 定义安全的集合2 collection集合用迭代器删除元素时避免并发修改异常3 异常捕获4 RequestBody和RequestParam和Parameter区别4.1 RequestBody4.2 RequestParam4.3 Parameter 1 定义安全的集合 void old() {ArrayList<Apple> apples new ArrayList<…...
大数据学习之 Hadoop部署
Hadoop部署 Linux桌面模式关闭 # 设置 systemctl set-default multi-user.target # 重启 reboot防火墙关闭 systemctl status firewalld systemctl stop firewalld # 关闭开机自启 systemctl disable firewalld配置Java环境 echo $JAVA_HOME java -version # Java配置 # 上传ja…...
xxe漏洞--xml外部实体注入漏洞
1.xxe漏洞介绍 XXE(XML External Entity Injection)是一种攻击技术,它允许攻击者注入恶意的外部实体到XML文档中。如果应用程序处理XML输入时未正确配置,攻击者可以利用这个漏洞访问受影响系统上的敏感文件、执行远程代码、探测内…...
Nginx反向代理与负载均衡:让网站像海豚一样灵活
引言:"当网站遇上海豚:Nginx让数据流动更流畅!"想象一下,你的网站是一片繁忙的海域,而Nginx就像一群聪明的海豚,它们不仅能够迅速地找到最佳的捕食路线(反向代理)…...
企业应考虑的优秀云安全措施
作为云客户,企业有责任确保正确使用他们提供的工具来保证数据和应用程序的安全。让德迅云安全来跟大家一起研究一些典型企业应该考虑的优秀云安全措施。 在数据安全和隐私方面,企业是否在努力跟上疫情的发展?企业不是一个人。就像多年以前,C…...
如何将老板的游戏机接入阿里云自建K8S跑大模型(下)- 安装nvidia/gpu-operator支持GPU在容器中共享
文章目录 安装nvidia/gpu-operator支持GPU在容器中共享 安装nvidia/gpu-operator支持GPU在容器中共享 安装 nvidia/gpu-operator遇到两个问题: 由于我们都懂的某个原因,导致某些镜像一直现在不成功。 解决办法,准备一个🪜&#…...
代码随想录-Day16
104. 二叉树的最大深度 方法一:深度优先搜索 class Solution {public int maxDepth(TreeNode root) {if (root null) {return 0;} else {int leftHeight maxDepth(root.left);int rightHeight maxDepth(root.right);return Math.max(leftHeight, rightHeight) …...
31.@Anonymous
1►@Anonymous原理 大家应该已经习惯我的教学套路,很多时候都是先使用,然后讲述原理。 上节课我们使用了注解@Anonymous,然后接口就可以直接被访问到了,不用token!不用token!不用token!。 我们一般知道,注解是给程序看的,给机器看的,当然也是给程序员看的。注解如果…...
oracle 表同一列只取最新一条数据写法
select * from (select t.*,row_number() over(partition by 去重列名 order by 排序列名 desc) as rnfrom 表名)where rn1 1.row_number() over(....): 为每条数据分配一个行号,1.2.3....这样的 2.partition by : 以某列作为分组,每个分组行号从1开始…...
C语言游戏实战(12):植物大战僵尸(坤版)
植物大战僵尸 前言: 本游戏使用C语言和easyx图形库编写,通过这个项目我们可以深度的掌握C语言的各种语言特性和高级开发技巧,以及锻炼我们独立的项目开发能力, 在开始编写代码之前,我们需要先了解一下游戏的基本规则…...
提权方式及原理汇总
一、Linux提权 1、SUID提权 SUID(设置用户ID)是赋予文件的一种权限,它会出现在文件拥有者权限的执行位上,具有这种权限的文件会在其执行时,使调用者暂时获得该文件拥有者的权限。 为可执行文件添加suid权限的目的是简…...
【leetcode----二叉树中的最大路径和】
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。 路径和 是路径中各节点值的总和。 给你一个二叉树的根节点 root ,…...
Rust: 编译过程中链接器 `cc` 没有找到
这个错误信息表明在编译过程中链接器 cc 没有找到。cc 通常是 C 编译器的符号链接,它指向系统上的实际 C 编译器,如 gcc 或 clang。这个错误通常意味着你的系统缺少必要的编译工具链。 要解决这个问题,你需要确保你的系统上安装了 C 编译器。…...
【vue-3】动态属性绑定v-bind
1、文本动态绑定: <input type"text" v-bind:value"web.url"> 简写: <input type"text" :value"web.url"> 2、文字样式动态绑定 <b :class"{textColor:web.fontStatus}">vue学…...
Rust:多线程环境下使用 Mutex<T> 还是 Arc<Mutex<T>> ?
在 Rust 中,Mutex 本身不是线程不安全的;它提供了内部的线程同步机制。然而,如果你想在多线程环境中共享同一个 Mutex,你需要确保这个 Mutex 可以被多个线程访问。为此,你通常需要使用 Arc<Mutex<T>>。Arc…...
关于如何创建一个可配置的 SpringBoot Web 项目的全局异常处理
前情概要 这个问题其实困扰了我一周时间,一周都在 Google 上旅游,我要如何动态的设置 RestControllerAdvice 里面的 basePackages 以及 baseClasses 的值呢?经过一周的时间寻求无果之后打算决定放弃的我终于找到了一些关键的线索。 当然在此…...
终极指南:3步用VR-Reversal将3D视频转为2D,普通设备也能自由探索VR世界
终极指南:3步用VR-Reversal将3D视频转为2D,普通设备也能自由探索VR世界 【免费下载链接】VR-reversal VR-Reversal - Player for conversion of 3D video to 2D with optional saving of head tracking data and rendering out of 2D copies. 项目地址…...
Kodi中文插件库终极指南:3分钟打造你的智能家庭影院
Kodi中文插件库终极指南:3分钟打造你的智能家庭影院 【免费下载链接】xbmc-addons-chinese Addon scripts, plugins, and skins for XBMC Media Center. Special for chinese laguage. 项目地址: https://gitcode.com/gh_mirrors/xb/xbmc-addons-chinese 还在…...
电商评论分析利器:GTE文本向量实战情感分析与产品问题挖掘
电商评论分析利器:GTE文本向量实战情感分析与产品问题挖掘 1. 电商评论分析的痛点与解决方案 电商平台每天产生海量用户评论,这些评论蕴含着消费者真实的产品体验和市场反馈。传统的人工分析方法面临三大挑战: 处理效率低:人工…...
终极Redis可视化工具:Another Redis Desktop Manager完全使用指南
终极Redis可视化工具:Another Redis Desktop Manager完全使用指南 【免费下载链接】AnotherRedisDesktopManager 🚀🚀🚀A faster, better and more stable Redis desktop manager [GUI client], compatible with Linux, Windows, …...
Anaconda镜像源失效?三步解决UnavailableInvalidChannel报错
1. 镜像源失效的典型症状 当你兴冲冲地打开终端准备创建新的Python虚拟环境时,突然看到这段红色报错信息: Collecting package metadata (current_repodata.json): failed UnavailableInvalidChannel: The channel is not accessible or is invalid.chan…...
网络爬虫主流思路及反爬破解技术应用(新手速成)
网络爬虫的主流思路是模拟浏览器行为自动化抓取网页数据,而反爬破解技术则通过代理IP、请求伪装、动态渲染处理等方式绕过网站防护机制,实现稳定高效的数据采集 。一、主流爬虫技术思路 1.请求模拟与数据提取 使用 requests 或 urllib 构建H…...
深入解析SCB_AIRCR:STM32中断与复位控制的关键寄存器
1. SCB_AIRCR寄存器:STM32的中枢神经 第一次接触STM32的中断系统时,我对着密密麻麻的寄存器列表发懵,直到发现了SCB_AIRCR这个"控制中枢"。它就像城市交通指挥中心,决定着所有中断车辆的通行规则。这个位于0xE000ED00地…...
如何突破教育资源壁垒?智能解析工具让电子课本获取效率提升200%
如何突破教育资源壁垒?智能解析工具让电子课本获取效率提升200% 【免费下载链接】tchMaterial-parser 国家中小学智慧教育平台 电子课本下载工具,帮助您从智慧教育平台中获取电子课本的 PDF 文件网址并进行下载,让您更方便地获取课本内容。 …...
【实战指南】解决Qt平台插件加载失败:从环境变量到PyQt5重装的完整方案
1. 遇到Qt平台插件加载失败?别慌,先看懂报错信息 最近在Windows上跑labelimg标注工具时,突然弹出一个让人头疼的错误: qt.qpa.plugin: Could not load the Qt platform plugin "windows" in "" even though…...
人工智能应用- 人工智能风险与伦理:01.数据安全
图: 人脸识别的滥用可能带来隐私风险,为不法分子提供可乘之机。特别是无处不在的摄像头,使我们的人脸生物信息可能暴露在风险中,被非法采集。人工智能的广泛应用离不开对数据的采集与分析,但也因此带来了数据安全方面的担忧。人工…...
