Go Echo 学习笔记(三)登陆案例


Echo 登陆案例

本文将使用 Echo 来实现一个登陆的案例,在这个案例中将使用到 Echo 的相关特性。

需求

实现一个用户登陆的案例,要求实现用户登陆以及注销的功能,并且具有会话控制的能力。

实现

首先创建项目的启动文件,main.go:

package main

import (
	"github.com/gorilla/sessions"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/labstack/gommon/log"
	"io"
	"math/rand"
	"os"
	"time"
)

// 将session信息进行保存(测试使用)
var cookieStore = sessions.NewCookieStore([]byte("studyEcho"))

// 初始化操作
func init() {
	rand.Seed(time.Now().UnixNano())
	os.Mkdir("./log", 0755)
}

// 实现一个登陆的案例
func main() {
	// 创建一个echo实例
	e := echo.New()

	// 配置日志信息
	configureLogger(e)

	// 设置静态路由
	e.Static("img", "./01-demo/img")
	e.File("/favicon.ico", "./01-demo/img/favicon.ico")
	e.File("/logo.png", "./01-demo/img/logo.png")

	// 设置中间件
	setMiddleWare(e)

	// 注册路由
	RegisterRouter(e)

	// 启动服务
	e.Logger.Fatal(e.Start(":2020"))

}

// configureLogger 设置当前服务器的Logger信息
func configureLogger(e *echo.Echo) {
	// 设置日志级别为info
	e.Logger.SetLevel(log.INFO)
	// 记录业务日志到文件中
	logFile, err := os.OpenFile("./log/echo.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0755)
	if err != nil {
		panic(err)
	}
	// 设置日志输出位置(文件以及终端)
	e.Logger.SetOutput(io.MultiWriter(logFile, os.Stdout))
}

// setMiddleWare 设置中间件
func setMiddleWare(e *echo.Echo) {
	// access log输出到文件中
	accessLog, err := os.OpenFile("./log/access.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		panic(err)
	}
	// 设置日志文件的输出路径(文件以及终端)
	middleware.DefaultLoggerConfig.Output = accessLog
    middleware.DefaultLoggerConfig.Output = os.Stdout

	// 设置对应的中间件
	e.Use(middleware.Logger())  // 使用日志中间件记录http请求信息
	e.Use(AutoLogin)            // 使用自定义中间件验证用户的登陆信息
	e.Use(middleware.Recover()) // 使用恢复中间件恢复panic恐慌状态
}

由于demo需要对用户的登陆信息进行验证,因此创建用户对应的结构体:user.go

package main

type User struct {
	UID      int64
	Username string
	Password string
}

在该demo中需要对每次请求进行用户登陆信息验证,因此自定义一个中间件:middleware.go

package main

import "github.com/labstack/echo/v4"

// AutoLogin 自定义中间件,如果上次记住了则自动登陆
func AutoLogin(next echo.HandlerFunc) echo.HandlerFunc {
	return func(ctx echo.Context) error {
		// 获取用户cookie信息
		cookie, err := ctx.Cookie("username")
		// 当前用户已经登陆
		if err == nil && cookie.Value != "" {
			// 将user信息放在context中,即记住用户信息
			user := &User{Username: cookie.Value}
			ctx.Set("user", user)
		}
		// 返回中间件
		return next(ctx)
	}
}

该demo中所涉及到的所有的路由信息都放置在route.go中

package main

import (
	"bytes"
	"github.com/gorilla/sessions"
	"github.com/labstack/echo/v4"
	"html/template"
	"net/http"
	"time"
)

// RegisterRouter 注册请求路由处理
func RegisterRouter(e *echo.Echo) {
	// 首页路由
	e.GET("/", func(ctx echo.Context) error {
		// 加载模版信息
		tpl := template.Must(template.ParseFiles("./01-demo/template/login.html"))
		ctx.Logger().Info("this is login page")
		// 获取当前的请求参数msg的值
		data := map[string]interface{}{
			"msg": ctx.QueryParam("msg"),
		}
		// 判断用户是否已经登陆,从context中获取user
		if user, ok := ctx.Get("user").(*User); ok {
			// 用户已经登陆则从context中获取用户的信息
			data["username"] = user.Username
			data["had_login"] = true
		} else {
			// 用户没有登陆则从session中获取用户的登陆信息
			sess := getCookieSession(ctx)
			if flashes := sess.Flashes("username"); len(flashes) > 0 {
				data["username"] = flashes[0]
			}
			sess.Save(ctx.Request(), ctx.Response())
		}
		// 将模版以及数据信息写入到缓冲区中
		var buf bytes.Buffer
		err := tpl.Execute(&buf, data)
		if err != nil {
			return err
		}
		// 将模版信息以html的方式返回
		return ctx.HTML(http.StatusOK, buf.String())
	})
	// 登陆路由
	e.POST("/login", func(ctx echo.Context) error {
		// 获取请求参数
		username := ctx.FormValue("username")
		passwd := ctx.FormValue("passwd")
		remember_me := ctx.FormValue("remember_me")
		if username == "admin" && passwd == "123456" {
			// 用户名密码正确则用标准库种cookie
			cookie := &http.Cookie{
				Name:     "username",
				Value:    username,
				HttpOnly: true,
			}
			// 查看用户是否选择记住用户名
			if remember_me == "1" {
				cookie.MaxAge = 7 * 24 * 3600 // 7天
			}
			ctx.SetCookie(cookie)
			// 重定向到首页
			return ctx.Redirect(http.StatusSeeOther, "/")
		}
		// 用户名密码错误则返回相应信息
		// 首先使用session保存当前用户的用户名信息
		session := getCookieSession(ctx)
		session.AddFlash("username", username)
		err := session.Save(ctx.Request(), ctx.Response())
		if err != nil {
			return ctx.Redirect(http.StatusSeeOther, "/?msg="+err.Error())
		}
		return ctx.Redirect(http.StatusSeeOther, "/?msg=用户名或者密码错误")
	})
	// 注销路由
	e.GET("/logout", func(ctx echo.Context) error {
		// 覆盖当前cookie
		cookie := &http.Cookie{
			Name:    "username",
			Value:   "",
			Expires: time.Now().Add(-1e9),
			MaxAge:  -1,
		}
		ctx.SetCookie(cookie)
		return ctx.Redirect(http.StatusSeeOther, "/")
	})
}

// getCookieSession 获取当前登陆context中的session信息
func getCookieSession(ctx echo.Context) *sessions.Session {
	session, _ := cookieStore.Get(ctx.Request(), "request-scope")
	return session
}

对应的登陆页面信息显示:login.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>登录 — Echo 框架学习</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="shortcut icon" href="/favicon.ico">
    <link href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<div class="container">

    <div class="text-center m-2">
        <img src="/img/logo.png" class="img-fluid" alt="Go语言中文网" width="64">
    </div>
    <h1 class="text-center">Echo 框架学习:登录案例</h1>
    <hr>

    <form class="form-horizontal" action="/login" method="post">
        
        <div class="p-2 mb-2 bg-success text-white text-center">欢迎您,
            <a href="/logout" class="text-white float-right">退出登录</a>
        </div>
        
        
        <div class="p-2 mb-2 bg-danger text-white text-center"></div>
        
        <div class="form-group row">
            <label for="username" class="col-sm-2 col-form-label">用户名</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="username" name="username" value="" placeholder="请输入用户名">
            </div>
        </div>
        <div class="form-group row">
            <label for="passwd" class="col-sm-2 col-form-label">密  码</label>
            <div class="col-sm-10">
                <input type="password" class="form-control" id="passwd" name="passwd" placeholder="请输入密码">
            </div>
        </div>
        <div class="form-group">
            <div class="offset-sm-2 col-sm-10">
                <div class="checkbox">
                    <label>
                        <input type="checkbox" name="remember_me" value="1"> 记住我
                    </label>
                </div>
            </div>
        </div>
        <div class="form-group row">
            <div class="offset-sm-5 col-sm-5">
                <button type="submit" class="btn btn-success">登录</button>
            </div>
        </div>
        
    </form>
</div>

</body>
</html>