当前位置: 首页 > news >正文

基于Session和Redis实现短信验证码登录

基于Session和Redis实现短信验证码登录

一. 基于Session实现短信验证码登录

1.1 发送短信验证码

//基本流程
提交手机号->校验手机号->如果符合,生成验证码->保存验证码->发送验证码|不符合返回到提交环节

代码模拟

package serviceimport ("log""math/rand""net/http""regexp""time""github.com/gin-gonic/gin"
)type User struct {Phone    stringNickName string
}// 模拟数据库
var userDB = make(map[string]*User)// 常量定义
const (VerifyCodeKey = "verify_code"LoginUserKey  = "login_user"
)// 工具函数:手机号校验
func IsPhoneInvalid(phone string) bool {reg := regexp.MustCompile(`^1[3-9]\d{9}$`)return !reg.MatchString(phone)
}// 工具函数:生成6位随机验证码
func RandomCode() string {rand.Seed(time.Now().UnixNano())return fmt.Sprintf("%06d", rand.Intn(1000000))
}// ---------------------- Handler ----------------------// 发送验证码
func SendCodeHandler(c *gin.Context) {phone := c.Query("phone")// 1. 校验手机号if IsPhoneInvalid(phone) {c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})return}// 2. 生成验证码并保存到 sessioncode := RandomCode()session := sessions.Default(c)session.Set(VerifyCodeKey, code)session.Save()log.Printf("验证码: %s", code)// 3. 返回结果c.JSON(http.StatusOK, gin.H{"msg": "验证码已发送"})
}// 登录
func LoginHandler(c *gin.Context) {type LoginForm struct {Phone string `json:"phone"`Code  string `json:"code"`}var form LoginFormif err := c.ShouldBindJSON(&form); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})return}// 1. 校验手机号if IsPhoneInvalid(form.Phone) {c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})return}// 2. 验证验证码session := sessions.Default(c)sessionCode := session.Get(VerifyCodeKey)if sessionCode == nil || form.Code != sessionCode.(string) {c.JSON(http.StatusBadRequest, gin.H{"error": "验证码不正确"})return}// 3. 查询用户是否存在user, ok := userDB[form.Phone]if !ok {user = &User{Phone:    form.Phone,NickName: "user_" + RandomString(8),}userDB[form.Phone] = user}// 4. 保存登录状态session.Set(LoginUserKey, user.Phone)session.Save()c.JSON(http.StatusOK, gin.H{"msg":  "登录成功","user": user,})
}// 生成随机昵称,用于创建用户
func RandomString(n int) string {letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")rand.Seed(time.Now().UnixNano())s := make([]rune, n)for i := range s {s[i] = letters[rand.Intn(len(letters))]}return string(s)
}

1.手机号校验函数分析

func IsPhoneInvalid(phone string) bool {reg := regexp.MustCompile(`^1[3-9]\d{9}$`)return !reg.MatchString(phone)
}
//regexp.MustCompile(...) 处理正则表达式,MustCompile 会编译正则表达式字符串;如果正则表达式写错,会在程序启动时直接 panic;
//reg.MatchString(phone) 表示用这个正则去匹配传入的字符串 phone,返回 true 或 false。//取反的原因是:函数名叫 IsPhoneInvalid(是否非法),所以当匹配成功(合法)时要返回 false,当匹配失败(非法)时返回 true。

这里运用了正则表达式,这里简单介绍一下各部分含义

^1[3-9]\d{9}$

逐个解释:

符号含义
^匹配字符串开始(防止前面有空格或其他字符)
1手机号第一位必须是 1
[3-9]第二位只能是 3、4、5、6、7、8、9 中的一个
\d{9}后面必须是 9 位数字(\d 表示 0-9,{9} 表示重复9次)
$匹配字符串结束(防止后面多字符)
  1. 生成6位数字验证码函数分析
func RandomCode() string {rand.Seed(time.Now().UnixNano())return fmt.Sprintf("%06d", rand.Intn(1000000))
}
/*rand.Seed(time.Now().UnixNano())Go 的 math/rand 是伪随机数生成器。
如果不设置种子 (Seed),它每次运行都会生成相同的随机序列。所以我们每次调用前用当前时间作为种子,让结果每次不同:time.Now().UnixNano()   // 当前时间的纳秒数这行代码相当于:“告诉随机数引擎:用当前时间纳秒作为随机种子。”这样每次执行都会得到不同的随机序列。rand.Intn(1000000)rand.Intn(n) 会返回一个 [0, n) 区间内的整数。这里传入 1000000,即 0 ≤ 随机数 < 1000000。fmt.Sprintf("%06d", rand.Intn(1000000))这一行的作用是把数字格式化成固定长度 6 位的字符串,不足前面补零。
*/

3.生成验证码部分函数

code := RandomCode()//调用之前的函数生成随机6位验证码session := sessions.Default(c)//取出当前http请求的Session对象session.Set(VerifyCodeKey, code)//存入session中,用于登录时对比session.Save()//把刚才设置的session数据写入存储,实现数据持久化
  1. 生成随机昵称部分函数
// 生成随机昵称
func RandomString(n int) string {letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")rand.Seed(time.Now().UnixNano())s := make([]rune, n)for i := range s {s[i] = letters[rand.Intn(len(letters))]/*遍历 s 的每一个索引。
rand.Intn(len(letters)) 会生成一个 0 ~ len(letters)-1 的随机整数。
用这个随机索引去 letters 里取一个字符,赋值给 s[i]。*/}return string(s)
}//这里首先定义一个rune类型的切片存储所有可能的字符,然后设置随机数避免每次运行都得到相同的值,再创建一个切片用于存放随机字符,最后是用for循环使得切片的每一个位置都能存储一个随机字符

1.2 登录拦截器

//校验登录状态
1.从session中获取用户
2.判断用户是否存在,若不存在进行拦截 
3.若存在,保存到context中

这里使用gin框架写一个中间件函数用于拦截

func LoginRequired() gin.HandlerFunc {return func(c *gin.Context) {session := sessions.Default(c)user := session.Get("user")if user == nil {c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})c.Abort()return}//保存用户到 Contextc.Set("user", user)// 放行c.Next()}
}

1.3 数据脱敏

脱敏(Desensitization) 的目的:

防止敏感信息在日志、接口返回或数据库中被暴露。

常见的脱敏字段:

  • 手机号
  • 邮箱
  • 身份证
  • 用户名 / 姓名
  • 密码:绝对不能返回

只要系统对外提供接口、写日志、暴露数据,都需要脱敏
语言不同,但原则一样。

可以写一个脱敏函数

type User struct {ID       int64  `json:"id"`Phone    string `json:"phone"`NickName string `json:"nick_name"`
}// 脱敏函数
func (u *User) Desensitize() {if len(u.Phone) >= 7 {u.Phone = u.Phone[:3] + "****" + u.Phone[7:]}
}// 登录返回时使用
func LoginHandler(c *gin.Context) {user := User{ID: 1, Phone: "13812345678", NickName: "cr7xin"}user.Desensitize()c.JSON(200, gin.H{"user": user})
}

也可以定义一个类型和方法,可用于全局

type Phone string//这里定义了一个新的类型Phone,底层类型是string,因为之后我们需要绑定脱敏的方法,如果单纯定义string类型无法绑定方法func (p Phone) MarshalJSON() ([]byte, error) {
//这里实现了MarshalJSON() 方法,当我们的类型被json.Marshal() 序列化时,就会自动调用定义的这个方法s := string(p)//需要拿到实际内容,强转为普通字符串if len(s) >= 7 {s = s[:3] + "****" + s[7:]//逻辑即为保留前三位和后四位,用*替换中间部分}return json.Marshal(s)//转换为标准的JSON格式
}type User struct {ID       int64  `json:"id"`Phone    Phone  `json:"phone"`NickName string `json:"nick_name"`
}

1.4 Session集群共享问题

当你的应用部署成多台服务器(集群)时,用户的 Session 存在单台机器的内存里,导致不同机器之间无法共享登录状态。

如何解决?

这里推荐使用Redis缓存的方法,把 Session 存放到 Redis,所有节点共用同一个 Redis,不论请求打到哪台服务器,都会去 Redis 拿 Session

Redis 相较于传统 Session 的 6 大核心优点

维度传统 Session(内存)Redis Session
1.集群共享每台服务器独立维护所有服务器共享同一份 Session
2. 可扩展性增加节点会导致 Session 不一致新节点自动共享,无需额外同步
3. 高可用性服务重启 Session 丢失Redis 可持久化 + 主从备份
4. 性能访问快(本地内存)接近内存访问速度(Redis内存级)
5. 可控性Session 生命周期受进程影响TTL 由 Redis 控制,可动态调整
6. 存储容量受限于单机内存Redis 可分布式扩容(Cluster 模式)

二. 基于Redis实现短信验证码的登录

//在短信验证码登录注册环节
1.提交手机号时以手机号为key读取验证码
2.保存用户时以随机token为key存储用户数据
3.返回token//校验登录环节
1.拿到请求中的token
2.以随机token为key获取用户数据
3.判断逻辑同上

这里使用hash结构,可以灵活对单个字段进行查删

package Redisimport ("context""fmt""log""math/rand""net/http""regexp""time""github.com/gin-gonic/gin""github.com/redis/go-redis/v9"
)var ctx = context.Background()
var rdb *redis.Client// 初始化 redis 客户端
func initRedis() {rdb = redis.NewClient(&redis.Options{Addr:     "localhost:6379",Password: "", // 无密码填空DB:       0,})if err := rdb.Ping(ctx).Err(); err != nil {panic(fmt.Sprintf("Redis连接失败: %v", err))}
}type User struct {Phone    string `json:"phone"`NickName string `json:"nickname"`
}// 模拟数据库
var userDB = make(map[string]*User)// 工具函数:手机号校验
func IsPhoneInvalid(phone string) bool {reg := regexp.MustCompile(`^1[3-9]\\d{9}$`)return !reg.MatchString(phone)
}// 工具函数:随机验证码
func RandomCode() string {rand.Seed(time.Now().UnixNano())return fmt.Sprintf("%06d", rand.Intn(1000000))
}// 工具函数:随机昵称
func RandomString(n int) string {letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")rand.Seed(time.Now().UnixNano())s := make([]rune, n)for i := range s {s[i] = letters[rand.Intn(len(letters))]}return string(s)
}// ----------------- handler -----------------// 发送验证码
func SendCodeHandler(c *gin.Context) {phone := c.Query("phone")if IsPhoneInvalid(phone) {c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})return}code := RandomCode()// 保存到 Redis,key = login:code:<手机号>,有效期 5 分钟err := rdb.Set(ctx, "login:code:"+phone, code, 5*time.Minute).Err()if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": "Redis 写入失败"})return}log.Printf("验证码: %s", code)c.JSON(http.StatusOK, gin.H{"msg": "验证码已发送"})
}// 登录接口
func LoginHandler(c *gin.Context) {var form struct {Phone string `json:"phone"`Code  string `json:"code"`}if err := c.ShouldBindJSON(&form); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})return}if IsPhoneInvalid(form.Phone) {c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})return}// 从 Redis 取验证码val, err := rdb.Get(ctx, "login:code:"+form.Phone).Result()if err == redis.Nil || val != form.Code {c.JSON(http.StatusBadRequest, gin.H{"error": "验证码不正确或已过期"})return}// 验证通过后删除验证码(防止复用)rdb.Del(ctx, "login:code:"+form.Phone)// 模拟数据库查找用户user, ok := userDB[form.Phone]if !ok {user = &User{Phone:    form.Phone,NickName: "user_" + RandomString(8),}userDB[form.Phone] = user}// 生成登录凭证(这里简化为打印)log.Printf("用户 %s 登录成功", user.Phone)c.JSON(http.StatusOK, gin.H{"msg":  "登录成功","user": user,})
}/*func main() {initRedis()r := gin.Default()r.GET("/send_code", SendCodeHandler)r.POST("/login", LoginHandler)r.Run(":8080")
}*/

2.1 配置刷新token的拦截器

为了防止在操作网站时突然由于Redis中的token过期,这里把刷新的操作放在拦截器中

func RefreshToken() gin.HandlerFunc {return func(c *gin.Context) {ctx := context.Background()token := c.GetHeader("Authorization")//从请求头中获取tokenif token == "" {c.Next() // 没 token 不做刷新,交给 LoginRequired 拦截return}userID, err := rdb.Get(ctx, token).Result()if err == nil {// 刷新 Redis TTLrdb.Expire(ctx, token, 30*time.Minute)// 将用户信息放入 contextc.Set("userID", userID)}c.Next() // 放行到下一个中间件或 handler}
}

注册中间件顺序

请求 → RefreshToken() → LoginRequired() → Handler|                |负责拦截一切路径      拦截需要登录的路径
authGroup := r.Group("/api")
authGroup.Use(RefreshToken())   // 先刷新 TTL 并把用户信息放入 context
authGroup.Use(LoginRequired())  // 再判断是否登录
{authGroup.GET("/userinfo", UserInfoHandler)
}

思路总结

1.拦截器

如果采用Redis缓存,我们需要配置两个拦截器,第一个拦截器负责刷新token,用于一切路径,第二个用于拦截用户不存在的的请求

2.核心函数部分

1.首先我们通过querystring参数拿到手机号进行校验,检查手机号格式是否合法,这里用regexp.MustCompile函数接收正则表达式进行检验

2.然后我们通过rand.Seed(time.Now().UnixNano())生成随机验证码保存到redis中并发送

3.根据手机号从redis中取到相应的验证码验证,通过后删除验证码

4.如果是注册用户,生成随机昵称

5.如果最后返回手机号等私密信息,需要进行数据脱敏

http://www.dtcms.com/a/494350.html

相关文章:

  • 视觉Slam14讲笔记第6讲非线性优化
  • 仓库管理系统:定义、需求和​​类型
  • 项目管理进阶——解读 软件质量体系白皮书【附全文阅读】
  • ARQC生成模拟
  • 网站架构演变过程ui和网页设计
  • ASR+LLM:B站学习视屏下载并生成学习笔记
  • C++中的引用
  • Linux 系统下 ZONE 区域的划分
  • 网站内部链接优化方法cpanel伪静态wordpress
  • LangChain 表达式语言核心组合:Prompt + LLM + OutputParser
  • 【管理多版本Python环境】Anaconda安装及使用
  • AI修图革命:IOPaint+cpolar让废片拯救触手可及
  • 读书笔记整理--网络学习与概念整合
  • 老铁推荐个2021网站好吗wordpress 入口文件
  • 前端自动化部署全流程(Jenkins + Nginx)
  • 音视频处理(一):什么决定了你的音色?声音的三要素
  • python+uniapp基于微信小程序的助眠小程序
  • ELK运维之路(Filebeat第二章-7.17.24)
  • (未成功)Chrome调试避免跳入第三方源码(设置Blackbox Scripts、将目录添加到忽略列表、向忽略列表添加脚本)
  • 网站建设毕业答辩问题学建设网站首页
  • 大模型在企业云计算领域的核心应用能力要求
  • CloudDM:一站式数据库开发管理工具
  • 适合用struts2做的网站批量发布网站
  • Azure OpenAI 错误码处理完整指南
  • NuxtJS从0到1开发SSR项目-添加Nuxt UI
  • 如何检查本地是否存在 Docker 镜像 ?
  • 查询工程建设项目的网站泉州网站制作平台
  • 单序列和双序列问题——动态规划
  • 【建模与仿真】基于TPE-SVM的乳腺癌诊断可解释人工智能方法
  • 2.5、物联网设备的“免疫系统”:深入解析安全启动与可信执行环境