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

来源:这里教程网 时间:2026-02-16 11:41:07 作者:
基于 session 实现短信验证码登录短信验证码登录发送验证码用户登录创建用户登录拦截器数据脱敏Session 集群共享问题基于 Redis 实现短信验证码登录短信验证登录发送验证码用户登录创建用户配置登录拦截器

基于 session 实现短信验证码登录

package main

import (
        "fmt"
        "log"
        "math/rand"
        "net/http"
        "time"

        "github.com/dchest/captcha"
        "github.com/gin-gonic/gin"
)

// Constants
const (
        VERIFY_CODE = "verify_code"
        LOGIN_CODE  = "login_code"
        LOGIN_USER  = "login_user"
        USER_NICK_NAME_PREFIX = "user_"
)

// User represents a user in the system
type User struct {
        Phone    string `json:"phone"`
        NickName string `json:"nick_name"`
}

// Result is a common response structure
type Result struct {
        Code    int    `json:"code"`
        Message string `json:"message"`
        Data    any    `json:"data"`
}

// Helper function to return success response
func Success(message string, data any) Result {
        return Result{Code: 200, Message: message, Data: data}
}

// Helper function to return failure response
func Failure(message string) Result {
        return Result{Code: 400, Message: message, Data: nil}
}

// isPhoneInvalid checks if the phone number is valid (basic validation)
func isPhoneInvalid(phone string) bool {
        // This is a placeholder for actual phone validation (e.g., regex)
        // For simplicity, we'll assume the phone should be 10 digits long
        return len(phone) != 10
}

// RandomNumbers generates a random numeric string of a given length
func RandomNumbers(length int) string {
        rand.Seed(time.Now().UnixNano())
        code := ""
        for i := 0; i < length; i++ {
                code += fmt.Sprintf("%d", rand.Intn(10))
        }
        return code
}

// SendCode handles the process of sending a verification code
func SendCode(c *gin.Context) {
        phone := c.DefaultPostForm("phone", "")
        if isPhoneInvalid(phone) {
                c.JSON(http.StatusBadRequest, Failure("手机号格式不正确"))
                return
        }
        // Generate the verification code and store it in the session
        code := RandomNumbers(6)
        session := c.MustGet("session").(map[string]interface{}) // Example: Use Gin session middleware to manage session
        session[VERIFY_CODE] = code
        log.Printf("验证码: %s", code)
        c.JSON(http.StatusOK, Success("验证码已发送", nil))
}

// Login handles user login logic
func Login(c *gin.Context) {
        var loginForm struct {
                Phone string `json:"phone"`
                Code  string `json:"code"`
        }
        if err := c.BindJSON(&loginForm); err != nil {
                c.JSON(http.StatusBadRequest, Failure("请求参数错误"))
                return
        }

        phone := loginForm.Phone
        code := loginForm.Code
        session := c.MustGet("session").(map[string]interface{}) // Example: Use Gin session middleware to manage session

        // Validate phone number format
        if isPhoneInvalid(phone) {
                c.JSON(http.StatusBadRequest, Failure("手机号格式不正确"))
                return
        }

        // Validate the verification code
        sessionCode, ok := session[LOGIN_CODE].(string)
        if !ok || code != sessionCode {
                c.JSON(http.StatusBadRequest, Failure("验证码不正确"))
                return
        }

        // Check if the user exists in the database (mocked in this example)
        user := getUserByPhone(phone)
        if user == nil {
                // User doesn't exist, create a new user
                user = createUserWithPhone(phone)
        }

        // Save user info to session for future reference
        session[LOGIN_USER] = user
        c.JSON(http.StatusOK, Success("登录成功", user))
}

// CreateUserWithPhone creates a new user based on the phone number
func createUserWithPhone(phone string) *User {
        // Generate a random nickname for the user
        nickName := USER_NICK_NAME_PREFIX + RandomNumbers(10)
        return &User{Phone: phone, NickName: nickName}
}

// Mock function to get a user by phone (in a real application, this would query the database)
func getUserByPhone(phone string) *User {
        // Here, we would normally query the database
        // For now, we just return nil to simulate a user not found
        return nil
}

func main() {
        r := gin.Default()

        // Simple session mock using a map (real app would use a proper session management system)
        r.Use(func(c *gin.Context) {
                c.Set("session", make(map[string]interface{}))
                c.Next()
        })

        // Routes
        r.POST("/send-code", SendCode)
        r.POST("/login", Login)

        // Run the server
        r.Run(":8080")
}

短信验证码登录

前期准备:
为了提高代码的可读性、可维护性和一致性,方便后续修改和减少出错的机会,把几个会用到的字符串赋值给常量

const (
        VERIFY_CODE = "verify_code"
        LOGIN_CODE  = "login_code"
        LOGIN_USER  = "login_user"
        USER_NICK_NAME_PREFIX = "user_"
)

定义结构体,接收前端数据

//用户结构体
type User struct {
        Phone    string `json:"phone"`
        NickName string `json:"nick_name"`
}

定义封装响应数据的数据结构

type Result struct {
        Code    int    `json:"code"`
        Message string `json:"message"`
        Data    any    `json:"data"`
}

使用 SuccessFailure 函数 构建标准化的响应格式,封装响应的内容,简化返回结果的构建过程,使得返回的数据格式统一且易于维护。

func Success(message string, data any) Result {
        return Result{Code: 200, Message: message, Data: data}
}
func Failure(message string) Result {
        return Result{Code: 400, Message: message, Data: nil}
}

发送验证码

想一下平时需要验证的过程,填写手机号 --> 接收验证码 --> 输入验证码
所以它的一个基本流程:

首先,输入了手机号,需要判断手机号合不合法

// 判断手机号是否合法,若手机号不合法,返回 True
func IsPhoneInvalid(phone string) bool {
    reg := regexp.MustCompile(`^1[3-9]\d{9}$`)
    return !reg.MatchString(phone)
}

解释:

regexp.MustCompile(^1[3-9]\d{9}$):

使用正则表达式 ^1[3-9]\d{9}$

^ 开始匹配字符串1手机号第一位必须是1[3-9] 手机号第二位必须在 3到9 之间\d{9}后面必须是九位数字,\d 表示 0-9, {9} 表示重复九次$ 表示匹配字符串结束
reg.MatchString(phone): 

reg 是用来“匹配手机号是否合法”的工具

随机生成六位数验证码

func RandomNumbers(length int) string {
        rand.Seed(time.Now().UnixNano())
        code := ""
        for i := 0; i < length; i++ {
                code += fmt.Sprintf("%d", rand.Intn(10))
        }
        return code
}

用户登录

// 用户登录
func Login(c *gin.Context) {
        //定义一个临时结构体,用来接收前端请求的 JSON 数据。
        var loginForm struct {
                Phone string `json:"phone"`   //接收手机号
                Code  string `json:"code"`    //接收验证码
        }
        //将请求的 JSON 数据绑定到 loginForm 结构体中。
        //BindJSON 是 Gin 框架中的方法,自动将请求体中的 JSON 数据转换成结构体形式,赋值给 loginForm
        if err := c.BindJSON(&loginForm); err != nil {
                c.JSON(http.StatusBadRequest, Failure("请求参数错误"))
                return
        }

        phone := loginForm.Phone
        code := loginForm.Code
        session := c.MustGet("session").(map[string]interface{}) // Example: Use Gin session middleware to manage session

        // 判断手机号是否合法
                if isPhoneInvalid(phone) {
                c.JSON(http.StatusBadRequest, Failure("手机号格式不正确"))
                return
        }

        // 验证验证码
        sessionCode, ok := session[LOGIN_CODE].(string)
        if !ok || code != sessionCode {
                c.JSON(http.StatusBadRequest, Failure("验证码不正确"))
                return
        }

        
        user := getUserByPhone(phone)//根据手机号查询数据库,看用户是否已经存在。
        if user == nil {  //用户不存在,创建一个新用户
                user = createUserWithPhone(phone)
        }

        // 将用户信息保存到 session 中
        session[LOGIN_USER] = user
        c.JSON(http.StatusOK, Success("登录成功", user))
}

创建用户

// 根据电话号码创建一个新用户
func createUserWithPhone(phone string) *User {
        // 为用户生成一个随机昵称
        nickName := USER_NICK_NAME_PREFIX + RandomNumbers(10)
        return &User{Phone: phone, NickName: nickName}
}

登录拦截器

package main

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

var LOGIN_USER = "login_user"

// User 模拟用户对象
type User struct {
        ID   int
        Name string
}

// ThreadLocalUtls 模拟存储用户信息的线程局部变量(Go 使用 context.Context)
var ThreadLocalUtls = make(map[string]interface{})

// LoginInterceptor 登录拦截器
func LoginInterceptor() gin.HandlerFunc {
        return func(c *gin.Context) {
                // 获取 session 中的用户信息
                session := c.MustGet("session").(map[string]interface{})
                user, exists := session[LOGIN_USER].(*User)
                if !exists || user == nil {
                        // 用户不存在,返回未授权状态
                        c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
                        c.Abort() // 中止后续处理
                        return
                }

                // 用户存在,将用户信息保存到 ThreadLocalUtls(可以使用 Context 中的 Key-Value 存储)
                ThreadLocalUtls["user"] = user

                c.Next() // 执行后续处理
        }
}

func main() {
        // 创建 Gin 引擎
        r := gin.Default()

        // 模拟用户登录的 Session
        r.Use(func(c *gin.Context) {
                session := make(map[string]interface{})
                // 假设用户已经登录
                session[LOGIN_USER] = &User{ID: 1, Name: "John Doe"}
                c.Set("session", session)
                c.Next()
        })

        // 应用登录拦截器
        r.Use(LoginInterceptor())

        // 一个受保护的接口
        r.GET("/profile", func(c *gin.Context) {
                user := ThreadLocalUtls["user"].(*User)
                c.JSON(http.StatusOK, gin.H{
                        "message": "User profile",
                        "user":    user,
                })
        })

        // 启动服务
        r.Run(":8080")
}

基本流程

可以用中间件来实现拦截器的功能

func LoginInterceptor() gin.HandlerFunc {
        return func(c *gin.Context) {
                // 获取 session 中的用户信息
                session := c.MustGet("session").(map[string]interface{})
                
                // 判断 用户是否存在
                user, exists := session[LOGIN_USER].(*User)
                if !exists || user == nil {
                        // 用户不存在,返回未授权状态
                        c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
                        c.Abort() // 中止后续处理
                        return
                }

                // 用户存在,将用户信息保存到 ThreadLocalUtls(可以使用 Context 中的 Key-Value 存储)
                ThreadLocalUtls["user"] = user

                c.Next() // 执行后续处理
        }
}

数据脱敏

为了 保护敏感数据,防止信息泄露并确保数据的隐私安全,要进行 数据脱敏

// UserDTO 用户数据
type UserDTO struct {
        ID       int64  `json:"id"`        // 用户 ID
        NickName string `json:"nickName"`  // 用户昵称
        Icon     string `json:"icon"`      // 用户头像
}

例如

    可以将 NickNameIcon 中的部分敏感信息进行替换或隐藏
func (u UserDTO) String() string {
    // 将昵称进行脱敏,显示前两个字符,后面用 * 替换
    maskedNickName := u.NickName
    if len(u.NickName) > 2 {
        maskedNickName = u.NickName[:2] + "****"
    }

    // 返回脱敏后的字符串表示
    return fmt.Sprintf("UserDTO{id: %d, nickName: %s, icon: %s}", u.ID, maskedNickName, u.Icon)
}
    完全隐藏某些字段(例如 ID 或 Icon)(比如 在分享或展示数据时,只保留不敏感的部分)
func (u UserDTO) String() string {
    return fmt.Sprintf("UserDTO{nickName: %s}", u.NickName)  // 只显示昵称
}
    如果是日期、年龄等信息,可以通过泛化的方式处理。例如,将精确的年龄替换为一个范围:
func (u UserDTO) String() string {
    age := 28 // 假设是用户年龄
    ageRange := "20-30" // 泛化处理
    return fmt.Sprintf("UserDTO{age: %s, nickName: %s}", ageRange, u.NickName)
}

Session 集群共享问题

假设有一个购物网站,这个网站的背后 有很多服务器在后端维护数据
用户第一次登录,访问服务器A ,登录信息存储在服务器A 上;下一次登录,用户的请求被分配到了服务器B ,而服务器A 上的Session 并没有同步到服务器B 上,服务器B 上不存在用户的Session 信息,导致用户数据丢失,可能需要重新登录
这就是会话丢失问题

如果用户在服务器 A 上修改了 Session 数据(比如更改了购物车的内容),这些修改不会自动同步到其他服务器上(如服务器 B)。当用户请求到 B 时,看到的还是旧的 Session 数据,造成数据不一致
这就是数据不一致问题

解决方案:
集中式存储(如 Redis):

使用 Redis 作为共享的会话存储。所有服务器都将用户的 Session 数据存储到 Redis 中,确保所有服务器可以访问同一份数据,不管用户请求到哪台服务器,都能获得一致的会话信息。

Session 复制:

在某些情况下,服务器之间可以复制 Session 数据。这样,当用户请求被路由到其他服务器时,已经存在的 Session 数据可以同步过来。缺点就是会增加服务器的额外内存开销

这里我们使用 Redis 解决

Redis 和 Session 对比

特性传统Session存储Redis
存储位置存储在服务器内存或数据库中存储在 Redis 服务器的内存中
高可用性与持久化无高可用,数据丢失风险大支持高可用和数据持久化(RDB/AOF)
性能单服务器内存快,数据库较慢高性能,能够处理大规模并发请求
扩展性不适合分布式,扩展困难水平扩展,支持 Redis 集群和分片
数据同步与共享需要外部机制来同步数据集中式存储,所有服务器共享数据

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

使用hash 存储用户信息

同样地,首先定义这几个数据结构

// 创建返回结果
func NewResult(status string, message string, data interface{}) Result {
        return Result{
                Status:  status,
                Message: message,
                Data:    data,
        }
}

短信验证登录

package main

import (
        "fmt"
        "log"
        "math/rand"
        "strconv"
        "strings"
        "time"

        "github.com/go-redis/redis/v8"
        "github.com/google/uuid"
        "github.com/golang-jwt/jwt/v4"
        "golang.org/x/net/context"
)

const (
        LOGIN_CODE_KEY    = "login:code:"
        LOGIN_USER_KEY    = "login:user:"
        LOGIN_CODE_TTL    = 5 * time.Minute
        LOGIN_USER_TTL    = 30 * time.Minute
)

var rdb *redis.Client

// Result 结构体,用于返回接口的结果
type Result struct {
        Status  string `json:"status"`
        Message string `json:"message,omitempty"`
        Data    interface{} `json:"data,omitempty"`
}

// 创建返回结果
func NewResult(status string, message string, data interface{}) Result {
        return Result{
                Status:  status,
                Message: message,
                Data:    data,
        }
}

// 验证手机号格式的简单函数
func isPhoneInvalid(phone string) bool {
        // 假设是一个简单的手机号格式检查
        return len(phone) != 11 || !strings.HasPrefix(phone, "1")
}

// 发送验证码
func sendCode(phone string) Result {
        // 1、判断手机号是否合法
        if isPhoneInvalid(phone) {
                return NewResult("fail", "手机号格式不正确", nil)
        }
        // 2、手机号合法,生成验证码,并保存到Redis中
        code := fmt.Sprintf("%06d", rand.Intn(1000000)) // 生成 6 位验证码
        ctx := context.Background()
        err := rdb.Set(ctx, LOGIN_CODE_KEY+phone, code, LOGIN_CODE_TTL).Err()
        if err != nil {
                log.Println("Error saving code to Redis:", err)
                return NewResult("fail", "验证码保存失败", nil)
        }
        // 3、发送验证码(这里只是打印日志,实际应用中需要通过短信服务发送)
        log.Printf("验证码: %s", code)

        return NewResult("ok", "", nil)
}

// 用户登录
func login(phone, code string) Result {
        // 1、判断手机号是否合法
        if isPhoneInvalid(phone) {
                return NewResult("fail", "手机号格式不正确", nil)
        }
        // 2、判断验证码是否正确
        ctx := context.Background()
        redisCode, err := rdb.Get(ctx, LOGIN_CODE_KEY+phone).Result()
        if err == redis.Nil || code != redisCode {
                return NewResult("fail", "验证码不正确", nil)
        }
        // 3、判断手机号是否是已存在的用户
        user, err := getUserByPhone(phone)
        if err != nil {
                // 用户不存在,需要注册
                user = createUserWithPhone(phone)
        }
        // 4、保存用户信息到Redis中,便于后面逻辑的判断(比如登录判断、随时取用户信息,减少对数据库的查询)
        userMap := map[string]interface{}{
                "phone":   user.Phone,
                "nickName": user.NickName,
        }

        token := uuid.New().String()
        tokenKey := LOGIN_USER_KEY + token
        err = rdb.HSet(ctx, tokenKey, userMap).Err()
        if err != nil {
                log.Println("Error saving user data to Redis:", err)
                return NewResult("fail", "用户数据保存失败", nil)
        }
        // 设置过期时间
        rdb.Expire(ctx, tokenKey, LOGIN_USER_TTL)

        return NewResult("ok", "", token)
}

// 根据手机号创建用户并保存
func createUserWithPhone(phone string) User {
        user := User{
                Phone:   phone,
                NickName: "user_" + strconv.Itoa(rand.Intn(100000)),
        }
        // 保存用户数据到数据库(这里用打印代替数据库操作)
        fmt.Printf("用户创建: %+v\n", user)
        return user
}

// 从数据库中获取用户(假设是从内存中模拟数据库)
func getUserByPhone(phone string) (User, error) {
        // 假设没有找到用户,返回错误
        return User{}, fmt.Errorf("user not found")
}

// 用户结构体
type User struct {
        Phone    string `json:"phone"`
        NickName string `json:"nickName"`
}

func main() {
        // Redis 客户端初始化
        rdb = redis.NewClient(&redis.Options{
                Addr:     "localhost:6379", // Redis 地址
                Password: "",               // 没有密码
                DB:       0,                // 默认数据库
        })

        // 发送验证码
        result := sendCode("13812345678")
        fmt.Printf("Result: %+v\n", result)

        // 用户登录
        loginResult := login("13812345678", "123456")
        fmt.Printf("Login Result: %+v\n", loginResult)
}

发送验证码

// 发送验证码
func sendCode(phone string) Result {
        // 1、判断手机号是否合法
        if isPhoneInvalid(phone) {
                return NewResult("fail", "手机号格式不正确", nil)
        }
        // 2、手机号合法,生成验证码,并保存到Redis中
        code := fmt.Sprintf("%06d", rand.Intn(1000000)) // 生成 6 位验证码
        ctx := context.Background()
        err := rdb.Set(ctx, LOGIN_CODE_KEY+phone, code, LOGIN_CODE_TTL).Err()
        if err != nil {
                log.Println("Error saving code to Redis:", err)
                return NewResult("fail", "验证码保存失败", nil)
        }
        // 3、发送验证码(这里只是打印日志,实际应用中需要通过短信服务发送)
        log.Printf("验证码: %s", code)

        return NewResult("ok", "", nil)
}

用户登录

// 用户登录
func login(phone, code string) Result {
        // 1、判断手机号是否合法
        if isPhoneInvalid(phone) {
                return NewResult("fail", "手机号格式不正确", nil)
        }
        // 2、判断验证码是否正确
        ctx := context.Background()
        redisCode, err := rdb.Get(ctx, LOGIN_CODE_KEY+phone).Result()
        if err == redis.Nil || code != redisCode {
                return NewResult("fail", "验证码不正确", nil)
        }
        // 3、判断手机号是否是已存在的用户
        user, err := getUserByPhone(phone)
        if err != nil {
                // 用户不存在,需要注册
                user = createUserWithPhone(phone)
        }
        // 4、保存用户信息到Redis中,便于后面逻辑的判断(比如登录判断、随时取用户信息,减少对数据库的查询)
        userMap := map[string]interface{}{
                "phone":   user.Phone,
                "nickName": user.NickName,
        }

        token := uuid.New().String()
        tokenKey := LOGIN_USER_KEY + token
        err = rdb.HSet(ctx, tokenKey, userMap).Err()
        if err != nil {
                log.Println("Error saving user data to Redis:", err)
                return NewResult("fail", "用户数据保存失败", nil)
        }
        // 设置过期时间
        rdb.Expire(ctx, tokenKey, LOGIN_USER_TTL)

        return NewResult("ok", "", token)
}

创建用户

// 根据手机号创建用户并保存
func createUserWithPhone(phone string) User {
        user := User{
                Phone:   phone,
                NickName: "user_" + strconv.Itoa(rand.Intn(100000)),
        }
        // 保存用户数据到数据库(这里用打印代替数据库操作)
        fmt.Printf("用户创建: %+v\n", user)
        return user
}

配置登录拦截器

与基于 Session 的登录方式不同,Session 通常在用户每次请求时自动延长有效期,而 Redis 中的 token 则需要显式地刷新,否则会在过期后导致用户突然失去登录状态。所以,为了保证 Redis 中的 token 不会因过期导致用户退出,必须为所有请求单独配置一个刷新拦截器

所以需要两个拦截器,一个是登录时的拦截器,一个是刷新 token 的拦截器

先进行登录验证拦截器,然后执行刷新 token 拦截器

登录拦截器:

// 用户结构体,模拟存储用户信息
type User struct {
        ID       int    `json:"id"`
        Username string `json:"username"`
}

// LoginInterceptor 用于判断用户是否登录
func LoginInterceptor() gin.HandlerFunc {
        return func(c *gin.Context) {
                // 获取用户信息,假设我们将用户信息存储在上下文中
                user := c.MustGet("user").(*User) // 这里的 "user" 是存储在上下文中的用户信息

                // 判断当前用户是否已登录
                if user == nil {
                        // 用户未登录,返回401未授权
                        c.JSON(http.StatusUnauthorized, gin.H{
                                "message": "Unauthorized",
                        })
                        c.Abort() // 中断后续处理
                        return
                }

                // 用户存在,继续处理请求
                c.Next()
        }
}

刷新 token 的拦截器:

// 全局变量定义 Redis 客户端
var rdb *redis.Client

const (
        LOGIN_USER_KEY = "login:user:" // token 存储的 Redis 键前缀
        LOGIN_USER_TTL = 30 * time.Minute // 设置 token 过期时间
)

// UserDTO 用户数据传输对象,模拟获取的用户信息
type UserDTO struct {
        ID       int    `json:"id"`
        Username string `json:"username"`
}

// ThreadLocalUtils 模拟 ThreadLocal 的功能
var ThreadLocalUtils = struct {
        saveUser func(user UserDTO)
}{
        saveUser: func(user UserDTO) {
                // 模拟将用户信息保存到全局变量或上下文中(可以根据实际需求修改)
                log.Printf("保存用户信息:%+v", user)
        },
}

// RefreshTokenInterceptor 刷新 Token 拦截器
func RefreshTokenInterceptor() gin.HandlerFunc {
        return func(c *gin.Context) {
                // 1. 获取 token,并判断 token 是否存在
                token := c.GetHeader("authorization")
                if token == "" {
                        // token 不存在,说明当前用户未登录,不需要刷新,直接放行
                        c.Next()
                        return
                }
                
                //到这说明 token 是存在的
                // 2. 判断用户是否存在
                tokenKey := LOGIN_USER_KEY + token
                userMap, err := rdb.HGetAll(context.Background(), tokenKey).Result()
                if err != nil || len(userMap) == 0 {
                        // 用户不存在,说明当前用户未登录,不需要刷新直接放行
                        c.Next()
                        return
                }
                
                // 到这说明 用户是存在的
                // 3. 用户存在,将用户信息保存到模拟的 ThreadLocal 中
                var userDTO UserDTO
                // 假设从 userMap 中获取的字段是 `id` 和 `username`
                userDTO.ID = 1 // 假设从 userMap 中填充数据
                userDTO.Username = userMap["username"]

                // 将用户信息存储到 ThreadLocalUtils 中
                ThreadLocalUtils.saveUser(userDTO)

                // 4. 刷新 token 有效期
                err = rdb.Expire(context.Background(), token, LOGIN_USER_TTL).Err()
                if err != nil {
                        log.Println("刷新 token 过期时间失败:", err)
                }

                c.Next()
        }
}

到此这篇关于go语言基于Session和Redis实现短信验证码登录的文章就介绍到这了,更多相关go语言 短信验证码登录内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

相关推荐