前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于 Go 语言开发在线论坛(四):通过 Cookie + Session 实现用户认证

基于 Go 语言开发在线论坛(四):通过 Cookie + Session 实现用户认证

作者头像
学院君
发布2020-04-02 15:55:52
5220
发布2020-04-02 15:55:52
举报
文章被收录于专栏:学院君的专栏学院君的专栏

1、编写全局辅助函数

在此之前,我们现在 handlers 目录下创建一个 helper.go 文件,用于定义一些全局辅助函数(主要用在处理器中):

代码语言:javascript
复制
package handlers

import (
    "errors"
    "fmt"
    "github.com/xueyuanjun/chitchat/models"
    "html/template"
    "net/http"
)

// 通过 Cookie 判断用户是否已登录
func session(writer http.ResponseWriter, request *http.Request) (sess models.Session, err error) {
    cookie, err := request.Cookie("_cookie")
    if err == nil {
        sess = models.Session{Uuid: cookie.Value}
        if ok, _ := sess.Check(); !ok {
            err = errors.New("Invalid session")
        }
    }
    return
}

// 解析 HTML 模板(应对需要传入多个模板文件的情况,避免重复编写模板代码)
func parseTemplateFiles(filenames ...string) (t *template.Template) {
    var files []string
    t = template.New("layout")
    for _, file := range filenames {
        files = append(files, fmt.Sprintf("views/%s.html", file))
    }
    t = template.Must(t.ParseFiles(files...))
    return
}

// 生成响应 HTML
func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) {
    var files []string
    for _, file := range filenames {
        files = append(files, fmt.Sprintf("views/%s.html", file))
    }

    templates := template.Must(template.ParseFiles(files...))
    templates.ExecuteTemplate(writer, "layout", data)
}

// 返回版本号
func Version() string {
    return "0.1"
}

目前提供了版本信息,判断用户是否登录,HTML 模板的解析与生成等逻辑,我们将 HTML 模板解析与生成逻辑提取出来,主要是为了避免重复编写类似的模板代码,比如现在,我们可以将 handlers/index.go 中的 Index 方法改写如下:

代码语言:javascript
复制
func Index(w http.ResponseWriter, r *http.Request) {
    threads, err := models.Threads();
    if err == nil {
        generateHTML(w, threads, "layout", "navbar", "index")
    }
}

是不是看起来简单多了,更重要的是提高了代码的复用性。

session 函数中,通过从请求中获取指定 Cookie 字段里面存放的 Session ID,然后从 Session 存储器(这里存储驱动是数据库)查询对应 Session 是否存在来判断用户是否已认证,如果已认证则返回的 sess 不为空。

2、用户认证相关处理器

1)编写处理器代码

接下来,在 handlers 目录下创建一个 auth.go 来存放用户认证相关处理器:

代码语言:javascript
复制
package handlers

import (
    "fmt"
    "github.com/xueyuanjun/chitchat/models"
    "net/http"
)

// GET /login
// 登录页面
func Login(writer http.ResponseWriter, request *http.Request) {
    t := parseTemplateFiles("auth.layout", "navbar", "login")
    t.Execute(writer, nil)
}

// GET /signup
// 注册页面
func Signup(writer http.ResponseWriter, request *http.Request) {
    generateHTML(writer, nil, "auth.layout", "navbar", "signup")
}

// POST /signup
// 注册新用户
func SignupAccount(writer http.ResponseWriter, request *http.Request) {
    err := request.ParseForm()
    if err != nil {
        fmt.Println("Cannot parse form")
    }
    user := models.User{
        Name:     request.PostFormValue("name"),
        Email:    request.PostFormValue("email"),
        Password: request.PostFormValue("password"),
    }
    if err := user.Create(); err != nil {
        fmt.Println("Cannot create user")
    }
    http.Redirect(writer, request, "/login", 302)
}

// POST /authenticate
// 通过邮箱和密码字段对用户进行认证
func Authenticate(writer http.ResponseWriter, request *http.Request) {
    err := request.ParseForm()
    user, err := models.UserByEmail(request.PostFormValue("email"))
    if err != nil {
        fmt.Println("Cannot find user")
    }
    if user.Password == models.Encrypt(request.PostFormValue("password")) {
        session, err := user.CreateSession()
        if err != nil {
            fmt.Println("Cannot create session")
        }
        cookie := http.Cookie{
            Name:     "_cookie",
            Value:    session.Uuid,
            HttpOnly: true,
        }
        http.SetCookie(writer, &cookie)
        http.Redirect(writer, request, "/", 302)
    } else {
        http.Redirect(writer, request, "/login", 302)
    }
}

// GET /logout
// 用户退出
func Logout(writer http.ResponseWriter, request *http.Request) {
    cookie, err := request.Cookie("_cookie")
    if err != http.ErrNoCookie {
        fmt.Println("Failed to get cookie")
        session := models.Session{Uuid: cookie.Value}
        session.DeleteByUUID()
    }
    http.Redirect(writer, request, "/", 302)
}

上述代码中定义了用户注册、登录、退出相关业务逻辑,非常简单,和 Laravel 认证脚手架生成的默认认证相关控制器非常相似。

2)用户注册

用户注册逻辑比较简单,无非是填写注册表单(Signup 处理器方法),提交注册按钮将用户信息保存到数据库(SignupAccount 处理器方法)。

3)用户登录

接下来,服务端会将用户重定向到登录页面(Login 处理器方法),用户填写登录表单后,就可以通过 Authenticate 处理器方法执行认证操作。

用户认证是基于 Cookie + Session 实现的,Session 的数据结构如下所示:

代码语言:javascript
复制
type Session struct {
    Id        int
    Uuid      string
    Email     string
    UserId    int
    CreatedAt time.Time
}

通过 Uuid 字段可以唯一标识这个 Session,因此可以看作是对外可见的全局 Session ID,在客户端 Cookie 存储的 Session ID 也是这个 Uuid。当用户认证成功之后,就会创建 Session,有了 Session 之后,就可以创建 Cookie 并写到响应中:

代码语言:javascript
复制
cookie := http.Cookie{
    Name:     "_cookie",
    Value:    session.Uuid,
    HttpOnly: true,
}
http.SetCookie(writer, &cookie)

这样,下次用户访问在线论坛页面就会在请求头中带上包含 Session ID 的 Cookie,服务端通过解析这个 Uuid 并查询 Session 存储器(这里存储驱动是数据库)判断该用户 Session 是否存在,如果存在则用户认证通过,也就是前面辅助函数 session 所做的事情。

4)用户退出

上述 Cookie 未设置过期时间,所以生命周期和 Session 一致,当浏览器关闭时,Cookie 就自动删除,下次打开浏览器需要重新认证。

最后用户退出处理器方法 Logout 方法则是方便用户主动退出,当用户点击退出按钮,可以执行该处理器方法销毁当前用户 Session 和认证 Cookie,并将用户重定向到首页。

3、用户认证相关视图模板

定义好认证处理器后,我们来编写与认证相关的视图模板,主要是登录页面和注册页面,在 views 目录下新增 login.html 编写登录页面:

代码语言:javascript
复制
{{ define "content" }}

<form class="form-signin center" role="form" action="/authenticate" method="post">
  <h2 class="form-signin-heading">
    <i class="fa fa-comments-o">
      ChitChat
    </i>
  </h2>
  <input type="email" name="email" class="form-control" placeholder="Email address" required autofocus>
  <input type="password" name="password" class="form-control" placeholder="Password" required>
  <br/>
  <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
  <br/>
  <a class="lead pull-right" href="/signup">Sign up</a>
</form>

{{ end }}

然后创建 signup.html 编写注册页面:

代码语言:javascript
复制
{{ define "content" }}

<form class="form-signin" role="form" action="/signup_account" method="post">
  <h2 class="form-signin-heading">
    <i class="fa fa-comments-o">
      ChitChat
    </i>
  </h2>
  <div class="lead">Sign up for an account below</div>
  <input id="name" type="text" name="name" class="form-control" placeholder="Name" required autofocus>
  <input type="email" name="email" class="form-control" placeholder="Email address" required>
  <input type="password" name="password" class="form-control" placeholder="Password" required>
  <button class="btn btn-lg btn-primary btn-block" type="submit">Sign up</button>
</form>

{{ end }}

此外,我们还为登录和注册页面定义了单独的布局模板 auth.layout.html

代码语言:javascript
复制
{{ define "layout" }}

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=9">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ChitChat</title>
    <link href="/static/css/bootstrap.min.css" rel="stylesheet">
    <link href="/static/css/font-awesome.min.css" rel="stylesheet">
    <link href="/static/css/login.css" rel="stylesheet">
  </head>
  <body>
    <div class="container">
      
      {{ template "content" . }}
      
    </div> <!-- /container -->
    
    <script src="/static/js/jquery-2.1.1.min.js"></script>
    <script src="/static/js/bootstrap.min.js"></script>
  </body>
</html>

{{ end }}

以上视图模板已经在认证处理器方法中引用。

4、注册用户认证路由

最后,我们需要在 routes/routes.go 中注册用户认证相关路由:

代码语言:javascript
复制
// 定义所有 Web 路由
var webRoutes = WebRoutes{
    ... // 其他路由
    {
        "signup",
        "GET",
        "/signup",
        handlers.Signup,
    },
    {
        "signupAccount",
        "POST",
        "/signup_account",
        handlers.SignupAccount,
    },
    {
        "login",
        "GET",
        "/login",
        handlers.Login,
    },
    {
        "auth",
        "POST",
        "/authenticate",
        handlers.Authenticate,
    },
    {
        "logout",
        "GET",
        "/logout",
        handlers.Logout,
    },
}

5、测试用户认证功能

这样一来,我们就可以重启应用并访问用户注册页面 http://localhost:8080/signup 进行注册了:

注册成功后,页面会跳转到登录页面 http://localhost:8080/login

输入刚才填写的注册邮箱和密码,点击「SIGN IN」按钮登录成功后,页面跳转到首页。

我们还没有对首页做额外的认证判断和处理,所以此时显示的页面效果和之前一样,为了区别用户认证与未认证状态,我们可以基于认证状态渲染不同的导航模板,对于认证用户,渲染 auth.navbar 模板,对于未认证用户,还是保持和之前一样,为此,我们需要在 views 目录下新增 auth.navbar.html 视图:

代码语言:javascript
复制
{{ define "navbar" }}
<div class="navbar navbar-default navbar-static-top" role="navigation">
  <div class="container">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="/">
        <i class="fa fa-comments-o"></i>
        ChitChat
      </a>
    </div>
    <div class="navbar-collapse collapse">
      <ul class="nav navbar-nav">
        <li><a href="/">Home</a></li>
      </ul>
      <ul class="nav navbar-nav navbar-right">
        <li><a href="/logout">Logout</a></li>
      </ul>
    </div>
  </div>
</div>
{{ end }}

同时还要修改 handlers.Index 处理器方法实现:

代码语言:javascript
复制
func Index(writer http.ResponseWriter, request *http.Request) {
    threads, err := models.Threads();
    if err == nil {
        _, err := session(writer, request)
        if err != nil {
            generateHTML(writer, threads, "layout", "navbar", "index")
        } else {
            generateHTML(writer, threads, "layout", "auth.navbar", "index")
        }
    }
}

再次重启应用,刷新首页,导航条的展示效果就不一样了:

此时显示的是「Logout」链接,点击即可退出应用:

下篇教程我们将实现用户界面的群组和主题增删改查功能。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 极客书房 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 2、用户认证相关处理器
    • 1)编写处理器代码
      • 2)用户注册
        • 3)用户登录
          • 4)用户退出
          • 3、用户认证相关视图模板
          • 4、注册用户认证路由
          相关产品与服务
          数据库
          云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档