使用 gorilla/sessions 实现一个极简的短信认证后端模块
背景 :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
写一个请求认证验证码的request
如何证明是 session 和 cookie 起作用了?
看到这里可能有同学会问, 你怎么知道认证短信验证码的 request 一定绑定的是 Tom 这个人的验证码呢? 你怎么确定认证短信验证码的 request 一定绑定的刚刚请求发送短信验证码的requst呢? 有没有可能我在别的电脑开一个 request, 只要验证码一样也可以通过验证?
答案是没有可能, 除非别的request也携带相同的cookie。 因为 认证短信验证码的 request中携带了 cookie, 这个 cookie 来源于 刚刚请求发送短信的 response。不信我们打开postman查看 :
再打开请求 校验短信验证码的 request 查看 cookie:
可以发现这两个 cookie是一样的。这里的 user-login-session 就是刚才代码中的 userLoginName 变量。 我们可以点击 user-login-session 旁边的 叉叉, 将这个 cookie 给删掉, 再次请求,观察结果:
可以发现无论输入什么, 都会返回 Sms Code is expire, 控制台输出:
Failed to get session.
这是因为没有携带 cookie, 无法获取 session。 当然如果超过了验证码的有效时间, 即使cookie正确, 也会获取 session 失败。
至此,我们已经实现了一个极简版的短信认证后端模块, 还有许多可以优化的地方, 比如如何对请求发送短信验证码和校验短信验证码进行限流,防止重复获取短信验证码和暴力破解短信验证码,这里就不做展示了hhh
转载自:https://juejin.cn/post/7168118122581327908