likes
comments
collection

使用 gorilla/sessions 实现一个极简的短信认证后端模块

作者站长头像
站长
· 阅读数 15

背景 :session 和 cookie 是 web 开发中经常使用到的两个技术, 其经常用于存储一些需要临时保存的中间变量。在 go 语言中, gorilla/sessions 库对 session和 cookie 均提供了很好的支持。特此记录一贴使用gorilla/sessions 库实现一个极简的 短信认证后端模块。

新建一个工程

创建新目录 session_cookie, 在该目录下打开命令行输入:

go mod init session_cookie
go mod tidy

这样就在 session_cookie下面创建了一个 go 工程

创建一个初始化 session 的文件

在session_cookie/main 目录下创建 一个 session_center.go 文件:

package session_cookie

import (
   "github.com/gorilla/sessions"
   "os"
)

var cookieStore *sessions.CookieStore

var userLoginName = "user-login-session"

 
// 传入一个符串是用于 session 的认证加密 
func Init() {
   cookieStore = sessions.NewCookieStore(
      []byte(os.Getenv("SESSION_KEY")),
   )

   cookieStore.Options = &sessions.Options{
      HttpOnly: true,
      MaxAge:   60 * 1, // 设置过期时间, 1分钟过期
   }

}

文件主要定义了 一个 userLoginName 和 cookieStore 变量。 userLoginName 就是一个字符串而已, 在后面会用作 session 的名字, 而 cookieStore 会用于获取 session。 在 sessions.NewCookieStore 时需要传进去一个字符串, 这个字符串是用于加密session的, 一般不写在代码里面, 而是有环境变量生成。 sessions,Option 还可以设置 session过期的时间,单位为秒。

写一个发送短信的后端

在session_cookie/main 目录下创建 一个 send_sms.go 文件:

package session_cookie

import (
   "encoding/json"
   "fmt"
   "io/ioutil"
   "math/rand"
   "net/http"
   "time"
)

type SendSmsReq struct {
   Username string `json:"username"`
}

func SendSmsHandler(w http.ResponseWriter, r *http.Request) {

   // 如果不是 POST 方法, 直接 return
   if r.Method != http.MethodPost {
      w.Write([]byte("Expect Post method"))
      return
   }

   // 将请求Body进行解析
   bodyBytes, err := ioutil.ReadAll(r.Body)
   if err != nil {
      fmt.Println("Fail to read request body")
      w.Write([]byte("Fail to read request body"))
      return
   }

   var sendReq SendSmsReq
   err = json.Unmarshal(bodyBytes, &sendReq)
   if err != nil {
      fmt.Println("Fail to convert request body into  SendSmsReq ")
      w.Write([]byte("Fail to convert request body into  SendSmsReq "))
      return
   }

   // 生成6位随机码
   rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
   smsCode := fmt.Sprintf("%06v", rnd.Int31n(1000000))

   // 将6位随机码存入 session 中
   session, err := cookieStore.Get(r, userLoginName)
   if err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
      w.Write([]byte("Send sms code faild "))
      return
   }

   session.Values["SmsCode"] = smsCode
   err = session.Save(r, w)
   if err != nil {
      w.Write([]byte("Failed to save session"))
      fmt.Println(err)
      return
   }

   // 返回验证码
   w.Write([]byte(sendReq.Username + " 的验证码是 : " + smsCode))
}

函数非常简单, 先是判断一下是否为 post 请求, 再解析 body, 获取 session, 将短信随机码放入session, 最后return出去。session.Value是一个map,以键值对的方式存储数据。当然在实际的短信发送模块中, 是不会将短信码return 出去(这样还认证个锤子), 而是由短信运营商发到用户手机上, 这里为了后面使用 postman 演示, 所以直接返回。

写一个短信验证的后端

在session_cookie/main 目录下创建 一个 login.go 文件:

package session_cookie

import (
   "encoding/json"
   "fmt"
   "io/ioutil"
   "net/http"
)

type LoginReq struct {
   Passwd string `json:"passwd"`
}

type LoginRsp struct {
   msg string `json:"msg"`
}

func LoginHandler(w http.ResponseWriter, r *http.Request) {

   // 如果不是 post 方法
   if r.Method != http.MethodPost {
      w.Write([]byte("Expect Post method"))
      return
   }

   // 获取 request.Body
   bodyBytes, err := ioutil.ReadAll(r.Body)
   if err != nil {
      fmt.Println("Fail to read request body")
      w.Write([]byte("Fail to read request body"))
      return
   }

   var loginReq LoginReq
   err = json.Unmarshal(bodyBytes, &loginReq)
   if err != nil {
      fmt.Println("Fail to convert request body into LoginReq ")
      w.Write([]byte("Fail to convert request body into LoginReq "))
      return
   }

   // 获取 session
   session, err := cookieStore.Get(r, userLoginName)
   if err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
      return
   }

   smsCode, ok := session.Values["SmsCode"]
   if !ok {
      fmt.Println("Failed to get session. ")
      w.Write([]byte("Sms Code is expire. "))
      return
   }

   if loginReq.Passwd != smsCode {
      fmt.Println("Sms Code is wrong, Login Failed  ")
      w.Write([]byte("Sms Code is wrong, Login Failed "))
      return
   }

   // 校验已通过, 删除session中的验证码
   delete(session.Values, "SmsCode")
   err = session.Save(r, w)
   if err != nil {
      w.Write([]byte("Failed to save session"))
      fmt.Println(err)
      return
   }

   // 登陆成功
   fmt.Println("Login Success ")
   w.Write([]byte("Login Success "))
   return
}

函数也非常简单, 先是判断一下是否为 post 请求, 再解析 body, 获取 session, 再拿出session中存储的验证码和前端传来的验证码进行比对一下, 如果相等则认证通过, 否则认证失败。如果获取session的失败了, 说明这个session 已经过期了, 在业务层面上来说就是短信码过期了,直接返回验证码失效错误。

创建一个主函数

创建 session_cookie/main 目录, 再创建 一个main.go 文件:

 
package main

import (
   "fmt"
   "net/http"
   "session_cookie"
)

func main() {
   session_cookie.Init()
   http.HandleFunc("/login", session_cookie.LoginHandler)
   http.HandleFunc("/sendSms", session_cookie.SendSmsHandler)

   errChan := make(chan error)
   go func() {
      errChan <- http.ListenAndServe(":2000", nil)
   }()
   err := <-errChan
   if err != nil {
      fmt.Println("Server stop running.")
   }

}
 

主函数非常简单, 路由两个后端的handler, 监听2000端口,初始化 session。

用 postman 测试

写一个请求发送短信的request

使用 gorilla/sessions 实现一个极简的短信认证后端模块

写一个请求认证验证码的request

使用 gorilla/sessions 实现一个极简的短信认证后端模块

如何证明是 session 和 cookie 起作用了?

看到这里可能有同学会问, 你怎么知道认证短信验证码的 request 一定绑定的是 Tom 这个人的验证码呢? 你怎么确定认证短信验证码的 request 一定绑定的刚刚请求发送短信验证码的requst呢? 有没有可能我在别的电脑开一个 request, 只要验证码一样也可以通过验证?

答案是没有可能, 除非别的request也携带相同的cookie。 因为 认证短信验证码的 request中携带了 cookie, 这个 cookie 来源于 刚刚请求发送短信的 response。不信我们打开postman查看 :

使用 gorilla/sessions 实现一个极简的短信认证后端模块

再打开请求 校验短信验证码的 request 查看 cookie:

使用 gorilla/sessions 实现一个极简的短信认证后端模块 可以发现这两个 cookie是一样的。这里的 user-login-session 就是刚才代码中的 userLoginName 变量。 我们可以点击 user-login-session 旁边的 叉叉, 将这个 cookie 给删掉, 再次请求,观察结果:

使用 gorilla/sessions 实现一个极简的短信认证后端模块

可以发现无论输入什么, 都会返回 Sms Code is expire, 控制台输出:

Failed to get session. 

这是因为没有携带 cookie, 无法获取 session。 当然如果超过了验证码的有效时间, 即使cookie正确, 也会获取 session 失败。

至此,我们已经实现了一个极简版的短信认证后端模块, 还有许多可以优化的地方, 比如如何对请求发送短信验证码和校验短信验证码进行限流,防止重复获取短信验证码和暴力破解短信验证码,这里就不做展示了hhh