Go语言GoFrame框架之三:cookie和session之web服务示例多端登录系统
一、示例简介:
- 模版生成的命令(gf init gf-login-web -u)生成示例
- 示例通过ghttp.Server注册一个中间件来做session的认证(如果有相应的session表示刚才登录过,直接返回登录过的结果),接着处理四个协议:“登录界面 获取 api”,“登录 获取 api”,“登录 请求 api”,“登出 请求 api”,最后把相应的结果(其中包括html网页)返回
- 通过cmd.go,consts.go,login_web.go,webhtml.go,midddleware.go,html.go,config.yaml,web_html_util.go,main.go(有部分文件是框架生成的)等文件了解GoFrame框架的规范和如何使用
源码仓库https://github.com/CN007PG/gf-login-web查看。
二、目录结构
- api
- v1
- login_web.go
- v1
- hack(此目录在此暂时不介绍)
- internal
- cmd
- cmd.go
- consts
- consts.go
- controller
- login_web.go
- dao
- logic
- html
- webhtml.go
- logic.go
- html
- middleware
- middleware.go
- model
- do
- entity
- packed(此目录在此暂时不介绍)
- service
- html.go
- cmd
- manifest
- config
- config.yaml
- deploy(此目录在此暂时不介绍)
- docker(此目录在此暂时不介绍)
- config
- resource
- i18n
- public(此目录在此暂时不介绍)
- template
- index.html
- login.html
- utility
- web_html_util.go
- go.mod
- main.go
- Makefile
- README.MD
目录结构祥细使用规范前面我们已经介绍了
三、相应文件代码
login_web.go
package v1
import (
"github.com/gogf/gf/v2/frame/g"
)
type LoginUIGetReq struct {
g.Meta `path:"/" tags:"LoginWeb" method:"get" summary:"登录界面 获取 api"`
}
type LoginUIGetRes struct {
g.Meta `mime:"text/html" example:"string"`
}
type LoginGetReq struct {
g.Meta `path:"/login" tags:"LoginWeb" method:"get" summary:"登录 获取 api"`
}
type LoginGetRes struct {
g.Meta `mime:"text/html" example:"string"`
}
type LoginPostReq struct {
g.Meta `path:"/login" tags:"LoginWeb" method:"post" summary:"登录 请求 api"`
}
type LoginPostRes struct {
g.Meta `mime:"text/html" example:"string"`
}
type LogoutPostReq struct {
g.Meta `path:"/logout" tags:"LoginWeb" method:"post" summary:"登出 请求 api"`
}
type LogoutPostRes struct {
g.Meta `mime:"text/html" example:"string"`
}
cmd.go
package cmd
import (
"context"
"gf-login-web/internal/controller"
"gf-login-web/internal/middleware"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gcmd"
)
var (
Main = gcmd.Command{
Name: "main",
Usage: "main",
Brief: "start http server",
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
s := g.Server()
//设置组
s.Group("/", func(group *ghttp.RouterGroup) {
//设置中间件
group.Middleware(middleware.Middleware.MiddlewareAuth)
//绑定LoginWeb
group.Bind(
controller.LoginWeb,
)
})
//
s.Run()
return nil
},
}
)
consts.go
package consts
// 常量 登录web的session键
const SessionLoginWeb = "SessionLoginWeb"
- login_web.go
package controller
import (
"context"
"gf-login-web/internal/consts"
"gf-login-web/internal/service"
"gf-login-web/utility"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"gf-login-web/api/v1"
)
var (
LoginWeb = cLoginWeb{}
)
type cLoginWeb struct{}
// 登录界面 获取 api 监听回调
func (c *cLoginWeb) LoginGetUI(ctx context.Context, req *v1.LoginUIGetReq) (res *v1.LoginUIGetRes, err error) {
g.RequestFromCtx(ctx).Response.WriteTpl("login.html", g.Map{
"title": "登录页面",
})
return
}
// 登录 获取 api 监听回调
func (c *cLoginWeb) LoginGet(ctx context.Context, req *v1.LoginGetReq) (res *v1.LoginGetRes, err error) {
username := utility.GetSessionUsername(g.RequestFromCtx(ctx), consts.SessionLoginWeb, "username")
// 返回登录成功页面
service.WebHtml().ReturnIndexHtml(g.RequestFromCtx(ctx), gconv.String(username))
return
}
// 登录 请求 api 监听回调
func (c *cLoginWeb) LoginPost(ctx context.Context, req *v1.LoginPostReq) (res *v1.LoginPostRes, err error) {
username := g.RequestFromCtx(ctx).Get("username")
password := g.RequestFromCtx(ctx).Get("password")
// 暂时写死 后面再用数据库
dbUsername := "admin"
dbPassword := "123456"
if username.String() == dbUsername && password.String() == dbPassword {
// 添加session
g.RequestFromCtx(ctx).Session.Set(consts.SessionLoginWeb, g.Map{
"username": dbUsername,
})
// 返回登录成功页面
service.WebHtml().ReturnIndexHtml(g.RequestFromCtx(ctx), gconv.String(username))
}
// 返回登录失败
g.RequestFromCtx(ctx).Response.WriteJson(g.Map{
"code": -1,
"msg": "登录失败",
})
return
}
// 登出 请求 api 监听回调
func (c *cLoginWeb) LogoutPost(ctx context.Context, req *v1.LogoutPostReq) (res *v1.LogoutPostRes, err error) {
// 删除session
g.RequestFromCtx(ctx).Session.Remove(consts.SessionLoginWeb)
// 返回登出页面
g.RequestFromCtx(ctx).Response.WriteTpl("index.html", g.Map{
"tips": "再见!",
"username": "",
"button_display": "none",
})
return
}
webhtml.go
package html
import (
"gf-login-web/internal/service"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
type (
sWebHtml struct{}
)
// 此方法 为依赖注入时框架执行的匿名包初始化方法init
// 此方法 要在terminal执行gf gen service 命令导出service下的html.go后再加入
func init() {
service.RegisterWebHtml(New())
}
// 此方法在init调用的 同上要在terminal执行gf gen service 命令导出service下的html.go后再加入
func New() *sWebHtml {
return &sWebHtml{}
}
// 直接返回主页html
func (s *sWebHtml) ReturnIndexHtml(r *ghttp.Request, username string) {
// 返回登录成功页面
r.Response.WriteTpl("index.html", g.Map{
"tips": "欢迎您!",
"username": username,
"button_display": "inline-block",
})
r.Exit()
}
logic.go
package logic
import (
//增加html的引入 这样框架就会执行html包webhtml.go文件下的匿名包初始化方法init
_ "gf-login-web/internal/logic/html"
)
middleware.go
package middleware
import (
"gf-login-web/internal/consts"
"gf-login-web/internal/service"
"gf-login-web/utility"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/util/gconv"
)
var (
Middleware = sMiddleware{}
)
type (
sMiddleware struct{}
)
// 认证中间件
func (s *sMiddleware) MiddlewareAuth(r *ghttp.Request) {
if r.Method == "GET" {
//对GET模式下的“/"和”/login“申请作Session认证
if r.Router.Uri == "/" {
re, _ := r.Session.Contains(consts.SessionLoginWeb)
if re {
username := utility.GetSessionUsername(r, consts.SessionLoginWeb, "username")
// 直接返回登录成功页面(也可以用RedirectTo("/login")做跳转)
service.WebHtml().ReturnIndexHtml(r, gconv.String(username))
} else {
// 正常 登录
r.Middleware.Next()
}
} else if r.Router.Uri == "/login" {
re, _ := r.Session.Contains(consts.SessionLoginWeb)
if re {
r.Middleware.Next()
} else {
// 返回错误码
r.Response.WriteJson(g.Map{
"code": 403,
"msg": "您访问超时或已登出",
})
}
} else {
//get的其它申请直接下一步
r.Middleware.Next()
}
} else {
//post的申请直接下一步
r.Middleware.Next()
}
}
html.go此文件 由命令gf gen service 根据 webhtml.go生成的
// ================================================================================
// Code generated by GoFrame CLI tool. DO NOT EDIT.
// You can delete these comments if you wish manually maintain this interface file.
// ================================================================================
package service
import (
"github.com/gogf/gf/v2/net/ghttp"
)
type (
IWebHtml interface {
ReturnIndexHtml(r *ghttp.Request, username string)
}
)
var (
localWebHtml IWebHtml
)
func WebHtml() IWebHtml {
if localWebHtml == nil {
panic("implement not found for interface IWebHtml, forgot register?")
}
return localWebHtml
}
func RegisterWebHtml(i IWebHtml) {
localWebHtml = i
}
config.yaml
server:
address: ":8000"
openapiPath: "/api.json"
swaggerPath: "/swagger"
logger:
level : "all"
stdout: true
viewer:
Delimiters: ["${", "}"]
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>主页</title>
<style>
body {
background: no-repeat;
background-size: 100% 130%;
}
#bg_box {
width: 20%;
height: 400px;
background-color: #00000060;
margin: auto;
margin-top: 10%;
text-align: center;
border-radius: 10px;
padding: 50px 50px;
}
h2 {
color: #ffffff90;
margin-top: 5%;
}
button {
/*这种方式ide会报错*/
/*display: ${ .button_display };*/
margin-top: 50px;
width: 60%;
height: 30px;
border-radius: 10px;
border: 0;
color: #fff;
text-align: center;
line-height: 30px;
font-size: 15px;
background-color: #032379;
}
</style>
</head>
<body>
<div id="bg_box">
<h2>${ .tips }</h2>
<form class="form-horizontal " role="form" action="/logout" method="post" >
<h2 name="username">${ .username }</h2>
<button>退出</button><br>
</form>
</div>
</body>
<script>
// 这种方式ide不会报错(作为程序员最不想见到的就是报错了)
document.querySelectorAll("button").item(0).style.setProperty("display",'${ .button_display }');
</script>
</html>
login.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录</title>
<style>
body {
background: no-repeat;
background-size: 100% 130%;
}
#bg_box {
width: 20%;
height: 400px;
background-color: #00000060;
margin: auto;
margin-top: 10%;
text-align: center;
border-radius: 10px;
padding: 50px 50px;
}
h2 {
color: #ffffff90;
margin-top: 5%;
}
input {
border: 0;
width: 60%;
font-size: 15px;
color: #fff;
background: transparent;
border-bottom: 2px solid #fff;
padding: 5px 10px;
outline: none;
margin-top: 10px;
}
button {
margin-top: 50px;
width: 60%;
height: 30px;
border-radius: 10px;
border: 0;
color: #fff;
text-align: center;
line-height: 30px;
font-size: 15px;
background-color: #032379;
}
</style>
</head>
<body>
<div id="bg_box">
<h2>${ .title }</h2>
<form class="form-horizontal " role="form" action="/login" method="post" >
<div id="input_box">
<input type="text" name="username" placeholder="请输入用户名">
</div>
<div class="input_box">
<input type="password" name="password" placeholder="请输入密码">
</div>
<button>登录</button><br>
</form>
</div>
</body>
</html>
web_html_util.go
package utility
import (
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/util/gconv"
)
// 取session 的用户名
func GetSessionUsername(r *ghttp.Request, sessionKey string, usernameKey string) string {
// 返回相应的 session key 的 username key 的用户名
re_val, _ := r.Session.Get(sessionKey)
return gconv.String(re_val.Map()[usernameKey])
}
main.go
package main
import (
//增加logic的引入 这样可以框架就会执行logic匿名包初始化方法
_ "gf-login-web/internal/logic"
_ "gf-login-web/internal/packed"
"github.com/gogf/gf/v2/os/gctx"
"gf-login-web/internal/cmd"
)
func main() {
cmd.Main.Run(gctx.New())
}
//readme: 这只是一个示例,不是真正的项目
四、项目搭建流程
- 先创建 login_web.go 定义相应的接口,path是路由(也就是常说的接口,method协议方式get或post)
- 创建index.html,login.html两个网页html其中页面内以${ .变量名 }的方式来定义,运行的时候使用GoFrame框架来填充相应的变量,再返回前端
- 修改config.yaml文件,在文件内加入 viewer:Delimiters: [“${”, “}”] ,这里注意的是不用定义viewer的属性Path因为框架gview_parse.go已经做相应的查找了,加了反而会报错,如果直接复制过去要注意一下双引号的编码,最好是手动输入。
- 创建web_html_util.go文件,加入取session的用户名公共方法,因为我们可以了解到此文件夹是放公共函数的地方
- 创建webhtml.go文件,加入一个直接返回主页html的函数
- 通过命令gf gen service 生成 webhtml.go,并在webhtml.go文件加入init和New方法
- 在consts.go里加入SessionLoginWeb常量,也就是我们要定义的常量最后都放在此文件夹下
- 创建login_web.go文件,并定义type cLoginWeb struct 结构体实例 LoginWeb 和 相应的监听回调方法
- 创建middleware.go文件,加入MiddlewareAuth认证方法(也可以在根路由认证那,用RedirectTo的跳转)
- 修改cmd.go文件,在 Group回调设置中间件和绑定回调对象controller.LoginWeb
- 最后一步很重要,创建logic.go文件,并import “gf-login-web/internal/logic/html” 然后在main.go文件import “gf-login-web/internal/logic” 这样才框架才能执行webhtml.go下的init匿名包初始化方法
五、核心讲解
框架的cookie和session已经完美的做好并在ghttp里保存了,并且以RemoteAddr + Header通过guid模块做到了session id 的唯一,而且前端页面的cookie里以gfsession键保存session id ,申请时框架会通过前端页面的传来cookie取对应的session id保存在ghttp里,我们只要在这个唯一的session下保存登录状态就可以了。可以通过s.SetSessionMaxAge(time.Minute)设置Session一分钟过期来测试