浅见 發表於 2025-11-16 15:05:59

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

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">基于 session 实现短信验证码登录</a></li><ul class="second_class_ul"><li><a href="#_lab2_0_0">短信验证码登录</a></li><ul class="third_class_ul"><li><a href="#_label3_0_0_0">发送验证码</a></li><li><a href="#_label3_0_0_1">用户登录</a></li><li><a href="#_label3_0_0_2">创建用户</a></li></ul><li><a href="#_lab2_0_1">登录拦截器</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_0_2">数据脱敏</a></li><ul class="third_class_ul"></ul></ul><li><a href="#_label1">Session 集群共享问题</a></li><ul class="second_class_ul"></ul><li><a href="#_label2">基于 Redis 实现短信验证码登录</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_3">短信验证登录</a></li><ul class="third_class_ul"><li><a href="#_label3_2_3_3">发送验证码</a></li><li><a href="#_label3_2_3_4">用户登录</a></li><li><a href="#_label3_2_3_5">创建用户</a></li></ul><li><a href="#_lab2_2_4">配置登录拦截器</a></li><ul class="third_class_ul"></ul></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>基于 session 实现短信验证码登录</h2>
<div class="jb51code"><pre class="brush:go;">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 &lt; 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").(mapinterface{}) // Example: Use Gin session middleware to manage session
      session = 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"`
                Codestring `json:"code"`
      }
      if err := c.BindJSON(&amp;loginForm); err != nil {
                c.JSON(http.StatusBadRequest, Failure("请求参数错误"))
                return
      }

      phone := loginForm.Phone
      code := loginForm.Code
      session := c.MustGet("session").(mapinterface{}) // 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.(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 = 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 &amp;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(mapinterface{}))
                c.Next()
      })

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

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

</pre></div>
<p class="maodian"><a name="_lab2_0_0"></a></p><h3>短信验证码登录</h3>
<p><strong>前期准备:</strong><br />为了提高代码的可读性、可维护性和一致性,方便后续修改和减少出错的机会,把几个会用到的字符串赋值给常量</p>
<div class="jb51code"><pre class="brush:go;">const (
      VERIFY_CODE = "verify_code"
      LOGIN_CODE= "login_code"
      LOGIN_USER= "login_user"
      USER_NICK_NAME_PREFIX = "user_"
)
</pre></div>
<p>定义结构体,接收前端数据</p>
<div class="jb51code"><pre class="brush:go;">//用户结构体
type User struct {
      Phone    string `json:"phone"`
      NickName string `json:"nick_name"`
}
</pre></div>
<p>定义封装响应数据的数据结构</p>
<div class="jb51code"><pre class="brush:go;">type Result struct {
      Code    int    `json:"code"`
      Message string `json:"message"`
      Data    any    `json:"data"`
}
</pre></div>
<p>使用 <code>Success</code> 和 <code>Failure</code> 函数 构建标准化的响应格式,<strong>封装响应的内容</strong>,简化返回结果的构建过程,使得返回的数据格式统一且易于维护。</p>
<div class="jb51code"><pre class="brush:go;">func Success(message string, data any) Result {
      return Result{Code: 200, Message: message, Data: data}
}
</pre></div>
<div class="jb51code"><pre class="brush:go;">func Failure(message string) Result {
      return Result{Code: 400, Message: message, Data: nil}
}
</pre></div>
<p class="maodian"><a name="_label3_0_0_0"></a></p><p class="maodian"><a name="_label3_2_3_3"></a></p><h4>发送验证码</h4>
<p>想一下平时需要验证的过程,填写手机号 --&gt; 接收验证码 --&gt; 输入验证码<br />所以它的一个基本流程:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202511/2025111615040416.png" /></p>
<p>首先,输入了手机号,需要<strong>判断手机号合不合法</strong></p>
<div class="jb51code"><pre class="brush:go;">// 判断手机号是否合法,若手机号不合法,返回 True
func IsPhoneInvalid(phone string) bool {
    reg := regexp.MustCompile(`^1\d{9}$`)
    return !reg.MatchString(phone)
}
</pre></div>
<p>解释:</p>
<div class="jb51code"><pre class="brush:go;">regexp.MustCompile(^1\d{9}$):
</pre></div>
<p>使用正则表达式 ^1\d{9}$</p>
<ul><li>^ 开始匹配字符串</li><li>1手机号第一位必须是1</li><li> 手机号第二位必须在 3到9 之间</li><li>\d{9}后面必须是九位数字,\d 表示 0-9, {9} 表示重复九次</li><li>$ 表示匹配字符串结束</li></ul>
<div class="jb51code"><pre class="brush:go;">reg.MatchString(phone):
</pre></div>
<p>reg 是用来&ldquo;匹配手机号是否合法&rdquo;的工具</p>
<p><strong>随机生成六位数验证码</strong></p>
<div class="jb51code"><pre class="brush:go;">func RandomNumbers(length int) string {
      rand.Seed(time.Now().UnixNano())
      code := ""
      for i := 0; i &lt; length; i++ {
                code += fmt.Sprintf("%d", rand.Intn(10))
      }
      return code
}
</pre></div>
<p class="maodian"><a name="_label3_0_0_1"></a></p><p class="maodian"><a name="_label3_2_3_4"></a></p><h4>用户登录</h4>
<div class="jb51code"><pre class="brush:go;">// 用户登录
func Login(c *gin.Context) {
      //定义一个临时结构体,用来接收前端请求的 JSON 数据。
      var loginForm struct {
                Phone string `json:"phone"`   //接收手机号
                Codestring `json:"code"`    //接收验证码
      }
      //将请求的 JSON 数据绑定到 loginForm 结构体中。
      //BindJSON 是 Gin 框架中的方法,自动将请求体中的 JSON 数据转换成结构体形式,赋值给 loginForm
      if err := c.BindJSON(&amp;loginForm); err != nil {
                c.JSON(http.StatusBadRequest, Failure("请求参数错误"))
                return
      }

      phone := loginForm.Phone
      code := loginForm.Code
      session := c.MustGet("session").(mapinterface{}) // Example: Use Gin session middleware to manage session

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

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

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

      // 将用户信息保存到 session 中
      session = user
      c.JSON(http.StatusOK, Success("登录成功", user))
}
</pre></div>
<p class="maodian"><a name="_label3_0_0_2"></a></p><p class="maodian"><a name="_label3_2_3_5"></a></p><h4>创建用户</h4>
<div class="jb51code"><pre class="brush:go;">// 根据电话号码创建一个新用户
func createUserWithPhone(phone string) *User {
      // 为用户生成一个随机昵称
      nickName := USER_NICK_NAME_PREFIX + RandomNumbers(10)
      return &amp;User{Phone: phone, NickName: nickName}
}
</pre></div>
<p class="maodian"><a name="_lab2_0_1"></a></p><h3>登录拦截器</h3>
<div class="jb51code"><pre class="brush:go;">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(mapinterface{})

// LoginInterceptor 登录拦截器
func LoginInterceptor() gin.HandlerFunc {
      return func(c *gin.Context) {
                // 获取 session 中的用户信息
                session := c.MustGet("session").(mapinterface{})
                user, exists := session.(*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(mapinterface{})
                // 假设用户已经登录
                session = &amp;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")
}
</pre></div>
<p>基本流程</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202511/2025111615040414.png" /></p>
<p>可以用中间件来实现拦截器的功能</p>
<div class="jb51code"><pre class="brush:go;">func LoginInterceptor() gin.HandlerFunc {
      return func(c *gin.Context) {
                // 获取 session 中的用户信息
                session := c.MustGet("session").(mapinterface{})
               
                // 判断 用户是否存在
                user, exists := session.(*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() // 执行后续处理
      }
}
</pre></div>
<p class="maodian"><a name="_lab2_0_2"></a></p><h3>数据脱敏</h3>
<p>为了 <strong>保护敏感数据,防止信息泄露并确保数据的隐私安全</strong>,要进行 <code>数据脱敏</code></p>
<div class="jb51code"><pre class="brush:go;">// UserDTO 用户数据
type UserDTO struct {
      ID       int64`json:"id"`      // 用户 ID
      NickName string `json:"nickName"`// 用户昵称
      Icon   string `json:"icon"`      // 用户头像
}
</pre></div>
<p>例如</p>
<ol><li>可以将 <strong>NickName</strong> 或 <strong>Icon</strong> 中的部分敏感信息进行替换或隐藏</li></ol>
<div class="jb51code"><pre class="brush:go;">func (u UserDTO) String() string {
    // 将昵称进行脱敏,显示前两个字符,后面用 * 替换
    maskedNickName := u.NickName
    if len(u.NickName) &gt; 2 {
      maskedNickName = u.NickName[:2] + "****"
    }

    // 返回脱敏后的字符串表示
    return fmt.Sprintf("UserDTO{id: %d, nickName: %s, icon: %s}", u.ID, maskedNickName, u.Icon)
}
</pre></div>
<ol start="2"><li>完全隐藏某些字段(例如 ID 或 Icon)(比如 在分享或展示数据时,只保留不敏感的部分)</li></ol>
<div class="jb51code"><pre class="brush:go;">func (u UserDTO) String() string {
    return fmt.Sprintf("UserDTO{nickName: %s}", u.NickName)// 只显示昵称
}
</pre></div>
<ol start="3"><li>如果是日期、年龄等信息,可以通过泛化的方式处理。例如,将精确的年龄替换为一个范围:</li></ol>
<div class="jb51code"><pre class="brush:go;">func (u UserDTO) String() string {
    age := 28 // 假设是用户年龄
    ageRange := "20-30" // 泛化处理
    return fmt.Sprintf("UserDTO{age: %s, nickName: %s}", ageRange, u.NickName)
}
</pre></div>
<p class="maodian"><a name="_label1"></a></p><h2>Session 集群共享问题</h2>
<p>假设有一个购物网站,这个网站的背后 有很多服务器在后端维护数据<br />用户第一次登录,访问服务器A ,登录信息存储在服务器A 上;下一次登录,用户的请求被分配到了服务器B ,而服务器A 上的Session 并没有同步到服务器B 上,服务器B 上不存在用户的Session 信息,导致用户数据丢失,可能需要重新登录<br />这就是<strong>会话丢失</strong>问题</p>
<p>如果用户在服务器 A 上修改了 Session 数据(比如更改了购物车的内容),这些修改不会自动同步到其他服务器上(如服务器 B)。当用户请求到 B 时,看到的还是旧的 Session 数据,造成数据不一致<br />这就是<strong>数据不一致</strong>问题</p>
<p><strong>解决方案:</strong><br /><strong>集中式存储(如 Redis):</strong></p>
<ul><li>使用 Redis 作为共享的会话存储。所有服务器都将用户的 Session 数据存储到 Redis 中,确保所有服务器可以访问同一份数据,不管用户请求到哪台服务器,都能获得一致的会话信息。</li></ul>
<p><strong>Session 复制:</strong></p>
<ul><li>在某些情况下,服务器之间可以复制 Session 数据。这样,当用户请求被路由到其他服务器时,已经存在的 Session 数据可以同步过来。</li><li>缺点就是会增加服务器的额外内存开销</li></ul>
<p><strong>这里我们使用 Redis 解决</strong></p>
<p>Redis 和 Session 对比</p>
<table><thead><tr><th>特性</th><th>传统Session存储</th><th>Redis</th></tr></thead><tbody><tr><td>存储位置</td><td>存储在服务器内存或数据库中</td><td>存储在 Redis 服务器的内存中</td></tr><tr><td>高可用性与持久化</td><td>无高可用,数据丢失风险大</td><td>支持高可用和数据持久化(RDB/AOF)</td></tr><tr><td>性能</td><td>单服务器内存快,数据库较慢</td><td>高性能,能够处理大规模并发请求</td></tr><tr><td>扩展性</td><td>不适合分布式,扩展困难</td><td>水平扩展,支持 Redis 集群和分片</td></tr><tr><td>数据同步与共享</td><td>需要外部机制来同步数据</td><td>集中式存储,所有服务器共享数据</td></tr></tbody></table>
<p class="maodian"><a name="_label2"></a></p><h2>基于 Redis 实现短信验证码登录</h2>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202511/2025111615040482.png" /></p>
<p>使用hash 存储用户信息</p>
<p>同样地,首先定义这几个数据结构</p>
<div class="jb51code"><pre class="brush:go;">// 创建返回结果
func NewResult(status string, message string, data interface{}) Result {
      return Result{
                Status:status,
                Message: message,
                Data:    data,
      }
}
</pre></div>
<p class="maodian"><a name="_lab2_2_3"></a></p><h3>短信验证登录</h3>
<div class="jb51code"><pre class="brush:go;">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 {
      Statusstring `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 := mapinterface{}{
                "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(&amp;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)
}
</pre></div>
<h4>发送验证码</h4>
<div class="jb51code"><pre class="brush:go;">// 发送验证码
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)
}
</pre></div>
<h4>用户登录</h4>
<div class="jb51code"><pre class="brush:go;">// 用户登录
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 := mapinterface{}{
                "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)
}
</pre></div>
<h4>创建用户</h4>
<div class="jb51code"><pre class="brush:go;">// 根据手机号创建用户并保存
func createUserWithPhone(phone string) User {
      user := User{
                Phone:   phone,
                NickName: "user_" + strconv.Itoa(rand.Intn(100000)),
      }
      // 保存用户数据到数据库(这里用打印代替数据库操作)
      fmt.Printf("用户创建: %+v\n", user)
      return user
}
</pre></div>
<p class="maodian"><a name="_lab2_2_4"></a></p><h3>配置登录拦截器</h3>
<p>与基于 <strong>Session</strong> 的登录方式不同,<strong>Session</strong> 通常在用户每次请求时自动延长有效期,而 <strong>Redis</strong> 中的 token 则需要显式地刷新,否则会在过期后导致用户突然失去登录状态。所以,为了保证 <strong>Redis</strong> 中的 token 不会因过期导致用户退出,必须为所有请求单独配置一个刷新拦截器</p>
<p>所以需要两个拦截器,一个是登录时的拦截器,一个是刷新 token 的拦截器</p>
<p>先进行登录验证拦截器,然后执行刷新 token 拦截器</p>
<p><strong>登录拦截器:</strong></p>
<div class="jb51code"><pre class="brush:go;">// 用户结构体,模拟存储用户信息
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()
      }
}
</pre></div>
<p><strong>刷新 token 的拦截器:</strong></p>
<div class="jb51code"><pre class="brush:go;">// 全局变量定义 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()
      }
}
</pre></div>
頁: [1]
查看完整版本: go语言基于Session和Redis实现短信验证码登录