网站首页 返回列表 像“草根”一样,紧贴着地面,低调的存在,冬去春来,枯荣无恙。

软件技术-零基础-Golang操作Cookie

2020-06-10 03:59:51 admin 714

欢迎关注我的专栏( つ•̀ω•́)つ【人工智能通识】
【汇总】2019年4月专题


如何实现用户自动登录?

上一篇文章,软件技术-零基础-Golang注册验证与忘记密码
人工智能通识-2019年3月专题汇总

浏览器其实可以帮助网站记录我们浏览的信息,包括用户名,密码,或者上一次滚动页面的位置,或者任何网站开发者希望记录的信息。

这些信息其实就是很多小文件,浏览器为每个网站配一个小文件,用来记录用户浏览信息,而到底要记录什么,则由网站的开发者来决定。

这些小文件有个可爱的名字,就叫做Cookie小甜饼。

Token

如果当用户第一次登录成功的时候,我们就把用户名和密码放在Cookie里面,然后每次页面打开都自动用script执行post登录,这样可以吗?

可以的。但把用户密码放在Cookie里面很不安全,随便谁获得了这个电脑都能从网页里查看到Cookie,所以你绝对不希望网站开发者把你的银行卡密码放在Cookie里面。

另外一个方法就比较好些。
当用户登录成功的时候,我们用Golang为用户生成一个特殊的额唯一数字令牌Token,然后把这个数字Token放在Cookie里面,当用户把这个数字发送给Golang服务器程序的时候,我们再用这个数字找到对应的用户名和密码,这样我们就知道他又回来了。
这样的数字我们之前在代码里使用过,比如注册和找回密码时候返回的那个_id数字。

但还有一个问题,这个id是数据库user里面固定的数字,如果用户在新的电脑上重新用密码登录了,那么旧电脑和新电脑的Token就一样的,而且能同时登录,用户只能跑回去旧电脑注销才可以清除,以防止其他人冒用。——这很糟糕。
如果用户每次手工密码登录,我们就为他生成一个新的Token,问题就解决了。

UUID

通用唯一识别码(英语:Universally Unique
Identifier,UUID),是用于计算机体系中以识别信息数目的一个128位标识符,还有相关的术语:全局唯一标识符(GUID)。
通俗说就是有个程序不断生产字符串(或数字),每次生产的数字都不同,永远不会相同。

我们需要为每次用户手工登录创建一个独一无二的UUID。

我们使用下面的命令安装能够生产uuid的模块:

__

  1. go get github.com/satori/go.uuid
  2. go install github.com/satori/go.uuid

用法很简单,a, _ := uuid.NewV4()就能得到一串547d9f4b-05bd-4dc2-89d1-bab1c0f6ecd8这样的代码。

改进后的func Login函数代码如下:

__

  1. //Login 注册接口处理函数
  2. func Login(w http.ResponseWriter, r *http.Request) {
  3. ds := loginReqDS{}
  4. json.NewDecoder(r.Body).Decode(&ds)
  5. // //访问数据集
  6. dbc := tool.MongoDBCLient.Database("myweb").Collection("user")
  7. //验证用户邮箱是否与用户名匹配
  8. var u bson.M
  9. dbc.FindOne(context.TODO(), bson.M{"Email": ds.Email}).Decode(&u)
  10. if u["Pw"] == ds.Pw {
  11. //创建token并写入数据库
  12. uid, _ := uuid.NewV4()
  13. uids := uid.String()
  14. ctoken := tool.MongoDBCLient.Database("myweb").Collection("token")
  15. du := bson.M{"Token": uids, "Id": u["_id"], "Ts": time.Now().Unix()}
  16. ctoken.InsertOne(context.TODO(), du)
  17. //返回id,并将token写入cookie
  18. expire := time.Now().AddDate(0, 1, 0)
  19. c := http.Cookie{
  20. Name: "Token",
  21. Path: "/",
  22. Value: uids,
  23. HttpOnly: true,
  24. Expires: expire,
  25. }
  26. w.Header().Set("Set-Cookie", c.String())
  27. util.WWrite(w, 0, "登录成功", u["_id"])
  28. } else {
  29. util.WWrite(w, 1, "邮箱与用户名不匹配", nil)
  30. }
  31. return
  32. }

注意几点:

  • 我们把token_id的对应关系存储在token数据集里面了。
  • 使用http.Cookie创建要存储的数据,HttpOnly是限定只能用Golang服务器端修改,不能用网页的script修改。
  • Cookie必须注意Path路径和Expires过期时间的设置,否则可能导致只在/api路径下有效(实际这只是个接口,真实浏览器并没有这个路径,所以导致Cookie刷新后就会消失)。
  • 使用w.Header().Set设置Cookie
  • 设置Cookie和返回信息数据没有关系。

分离SetCookie.go

写入Cookie这个还是比较啰嗦的,因为以后会一直使用,我们把它单独出来放到util里面util/SetCookie.go,内容如下:

__

  1. package util
  2. import (
  3. "net/http"
  4. "time"
  5. )
  6. //SetCookie 设置Cookie,默认1月/路径
  7. func SetCookie(w http.ResponseWriter, k string, v string) {
  8. exp := time.Now().AddDate(0, 1, 0)
  9. path := "/"
  10. SetCookieExt(w, k, v, exp, path, 0)
  11. }
  12. //DelCookie 删除Cookie,MaxAge=-1
  13. func DelCookie(w http.ResponseWriter, k string) {
  14. exp := time.Now()
  15. path := "/"
  16. SetCookieExt(w, k, "", exp, path, -1)
  17. }
  18. //SetCookieExt 设置Cookie扩展版
  19. func SetCookieExt(w http.ResponseWriter, k string, v string, exp time.Time, path string, max int) {
  20. c := http.Cookie{
  21. Name: k,
  22. Path: path,
  23. Value: v,
  24. HttpOnly: true,
  25. Expires: exp,
  26. MaxAge: max,
  27. }
  28. http.SetCookie(w, &c)
  29. }

注意以下几点:

  • 由于Golang不支持函数的参数默认值(每个值必须设置),所以我们做了三个函数,一个简化版func SetCookie的只有3个参数,另一个用来删除Cookie的DelCookie只有2个参数,还有一个扩展版SetCookieExt有5个参数。
  • 删除一个Cookie只要把它的MaxAge设置为小于0。虽然你仍然可以在浏览器中看到这个Cookie,但是由于已经过期,所以读取出来是nil空的,等于不存在。
  • http.SetCookie(w, &c)可以叠加多个Cookie,而w.Header().Set("Set-Cookie", c.String())只会执行最后一个Cookie。

然后我们就可以修改login.go中的代码:

__

  1. //Login 注册接口处理函数
  2. func Login(w http.ResponseWriter, r *http.Request) {
  3. ds := loginReqDS{}
  4. json.NewDecoder(r.Body).Decode(&ds)
  5. // //访问数据集
  6. dbc := tool.MongoDBCLient.Database("myweb").Collection("user")
  7. //验证用户邮箱是否与用户名匹配
  8. var u bson.M
  9. dbc.FindOne(context.TODO(), bson.M{"Email": ds.Email}).Decode(&u)
  10. if u["Pw"] == ds.Pw {
  11. uid := u["_id"].(primitive.ObjectID).Hex()
  12. //创建token并写入数据库
  13. token, _ := uuid.NewV4()
  14. tokens := token.String()
  15. ctoken := tool.MongoDBCLient.Database("myweb").Collection("token")
  16. du := bson.M{"Token": tokens, "Id": u["_id"], "Ts": time.Now().Unix()}
  17. ctoken.InsertOne(context.TODO(), du)
  18. //返回id,写入Token和Uid
  19. util.SetCookie(w, "Token", tokens)
  20. util.SetCookie(w, "Uid", uid)
  21. util.WWrite(w, 0, "登录成功", u["_id"])
  22. } else {
  23. util.WWrite(w, 1, "邮箱与用户名不匹配", nil)
  24. }
  25. return
  26. }

注意这里我们写入了两个Cookie:TokenUid
其中uid(userId)使用uid := u["_id"].(primitive.ObjectID).Hex()把从数据库中读取的内容转成了字符string

运行代码,在页面上登录之后就可以看到新增了两个Cookie:
[图片上传失败…(image-f37ea6-1554260822526)]

中间件MiddleWare.go

app.go中,我们使用了文件服务,http.Handle("/", http.FileServer(http.Dir(webDir))把所有没明确指出处理服务的路径都指向了文件服务。(api/...都是明确指出处理服务的)。

如果我们能够在用户每次打开新页面.html的时候就自动检测他是否已经登录过,那么以后处理就容易很多。

我们目前的文件路径处理是:

中间加一个事情,我们叫它中间件,就变为:

我们先修改app.go

__

  1. //文件服务器和中间件
  2. fileHandler := http.FileServer(http.Dir(webDir))
  3. http.Handle("/", ext.MiddleWare(fileHandler))

这里我们给原来的fileHandler加了一层外套ext.MiddleWare(fileHandler)。然后我们来看app/ext/MiddleWare.go,代码如下:

__

  1. package ext
  2. import (
  3. "app/tool"
  4. "app/util"
  5. "context"
  6. "net/http"
  7. "regexp"
  8. "go.mongodb.org/mongo-driver/bson"
  9. "go.mongodb.org/mongo-driver/bson/primitive"
  10. )
  11. //MiddleWare 文件服务中间件
  12. func MiddleWare(h http.Handler) http.Handler {
  13. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  14. //仅对.html文件处理
  15. htmlRe, _ := regexp.Compile(`^.+\.html[\?]*.*$`)
  16. if !htmlRe.MatchString(r.URL.String()) {
  17. h.ServeHTTP(w, r)
  18. return
  19. }
  20. //获取Token
  21. token, _ := r.Cookie("Token")
  22. if token == nil {
  23. util.DelCookie(w, "Uid")
  24. h.ServeHTTP(w, r)
  25. return
  26. }
  27. tv := token.Value
  28. if tv == "" {
  29. util.DelCookie(w, "Uid")
  30. h.ServeHTTP(w, r)
  31. return
  32. }
  33. //如果token匹配就向Cookie添加"Uid"
  34. ctoken := tool.MongoDBCLient.Database("myweb").Collection("token")
  35. var t bson.M
  36. ctoken.FindOne(context.TODO(), bson.M{"Token": tv}).Decode(&t)
  37. uid := t["Id"]
  38. if uid != nil {
  39. uids := uid.(primitive.ObjectID).Hex()
  40. util.SetCookie(w, "Uid", uids)
  41. } else {
  42. util.DelCookie(w, "Uid")
  43. }
  44. //文件服务
  45. h.ServeHTTP(w, r)
  46. })
  47. }

注意几点:

  • 我们的这个func MiddleWare(h http.Handler) http.Handler可以看得出,进来的参数是h http.Handler,返回的也是http.Handler,就是说吃进来的和吐出来的是一样类型。这样我们在app.go里面才能确保fileHandlerext.MiddleWare(fileHandler)类型一样不会错。
  • 我们使用了正则表达式regexp.Compile(^.+.html[\?].$)来判断是否是.html文件。只对.html文件页面做自动登录处理。
  • 读取Cookie的代码是r.Cookie("Token"),但要取得它的.Value才能用。
  • 仅对检测到匹配的Token的时候增加写入Uid,对于未检测到或者不匹配的就删除掉Uid

添加autoLogin.go

我们来增加一个自动登录的接口api/autoLogin.go,每个需要自动登录检查的页面都可以调用这个地址,如果成功就返回用户的邮箱信息,如果失败就跳转到login.html页面。

__

  1. package api
  2. import (
  3. "app/tool"
  4. "app/util"
  5. "context"
  6. "encoding/json"
  7. "net/http"
  8. "go.mongodb.org/mongo-driver/bson"
  9. "go.mongodb.org/mongo-driver/bson/primitive"
  10. )
  11. type autoLoginReqDS struct {
  12. Email string
  13. }
  14. //AutoLogin 注册接口处理函数
  15. func AutoLogin(w http.ResponseWriter, r *http.Request) {
  16. ds := autoLoginReqDS{}
  17. json.NewDecoder(r.Body).Decode(&ds)
  18. //直接信任Cookie中的Uid
  19. uid, _ := r.Cookie("Uid")
  20. //没登录返回空
  21. if uid == nil || uid.Value == "" {
  22. util.WWrite(w, 1, "自动登录失败。", nil)
  23. return
  24. }
  25. //登录成功返回对象
  26. var u bson.M
  27. coll := tool.MongoDBCLient.Database("myweb").Collection("user")
  28. idobj, err := primitive.ObjectIDFromHex(uid.Value)
  29. if err != nil {
  30. util.WWrite(w, 1, "自动登录Cookie.Uid异常。", nil)
  31. return
  32. }
  33. coll.FindOne(context.TODO(), bson.M{"_id": idobj}).Decode(&u)
  34. data := map[string]string{
  35. "Email": u["Email"].(string),
  36. "Uid": uid.Value}
  37. datas, err := json.Marshal(data)
  38. if err != nil {
  39. util.WWrite(w, 1, "自动登录数据库内容异常。", nil)
  40. return
  41. }
  42. util.WWrite(w, 0, "自动登录成功。", string(datas))
  43. return
  44. }

这个代码没有很特别的地方,注意最后我们利用json.Mashal返回了较复杂一些的数据,稍后我们会在页面上读取这个内容。

改进MiddleWare.go

在上面的自动登录autoLogin.go中我们直接信任了Cookie里面的Uid。然而原则上前端网页带来的信息都是不可靠的,可以被伪造的。所以最好我们也应该在autoLogin处理之前最好也用中间件验证一下这个Cookie里面的Uid是否可靠。

我们改进MiddleWare.go

__

  1. package ext
  2. import (
  3. "app/tool"
  4. "app/util"
  5. "context"
  6. "net/http"
  7. "regexp"
  8. "go.mongodb.org/mongo-driver/bson"
  9. "go.mongodb.org/mongo-driver/bson/primitive"
  10. )
  11. //MiddleWare 文件服务中间件
  12. func MiddleWare(h http.Handler) http.Handler {
  13. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  14. //仅对.html文件处理
  15. htmlRe, _ := regexp.Compile(`^.+\.html[\?]*.*$`)
  16. if !htmlRe.MatchString(r.URL.String()) {
  17. h.ServeHTTP(w, r)
  18. return
  19. }
  20. //检查Cookie中的Uid是否合法
  21. loginCheck(w, r)
  22. //文件服务
  23. h.ServeHTTP(w, r)
  24. })
  25. }
  26. //MiddleWareAPI API中间件:检查Uid和Token的合理性
  27. func MiddleWareAPI(next http.HandlerFunc) http.HandlerFunc {
  28. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  29. //检查Cookie中的Uid是否合法
  30. loginCheck(w, r)
  31. //API服务
  32. next(w, r)
  33. })
  34. }
  35. //loginCheck 检查Cookie中的Uid是否合法
  36. func loginCheck(w http.ResponseWriter, r *http.Request) {
  37. //获取Token
  38. token, _ := r.Cookie("Token")
  39. if token == nil {
  40. util.DelCookie(w, "Uid")
  41. return
  42. }
  43. tv := token.Value
  44. if tv == "" {
  45. util.DelCookie(w, "Uid")
  46. return
  47. }
  48. //如果token匹配就向Cookie添加"Uid"
  49. ctoken := tool.MongoDBCLient.Database("myweb").Collection("token")
  50. var t bson.M
  51. ctoken.FindOne(context.TODO(), bson.M{"Token": tv}).Decode(&t)
  52. uid := t["Id"]
  53. if uid != nil {
  54. uids := uid.(primitive.ObjectID).Hex()
  55. util.SetCookie(w, "Uid", uids)
  56. } else {
  57. util.DelCookie(w, "Uid")
  58. }
  59. }

注意几点:

  • 我们把验证用户登录的方法单独拉出来变为loginCheck
  • 我们再原有文件处理中间件的基础上新增了API版本MiddleWareAPI
  • MiddleWareAPI其实比较简单,它吃http.HandlerFunc,也返回http.HandlerFunc,只是中间我们插入了loginCheck(w,r)

然后我们终于可以到app.go设置服务路径了:

__

  1. http.HandleFunc("/api/AutoLogin", ext.MiddleWareAPI(api.AutoLogin))

改进index.html

我们来改一下index.html,让首页尝试自动登录,如果登录失败就跳转到登录页面,下面是index.html的完整代码:

__

  1. <!doctype html>
  2. <html lang="zh-cmn-Hans">
  3. <head>
  4. <!-- Required meta tags -->
  5. <meta charset="utf-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  7. <!-- Bootstrap CSS -->
  8. <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css"
  9. integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
  10. <title>我的站点</title>
  11. </head>
  12. <body>
  13. <div class="row justify-content-center" style="margin-top:100px;margin-bottom:20px">
  14. <h4>~欢迎您来到我的网站~</h4>
  15. </div>
  16. <div class="row justify-content-center">
  17. <div id='uEmail'>正在为您登录</div>
  18. </div>
  19. <!-- Optional JavaScript -->
  20. <!-- jQuery first, then Popper.js, then Bootstrap JS -->
  21. <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
  22. <script src="https://cdn.bootcss.com/popper.js/1.12.9/umd/popper.min.js"></script>
  23. <script src="https://cdn.bootcss.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
  24. </body>
  25. <script type="text/javascript">
  26. function autoLogin() {
  27. $.post('/api/AutoLogin', function (res) {
  28. obj = JSON.parse(res.Data);
  29. if (obj && obj['Email']) {
  30. $('#uEmail').html(obj['Email'])
  31. }else{
  32. $('#uEmail').html("自动登录失败,正在为您跳转...")
  33. setTimeout(() => {
  34. location='/page/login.html'
  35. }, 1000);
  36. }
  37. }, 'json')
  38. }
  39. autoLogin()
  40. </script>
  41. </html>

注意以下几点:

  • 我们在结尾自动执行了autologin()
  • 因为Golang传过来的都是string,所以我们obj = JSON.parse(res.Data)把string转为对象,这样就可以obj['Email']获取数据了。
  • 使用location='/page/login.html'方法跳转页面。
  • 使用setTimeout(() => {...}, 1000)延迟1秒再跳转。

好了,可以运行测试了,正常的话如果还没登录(或者把Cookie删掉了),那么首页就会为你跳转到登录页面,正常登陆之后,再回到首页就可以看到自己的邮箱了:

小结

  • Cookie就是浏览器为每个网站的开发者准备的用于记录用户信息的小文件。可以用Golang直接操作Cookie。
  • Token是我们在用户每次手工登录时候创建的唯一字符串,和用户的Uid是对应的,也对应到数据库中的条目。注意可能多个Token对应一个Uid,但不可能多个Uid对应同一个Token。
  • 中间件概念可以让我们为多个路径处理服务插入同一个处理程序,比如我们为每个.html文件服务都插入了验证Cookie中Token和Uid的功能,同样我们也为api/Autologin路径插入了这个验证,如果需要的话任何一个api处理都可以先加上这个验证以确保Uid可靠性。
  • 别忘了提及到Git再提交到Github。

>

虽然还有一些链接没有添加,但似乎登录注册功能基本完成了。但还有一个严重缺陷,那就是我们一直把用户的密码反复的明文传输,如果被坏人中间截获了就不好了,当然,你的网站数据库中直接明明白白记录着这些重要的密码,本身就是非常不负责的,下一篇我们介绍如何解决这个缺陷。


欢迎关注我的专栏( つ•̀ω•́)つ【人工智能通识】


每个人的智能新时代

如果您发现文章错误,请不吝留言指正;
如果您觉得有用,请点喜欢;
如果您觉得很有用,欢迎转载~


END

转载文章,原文链接: 软件技术-零基础-Golang操作Cookie

关键字词golangCookie

分享到:

如需留言,请 登录,没有账号?请 注册

0 条评论 0 人参与

顶部 底部