Gin
Begin
安装Gin:
go get -u github.com/gin-gonic/gin
例子:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
)
func main() {
engine := gin.Default()
engine.GET("/hello", func(context *gin.Context) {
fmt.Println("请求路径", context.FullPath())
context.Writer.Write([]byte("Hello, Gin\n"))
})
if err := engine.Run(":8000"); err!= nil {
log.Fatal(err.Error())
}
}
网络请求与路由处理
创建Engine
在gin框架中,Engine被定义成为一个结构体,Engine代表gin框架的一个结构体定义,其中包含了路由组、中间件、页面渲染接口、框架配置设置等相关内容。默认的Engine可以通过gin.Default进行创建,或者使用gin.New()同样可以创建。两种方式如下所示:
engine1 = gin.Default()
engine2 = gin.New()
gin.Default()和gin.New()的区别在于gin.Default也使用gin.New()创建engine实例,但是会默认使用Logger和Recovery中间件。
Logger是负责进行打印并输出日志的中间件,方便开发者进行程序调试;Recovery中间件的作用是如果程序执行过程中遇到panic中断了服务,则Recovery会恢复程序执行,并返回服务器500内部错误。通常情况下,我们使用默认的gin.Default创建Engine实例。
RESTful
REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。
简单来说,REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。
- GET用来获取资源
- POST用来新建资源
- PUT用来更新资源
- DELETE用来删除资源。
只要API程序遵循了REST风格,那就可以称其为RESTful API。目前在前后端分离的架构中,前后端基本都是通过RESTful API来进行交互。
处理HTTP请求
在创建的engine实例中,包含很多方法可以直接处理不同类型的HTTP请求。
HTTP请求类型
http协议中一共定义了八种方法或者称之为类型来表明对请求网络资源(Request-URI)的不同的操作方式,分别是:OPTIONS、HEAD、GET、POST、PUT、DELETE、TRACE、CONNECT。
通用处理
engine中可以直接进行HTTP请求的处理,在engine中使用Handle方法进行http请求的处理。Handle方法包含三个参数,具体如下所示:
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes
- httpMethod:第一个参数表示要处理的HTTP的请求类型,是GET、POST、DELETE等8种请求类型中的一种。
- relativePath:第二个参数表示要解析的接口,由开发者进行定义。
- handlers:第三个参数是处理对应的请求的代码的定义。
例子:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.Default()
engine.Handle("GET", "/hello", func(context *gin.Context) {
fmt.Println(context.FullPath())
name := context.DefaultQuery("name", "hello")
fmt.Println(name)
context.Writer.Write([]byte("hello, " + name))
})
engine.Handle("POST", "/login", func(context *gin.Context) {
fmt.Println(context.FullPath())
username := context.PostForm("username")
password := context.PostForm("password")
fmt.Println(username, password)
context.Writer.Write([]byte(username + "登陆成功!"))
})
engine.Run()
}
分类处理
除了engine中包含的通用的处理方法以外,engine还可以按类型进行直接解析。engine中包含有get方法、post方法、delete方法等与http请求类型对应的方法。
例子:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.Default()
engine.GET("/hello", func(context *gin.Context) {
fmt.Println(context.FullPath())
name := context.Query("name")
fmt.Println(name)
context.Writer.Write([]byte("hello, " + name))
})
engine.POST("/login", func(context *gin.Context) {
fmt.Println(context.FullPath())
username, exist := context.GetPostForm("username")
if exist {
fmt.Println(username)
}
password, exists := context.GetPostForm("password")
if exists {
fmt.Println(password)
}
context.Writer.Write([]byte(username + "登陆成功!"))
})
engine.DELETE("/user/:id", func(context *gin.Context) {
id := context.Param("id")
fmt.Println(id)
context.Writer.Write([]byte("delete id = " + id))
})
engine.Run()
}
请求参数绑定与多数据格式处理
如果表单数据较多时,使用PostForm和GetPostForm一次获取一个表单数据,开发效率较慢。Gin框架提供给开发者表单实体绑定的功能,可以将表单数据与结构体绑定。
使用PostForm这种单个获取属性和字段的方式,代码量较多,需要一个一个属性进行获取。而表单数据的提交,往往对应着完整的数据结构体定义,其中对应着表单的输入项。gin框架提供了数据结构体和表单提交数据绑定的功能,提高表单数据获取的效率。
ShouldBindQuery
使用ShouldBindQuery可以实现Get方式的数据请求的绑定。具体实现如下:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
)
func main() {
engine := gin.Default()
engine.GET("/hello", func(context *gin.Context) {
fmt.Println(context.FullPath())
var student Student
err := context.ShouldBindQuery(&student)
if err != nil {
log.Fatal(err.Error())
return
}
fmt.Println(student.Name)
fmt.Println(student.Classes)
context.Writer.Write([]byte("hello, " + student.Name))
})
engine.Run()
}
type Student struct {
Name string `form:"name"`
Classes string `form:"classes"`
}
ShouldBind
使用ShouldBind可以实现Post方式的提交数据的绑定工作。具体实现如下:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
)
func main() {
engine := gin.Default()
engine.POST("/login", func(context *gin.Context) {
fmt.Println(context.FullPath())
var register Register
if err := context.ShouldBind(®ister); err != nil {
log.Fatal(err.Error())
return
}
fmt.Println(register)
context.Writer.Write([]byte("Name: " + register.UserName + "\nPhone: " + register.Phone))
})
engine.Run()
}
type Register struct {
UserName string `form:"name"`
Phone string `form:"phone"`
Password string `form:"password"`
}
ShouldBindJson
当客户端使用Json格式进行数据提交时,可以采用ShouldBindJson对数据进行绑定并自动解析。具体实现如下:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
)
func main() {
engine := gin.Default()
engine.POST("/addStudent", func(context *gin.Context) {
fmt.Println(context.FullPath())
var person Person
if err := context.BindJSON(&person); err != nil {
log.Fatal(err.Error())
return
}
fmt.Println(person)
context.Writer.Write([]byte("add: " + person.Name))
})
engine.Run()
}
type Person struct {
Name string `form:name`
Sex string `form:sex`
Age int `form:age`
}
多数据格式返回请求结果
一个完整的请求包含请求、处理请求和结果返回三个步骤,在服务器端对请求处理完成以后,会将结果返回给客户端。
在gin框架中,支持返回多种请求数据格式。
[]byte
engine.GET("/helloByte", func(context *gin.Context) {
fullPath := context.FullPath()
fmt.Println(fullPath)
context.Writer.Write([]byte(fullPath))
})
string
engine.GET("/helloString", func(context *gin.Context) {
fullPath := context.FullPath()
fmt.Println(fullPath)
context.Writer.WriteString(fullPath)
})
JSON
gin框架中的context包含的JSON方法可以将结构体类型的数据转换成JSON格式的结构化数据,然后返回给客户端。
map
engine.GET("/helloJson", func(context *gin.Context) {
fullPath := context.FullPath()
fmt.Println(fullPath)
context.JSON(200, map[string]interface{}{
"code":1,
"msg":"OK",
"data":fullPath,
})
})
结构体
engine.GET("/helloStruct", func(context *gin.Context) {
fullPath := context.FullPath()
fmt.Println(fullPath)
resp := Response{Code: 1, Msg: "OK", Data: fullPath}
context.JSON(200, &resp)
})
...
type Response struct {
Code int
Msg string
Data interface{}
}
HTML模板
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
engine := gin.Default()
engine.LoadHTMLGlob("./html/*")
engine.Static("/img", "./img")
engine.GET("/helloHtml", func(context *gin.Context) {
fullPath := context.FullPath()
fmt.Println(fullPath)
context.HTML(http.StatusOK, "index.html", gin.H{
"fullPath": fullPath,
"title": "Hello, Gin",
})
})
engine.Run()
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{.title}}</title>
</head>
<body>
<center>{{.fullPath}}</center>
<div align="center">
<img src="../img/go.jpeg">
</div>
</body>
</html>
加载静态资源
在上面的index.html的基础上,添加一张img进行展示。需要将img所在的目录进行静态资源路径设置才可能会生效,如下所示:
engine.Static("/img", "./img")
在工程项目的根目录下创建img目录,用于存放静态的img资源。
同理,在项目开发时,一些静态的资源文件如html、js、css等可以通过静态资源文件设置的方式来进行设置。
使用路由组分类处理请求
背景
在实际的项目开发中,均是模块化开发。同一模块内的功能接口,往往会有相同的接口前缀。
例如在系统中有用户模块,用户有不同注册、登录、用户信息。
注册:http://localhost:9000/user/register
登录:http://localhost:9000/user/login
用户信息:http://localhost:9000/user/info
删除:http://localhost:9000/user/1001
类似这种接口前缀统一,均属于相同模块的功能接口。可以使用路由组进行分类处理。
Group
gin框架中可以使用路由组来实现对路由的分类。
路由组是router.Group中的一个方法,用于对请求进行分组。
package main
import "github.com/gin-gonic/gin"
func main() {
engine := gin.Default()
routerGroup := engine.Group("/user")
// http://localhost:8080/user/register
routerGroup.POST("/register", regHandle)
routerGroup.DELETE("/:id", delHandle)
engine.Run()
}
func regHandle(context *gin.Context) {
context.Writer.WriteString(context.FullPath())
}
func delHandle(context *gin.Context) {
id := context.Param("id")
context.Writer.WriteString(string(id))
}
中间件的编写和使用
中间件
在web应用服务中,完整的一个业务处理在技术上包含客户端操作、服务器端处理、返回处理结果给客户端三个步骤。
在实际的业务开发和处理中,会有更负责的业务和需求场景。一个完整的系统可能要包含鉴权认证、权限管理、安全检查、日志记录等多维度的系统支持。
鉴权认证、权限管理、安全检查、日志记录等这些保障和支持系统业务属于全系统的业务,和具体的系统业务没有关联,对于系统中的所有业务都适用。
由此,在业务开发过程中,为了更好的梳理系统架构,可以将上述描述所涉及的一些通用业务单独抽离并进行开发,然后以插件化的形式进行对接。这种方式既保证了系统功能的完整,同时又有效的将具体业务和系统功能进行解耦,并且,还可以达到灵活配置的目的。
这种通用业务独立开发并灵活配置使用的组件,一般称之为"中间件",因为其位于服务器和实际业务处理程序之间。其含义就是相当于在请求和具体的业务逻辑处理之间增加某些操作,这种以额外添加的方式不会影响编码效率,也不会侵入到框架中。中间件的位置和角色示意图如下图所示:
Gin的中间件
在gin中,中间件称之为middleware,中间件的类型定义如下所示:
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
HandlerFunc是一个函数类型,接收一个Context参数。用于编写程序处理函数并返回HandleFunc类型,作为中间件的定义。
中间件Use用法
使用gin.Default创建了gin引擎engins变量,其中,就使用了中间件。如下图所示:
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
//Log中间件
func Logger() HandlerFunc {
return LoggerWithConfig(LoggerConfig{})
}
//Recovery中间件
func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}
在Default函数中,engine调用Use方法设置了Logger中间件和Recovery中间件。Use函数接收一个可变参数,类型为HandlerFunc,恰为中间件的类型。Use方法定义如下:
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
自定义中间件
根据上文的介绍,可以自己定义实现一个特殊需求的中间件,中间件的类型是函数,有两条标准:
- func函数
- 返回值类型为HandlerFunc
比如,我们自定义一个自己的中间件。在前面所学的内容中,我们在处理请求时,为了方便代码调试,通常都将请求的一些信息打印出来。有了中间件以后,为了避免代码多次重复编写,使用统一的中间件来完成。定义一个名为RequestInfos的中间件,在该中间件中打印请求的path和method。具体代码实现如下所示:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.Default()
//全部路由都使用中间件
//engine.Use(RequestInfos())
engine.GET("/query", func(context *gin.Context) {
context.JSON(200, map[string]interface{}{
"code":1,
"msg":context.FullPath(),
})
})
//单个路由使用中间件,放在第二个参数
engine.GET("/single", RequestInfos(), func(context *gin.Context) {
context.Writer.WriteString("single")
})
engine.Run()
}
// RequestInfos 打印请求信息的中间件
func RequestInfos() gin.HandlerFunc {
return func(context *gin.Context) {
path := context.FullPath()
method := context.Request.Method
fmt.Println("请求path:", path)
fmt.Println("请求method:", method)
}
}
context.Next函数
在上文自定义的中间件RequestInfos中,打印了请求了请求的path和method,接着去执行了正常的业务处理函数。如果我们想输出业务处理结果的信息,该如何实现呢。答案是使用context.Next函数。
context.Next函数可以将中间件代码的执行顺序一分为二,Next函数调用之前的代码在请求处理之前之前,当程序执行到context.Next时,会中断向下执行,转而先去执行具体的业务逻辑,执行完业务逻辑处理函数之后,程序会再次回到context.Next处,继续执行中间件后续的代码。
例子:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.Default()
//全部路由都使用中间件
engine.Use(RequestInfos())
engine.GET("/query", func(context *gin.Context) {
fmt.Println("处理接口请求。")
context.JSON(404, map[string]interface{}{
"code":1,
"msg":context.FullPath(),
})
})
engine.Run()
}
// RequestInfos 打印请求信息的中间件
func RequestInfos() gin.HandlerFunc {
return func(context *gin.Context) {
path := context.FullPath()
method := context.Request.Method
fmt.Println("请求path:", path)
fmt.Println("请求method:", method)
fmt.Println("状态码:", context.Writer.Status())
context.Next()
fmt.Println("状态码:", context.Writer.Status())
}
}
输出:
请求path: /query
请求method: GET
状态码: 200
处理接口请求。
状态码: 404
访问和操作数据库
引入:
go get "github.com/go-sql-driver/mysql"
例子:
package main
import (
"database/sql"
"fmt"
"log"
)
import _ "github.com/go-sql-driver/mysql"
func main() {
//连接数据库
connStr := "root:@tcp(127.0.0.1:3306)/mydb1"
db, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatal(err.Error())
return
}
//创建数据库
_, err = db.Exec(`create table person(
id int primary key auto_increment,
name varchar(20) not null,
age int default 1);`)
if err != nil {
log.Fatal(err.Error())
return
} else {
fmt.Println("创建数据库成功!")
}
//插入数据
_, err = db.Exec("insert into person(name, age) values (?, ?);", "Tom", 18)
if err != nil {
log.Fatal(err.Error())
return
} else {
fmt.Println("数据插入成功!")
}
//查询数据
rows, err := db.Query("select id, name, age from person;")
if err != nil {
log.Fatal(err.Error())
return
}
scan:
if rows.Next() {
person := new(Person)
err = rows.Scan(&person.Id, &person.Name, &person.Age)
if err != nil {
log.Fatal(err.Error())
return
}
fmt.Println(person)
goto scan
}
}
type Person struct {
Id int
Name string
Age int
}
项目初始化
app.json:
{
"app_name": "Cloud Restaurant",
"app_mode": "debug",
"app_host": "localhost",
"app_port": "8000"
}
HelloController.go:
package controller
import "github.com/gin-gonic/gin"
type HelloController struct {
}
func (hello *HelloController)Router(engine *gin.Engine) {
engine.GET("/hello", hello.Hello)
}
func (hello *HelloController) Hello(context *gin.Context) {
context.JSON(200, map[string]interface{}{
"hello":"hello Cloud Restaurant",
})
}
Config.go:
package tool
import (
"bufio"
"encoding/json"
"os"
)
type Config struct {
AppName string `json:"app_name"`
AppMode string `json:"app_mode"`
AppHost string `json:"app_host"`
AppPort string `json:"app_port"`
}
var _cfg *Config = nil
func ParseConfig(path string) (*Config, error) {
file, err := os.Open(path)
if err!= nil {
panic(err)
}
defer file.Close()
reader := bufio.NewReader(file)
decoder := json.NewDecoder(reader)
if err = decoder.Decode(&_cfg); err != nil {
return nil, err
}
return _cfg, nil
}
main.go:
package main
import (
"CloudRestaurant/controller"
"CloudRestaurant/tool"
"github.com/gin-gonic/gin"
)
func main() {
config, err := tool.ParseConfig("./config/app.json")
if err != nil {
panic(err.Error())
}
app := gin.Default()
registerRouter(app)
app.Run(config.AppHost + ":" + config.AppPort)
}
//路由设置
func registerRouter(router *gin.Engine) {
new(controller.HelloController).Router(router)
}
项目前端介绍
目录说明
- build:build目录是指定的项目编译目录,该项目的编译配置相关的操作,都在该目录中进行配置和指定。
- config:config目录主要是对项目全局进行相关的配置,以及测试或者发布版本进行配置。
- dist:所有的.vue页面编译后成为的js文件,都会输出到该目录中。
- node_modules:该目录是nodejs项目所需要的依赖的第三方库的代码目录。由于该目录体积较大,在进行项目迁移或者项目拷贝时,可以将该目录删除,在项目的根目录中执行npm install命令会重新生成并下载所需要的第三方的代码库。
- src:该目录是存放前端工程项目的源码目录。
- static:该目录用于存放静态的文件,比如js、css文件等。
- package.json:执行npm init命令会生成该文件,该文件是node项目的配置文件,其中包含项目的编译和第三方依赖库依赖信息等内容。
请求接口API
在shop-client前端项目的src目录下的api目录中,有两个js文件,分别为ajax.js文件和index.js文件。
- ajax.js文件:该文件中封装了前端项目进行异步网络请求的方法ajax,该函数包含三个参数,分别是:url、data、type,表示请求路径,请求参数和请求方法。
- index.js文件:在该文件中,引入了ajax.js文件和其中的ajax方法,定义了基础请求路径BASE_URL常量,此项目中的请求端口为8090,与后台服务器监听的端口一致。如果想自己修改服务器监听端口,要记得将前端代码BASE_URL常量中的端口也要一并修改。另外,在index.js文件中定义了本项目功能开发所需要的接口,供在.vue文件中进行使用。
常用命令
可以使用一些常用命令对项目进行编译、测试启动运行等操作。在项目根目录下的package.json目录文件中,可以找到有scripts标签配置,其中就定义和配置了不同功能的脚本命令。scripts标签配置内容如下所示:
...
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"lint": "eslint --ext .js,.vue src",
"build": "node build/build.js"
}
...
按照上述的脚本命令配置,可以实现很多指令功能。 比如说,可以使用如下命令运行编译项目:
npm run build
上述命令会具体执行node build/build.js命令,用于根据build.js文件对项目进行编译。
另外,还可以使用如下命令运行nodejs项目:
npm start
因为在scripts脚本中配置了start命令,只有start命令可以省略run。当然,npm start
会具体执行的指令是npm run dev
,因此也可以通过npm run dev
命令来运行项目。
集成第三方短信发送功能
申请阿里云服务
- 申请签名
- 申请模板
- 创建AccessKey和AccessKeySercet
集成
SDK
安装阿里云Go SDK:
go get github.com/aliyun/alibaba-cloud-sdk-go
tool/Config.go
type SmsConfig struct {
SignName string `json:"sign_name"`
TemplateCode string `json:"template_code"`
AppKey string `json:"app_key"`
AppSecret string `json:"app_secret"`
RegionId string `json:"region_id"`
}
config/app.json
"sms": {
"sign_name": "",
"template_code": "",
"app_key": "",
"app_secret": "",
"region_id": "cn-hangzhou"
},
controller/MemberController.go
func (mc *MemberController) Router(engine *gin.Engine) {
engine.GET("/api/sendcode", mc.sendSmsCode)
}
func (mc *MemberController) sendSmsCode(context *gin.Context) {
phone, exist := context.GetQuery("phone")
if !exist{
context.JSON(200, map[string]interface{}{
"code":0,
"msg":"参数解析失败",
})
return
}
ms := service.MemberService{}
isSend := ms.SendCode(phone)
if isSend {
context.JSON(200, map[string]interface{}{
"code":1,
"msg":"发送成功",
})
return
}
context.JSON(200, map[string]interface{}{
"code":1,
"msg":"发送失败",
})
}
service/MemberService.go
func (ms *MemberService) SendCode(phone string) bool {
//产生一个验证码
code := fmt.Sprintf("%06v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000))
//调用阿里云短信接口发送短信
smsConfig := tool.GetConfig().Sms
client, err := dysmsapi.NewClientWithAccessKey(smsConfig.RegionId, smsConfig.AppKey, smsConfig.AppSecret)
if err != nil {
log.Fatal(err.Error())
return false
}
request := dysmsapi.CreateSendSmsRequest()
request.Scheme = "https"
request.SignName = smsConfig.SignName
request.TemplateCode = smsConfig.TemplateCode
request.PhoneNumbers = phone
par, err := json.Marshal(map[string]interface{}{
"name": code,
})
request.TemplateParam = string(par)
response, err := client.SendSms(request)
fmt.Println( response)
if err != nil {
log.Fatal(err.Error())
return false
}
if response.Code == "OK" {
return true
}
return false
}
测试
GET请求以下地址:
http://localhost:8000/api/sendcode?phone=17700000000
如果出现如下错误:
missing go.sum entry for module providing package <package_name>
执行go mod tidy
来整理依赖,这个命令会:
- 删除不需要的依赖包
- 下载新的依赖包
- 更新go.sum
验证码保存到数据库
在用户接受到验证码以后,输入验证码进行登录,我们需要验证用户输入的验证码是否正确。
因此,我们需要将发送过的验证码通过持久化的方式保存下来,方便我们进行校验。我们选择通过数据库来存储用户手机验证码。
Xorm
在项目开发过程中,我们会使用一些成熟的框架来操作数据库。xorm就是一个比较流行的数据库操作orm框架。
安装xorm:
go get github.com/go-xorm/xorm
安装mysql驱动:
go get github.com/go-sql-driver/mysql
连接数据库
首先创建数据库:
create database cloudrestaurant;
tool/OrmEngine.go
package tool
import (
"CloudRestaurant/model"
"github.com/go-xorm/xorm"
)
import _ "github.com/go-sql-driver/mysql"
var DbEngine *Orm
type Orm struct {
*xorm.Engine
}
func OrmEngine(cfg *Config) (*Orm, error) {
database := cfg.Database
conn := database.Username + ":" + database.Password + "@tcp(" + database.Host + ":" + database.Port + ")/" +database.DbName + "?charset=" + database.Charset
engine, err := xorm.NewEngine(database.Driver, conn)
if err != nil {
return nil, err
}
engine.ShowSQL(database.ShowSql)
//同步生成数据库表
err = engine.Sync2(new(model.Member), new(model.SmsCode))
if err != nil {
return nil,err
}
orm := new(Orm)
orm.Engine = engine
DbEngine = orm
return orm, nil
}
app.json
连接数据库有些参数需要自己指定,比如驱动类型,登录数据库的用户名,密码,数据库名等。将这些变量配置在app.json
配置文件中。
...
"database": {
"driver": "mysql",
"username": "root",
"password": "",
"host": "localhost",
"port": "3306",
"db_name": "cloudrestaurant",
"charset": "utf8",
"show_sql": true
}
...
tool/Config.go
在Config结构体中添加对dtabase的解析。
...
type DatabaseConfig struct {
Driver string `json:"driver"`
Username string `json:"username"`
Password string `json:"password"`
Host string `json:"host"`
Port string `json:"port"`
DbName string `json:"db_name"`
Charset string `json:"charset"`
ShowSql bool `json:"show_sql"`
}
...
model/SmsCode.go
要存储验证码,需要在数据库中创建表结构进行存储。我们可以创建SmsCode结构体,并通过tag设置数据库字段约束。
package model
type SmsCode struct {
Id int64 `xorm:"pk autoincr" json:"id"`
Phone string `xorm:"varchar(11)" json:"phone"`
BizId string `xorm:"varchar(30)" json:"biz_id"`
Code string `xorm:"varchar(6)" json:"code"`
CreateTime int64 `xorm:"bigint" json:"create_time"`
}
dao/MemberDao.go
package dao
import (
"CloudRestaurant/model"
"CloudRestaurant/tool"
"log"
)
type MemberDao struct {
*tool.Orm
}
func (md *MemberDao) InsertCode(sms model.SmsCode) int64 {
result, err := md.InsertOne(&sms)
if err != nil {
log.Fatal(err.Error())
}
return result
}
service/MemberService.go
将验证码数据保存到数据库中。
...
if response.Code == "OK" {
smsCode := model.SmsCode{Phone: phone, Code: code, BizId: response.BizId, CreateTime: time.Now().Unix()}
memberDao := dao.MemberDao{tool.DbEngine}
result := memberDao.InsertCode(smsCode)
return result > 0
}
...
main.go
package main
import (
"CloudRestaurant/controller"
"CloudRestaurant/tool"
"github.com/gin-gonic/gin"
"log"
)
func main() {
config, err := tool.ParseConfig("./config/app.json")
if err != nil {
panic(err.Error())
}
_, err = tool.OrmEngine(config)
if err != nil {
log.Fatal(err.Error())
return
}
app := gin.Default()
registerRouter(app)
app.Run(config.AppHost + ":" + config.AppPort)
}
//路由设置
func registerRouter(router *gin.Engine) {
new(controller.HelloController).Router(router)
new(controller.MemberController).Router(router)
}
注册数据插入
dao/MemberDao.go
package dao
import (
"CloudRestaurant/model"
"CloudRestaurant/tool"
"log"
)
type MemberDao struct {
*tool.Orm
}
func (md *MemberDao) ValidateSmsCode(phone string, code string) *model.SmsCode {
var sms model.SmsCode
if _, err := md.Where("phone=? and code=?", phone, code).Get(&sms); err != nil {
log.Fatal(err.Error())
}
return &sms
}
func (md *MemberDao) InsertCode(sms model.SmsCode) int64 {
result, err := md.InsertOne(&sms)
if err != nil {
log.Fatal(err.Error())
}
return result
}
func (md *MemberDao) QueryByPhone(phone string) *model.Member {
var member model.Member
if _, err := md.Where("mobile=?", phone).Get(&member); err != nil {
log.Fatal(err.Error())
}
return &member
}
func (md *MemberDao) InsertMember(member model.Member) int64 {
result, err := md.InsertOne(&member)
if err != nil {
log.Fatal(err.Error())
}
return result
}
全局跨域请求处理设置
跨域访问
在浏览器中的任意一个页面地址,或者访问后台的api接口url,其实都包含几个相同的部分:
- 通信协议:又称protocol,有很多通信协议,比如http,tcp/ip协议等等。
- 主机:也就是常说的host。
- 端口:即服务所监听的端口号。
- 资源路径:端口号后面的内容即是路径。
当在一个页面中发起一个新的请求时,如果通信协议、主机和端口,这三部分内容中的任意一个与原页面的不相同,就被称之为跨域访问。
如,在gin接口项目中,前端使用nodejs开发,运行在8080端口,我们访问的应用首页是:http://localhost:8080。 在使用gin框架开发的api项目中,服务端的监听端口为8090。
一个端口数8080,一个是8090,两者端口不同,因此按照规定,发生了跨域访问。
OPTIONS请求
前端vue开发的功能,使用axios发送POST登录请求。在请求时发生了跨域访问,因此浏览器为了安全起见,会首先发起一个请求测试一下此次访问是否安全,这种测试的请求类型为OPTIONS,又称之为options嗅探,同时在header中会带上origin,用来判断是否有跨域请求权限。
然后服务器相应Access-Control-Allow-Origin的值,该值会与浏览器的origin值进行匹配,如果能够匹配通过,则表示有跨域访问的权限。
跨域访问权限检查通过,会正式发送POST请求。
服务端设置跨域访问
可以在gin服务端,编写程序进行全局设置。通过中间件的方式设置全局跨域访问,用以返回Access-Control-Allow-Origin和浏览器进行匹配。
在服务端编写跨域访问中间件,如下:
// Cors 跨域访问:cross origin resource share
func Cors() gin.HandlerFunc {
return func(context *gin.Context) {
method := context.Request.Method
origin := context.Request.Header.Get("Origin")
var headerKeys []string
for k, _ := range context.Request.Header {
headerKeys = append(headerKeys, k)
}
headerStr := strings.Join(headerKeys, ",")
if headerStr != "" {
headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr)
} else {
headerStr = "access-control-allow-origin, access-control-allow-headers"
}
if origin != "" {
context.Writer.Header().Set("Access-Control-Allow-Origin", "*")
context.Header("Access-Control-Allow-Origin", "*") // 设置允许访问所有域
context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
context.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar")
context.Header("Access-Control-Max-Age", "172800")
context.Header("Access-Control-Allow-Credentials", "false")
context.Set("content-type", "application/json") // 设置返回格式是json
}
if method == "OPTIONS" {
context.JSON(http.StatusOK, "Options Request!")
}
//处理请求
context.Next()
}
}
需要在设置路由之前启用中间件:
...
//设置全局跨域访问
app.Use(Cors())
registerRouter(app)
app.Run(config.AppHost + ":" + config.AppPort)
...
服务器设置好跨域访问以后,重新启动服务器api程序,并在浏览器端重新访问。可以看到正常发送了OPTIONS嗅探后,正常发送了POST请求。
验证码的生成和验证
原理和安装
验证码的使用流程和原理为:在服务器端负责生成图形化验证码,并以数据流的形式供前端访问获取,同时将生成的验证码存储到全局的缓存中,在本案例中,我们使用Redis作为全局缓存,并设置缓存失效时间。当用户使用用户名和密码进行登录时,进行验证码验证。验证通过即可继续进行登录。
安装开源的验证码生成库:
go get -u github.com/mojocn/base64Captcha
生成和验证
本项目中采用的验证码的生成库支持三种验证码,分别是:audio,character和digit。我们选择character类型。
tool/Captcha.go
定义Captcha.go文件,实现验证码的生成和验证码函数的定义。在进行验证码生成时,默认提供验证码的配置,并生成验证码后返回给客户端浏览器。
package tool
import (
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"image/color"
)
type CaptchaResult struct {
Id string `json:"id"`
Base64Blob string `json:"base_64_blob"`
VertifyValue string `json:"vertify_value"`
}
// GenerateCaptchaHandler 生成验证码
func GenerateCaptchaHandler(ctx *gin.Context) {
//图形验证码的默认配置
parameters := base64Captcha.ConfigCharacter{
Height: 30,
Width: 100,
Mode: 3,
ComplexOfNoiseText: 0,
ComplexOfNoiseDot: 0,
IsUseSimpleFont: true,
IsShowHollowLine: false,
IsShowNoiseDot: false,
IsShowNoiseText: false,
IsShowSlimeLine: false,
IsShowSineLine: false,
CaptchaLen: 4,
BgColor: &color.RGBA{
R: 255,
G: 255,
B: 255,
A: 255,
},
}
captchaId, captchaInterfaceInstance := base64Captcha.GenerateCaptcha("", parameters)
base64blob := base64Captcha.CaptchaWriteToBase64Encoding(captchaInterfaceInstance)
captchaResult := CaptchaResult{Id: captchaId, Base64Blob: base64blob}
// 设置json响应
Success(ctx, map[string]interface{}{
"captcha_result": captchaResult,
})
}
func VertifyCaptcha(id string, value string) bool {
return base64Captcha.VerifyCaptcha(id, value)
}
controller/MemberController.go
//添加路由
func (mc *MemberController) Router(engine *gin.Engine) {
...
engine.GET("/api/captcha", mc.captcha)
engine.POST("/api/vertifycha", mc.vertifyCaptcha)
}
//校验验证码是否正确
func (mc *MemberController) vertifyCaptcha(context *gin.Context) {
var captcha tool.CaptchaResult
err := tool.Decode(context.Request.Body, &captcha)
if err != nil {
tool.Failed(context, "参数解析失败")
return
}
result := tool.VertifyCaptcha(captcha.Id, captcha.VertifyValue)
fmt.Println("登陆状态:", result)
}
func (mc *MemberController) captcha(context *gin.Context) {
tool.GenerateCaptchaHandler(context)
}
缓存到Redis
安装库
在项目中使用redis,需要安装go-redis库,可以在https://github.com/go-redis/redis中查看如何下载go-redis和配置。
增加Redis配置
在配置文件app.json中新增redis配置:
...
"redis_config": {
"addr": "127.0.0.1",
"port": "6379",
"password": "",
"db": 0
}
...
同时,新增RedisConfig结构体定义,如下所示:
type RedisConfig struct {
Addr string `json:"addr"`
Port string `json:"port"`
Password string `json:"password"`
Db int `json:"db"`
}
Redis初始化操作
进行了redis配置以后,需要对redis进行初始化。
tool/RedisStore.go
package tool
import (
"github.com/go-redis/redis"
"github.com/mojocn/base64Captcha"
"log"
"time"
)
type RedisStore struct {
client *redis.Client
}
var Redis_Store RedisStore
func InitRedisStore() *RedisStore {
config := GetConfig().RedisConfig
client := redis.NewClient(&redis.Options{
Addr: config.Addr + ":" + config.Port,
Password: config.Password,
DB: config.Db,
})
Redis_Store = RedisStore{client: client}
base64Captcha.SetCustomStore(&Redis_Store)
return &Redis_Store
}
func (rs *RedisStore) Set(id string, value string) {
if err := rs.client.Set(id, value, time.Minute*10).Err(); err != nil {
log.Println(err)
}
}
func (rs *RedisStore) Get(id string, clear bool) string {
val, err := rs.client.Get(id).Result()
if err != nil {
log.Println(err)
return ""
}
if clear {
if err := rs.client.Del(id).Err(); err != nil {
log.Println(err)
return ""
}
}
return val
}
对Redis进行初始化和定义完成以后,需要在main中调用一下初始化操作:
func main(){
...
//初始化redis配置
tool.InitRedisStore()
...
}
用户名密码登陆
此处程序登陆注册存在问题。
Param/LoginParam.go
package param
type LoginParam struct {
Name string `json:"name"`
Password string `json:"password"`
Id string `json:"id"`
Value string `json:"value"` //验证码输入值
}
controller/MemberController.go
engine.POST("/api/login_pwd", mc.nameLogin)
func (mc *MemberController) nameLogin(context *gin.Context) {
//解析用户传递参数
var loginParam param.LoginParam
err := tool.Decode(context.Request.Body, &loginParam)
fmt.Println(context.Request.Body)
if err != nil {
tool.Failed(context, "参数解析失败")
return
}
//验证验证码
validate := tool.VertifyCaptcha(loginParam.Id, loginParam.Value)
if !validate {
tool.Failed(context, "验证码错误")
return
}
//登陆
ms := service.MemberService{}
member := ms.Login(loginParam.Name, loginParam.Password)
if member.Id != 0 {
tool.Success(context, &member)
return
}
tool.Failed(context, "登陆失败")
}
service/MemberService.go
func (ms *MemberService) Login(name string, password string) *model.Member {
//是否存在当前用户信息
md := dao.MemberDao{tool.DbEngine}
member := md.Query(name, password)
if member.Id != 0 {
return member
}
//不存在,注册
user := model.Member{}
user.UserName = name
user.Password = tool.EncoderSha256(password)
user.RegisterTime = time.Now().Unix()
insertMember := md.InsertMember(user)
user.Id = insertMember
return &user
}
dao/MemberDao.go
涉及到操作数据库的两个方法分别是:Query和InsertMember方法。InsertMember方法之前已经编写过,只需要重新编写一个Query方法即可。
func (md *MemberDao) Query(name string, password string) *model.Member {
var member model.Member
_, err := md.Where("user_name = ? and password = ?", name, tool.EncoderSha256(password)).Get(&member)
if err != nil {
log.Fatal(err.Error())
return nil
}
return &member
}
tool/PasswordEncode.go
package tool
import (
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
)
func EncoderSha256(data string) string {
h := sha256.New()
h.Write([]byte(data))
sum := h.Sum(nil)
//由于是十六进制表示,因此需要进行转换
s := hex.EncodeToString(sum)
return string(s)
}
func Md5(data string) string {
w := md5.New()
io.WriteString(w, data)
bydate := w.Sum(nil)
result := fmt.Sprintf("%x", bydate)
return result
}
func Base64Encode(data string) string {
return base64.StdEncoding.EncodeToString([]byte(data))
}
func Base64Decode(data string) ([]byte, error) {
return base64.StdEncoding.DecodeString(data)
}
用户头像上传
此处程序返回用户ID存在问题。
本节开发上传一张图片到服务器,并保存成为用户的头像。
在文件上传过程中,后台服务器需要确认该头像文件是哪位用户上传的。前端在上传文件时,一并将用户id上传至服务器。服务器需要确认该用户是否已经登录,只有登录的用户才有权限上传。最通常的做法是通过session来获取用户是否已经登录,并进行权限判断。
Session功能集成
安装session库
go语言和gin框架都没有实现session库,可以通过第三方实现的session库来集成session功能。安装如下session库:
go get github.com/gin-contrib/sessions
初始化session
在项目中,集成session功能,首先要进行初始化。我们选择将session数据持久化保存到redis中,因此需要与redis相结合。
session功能初始化完成以后就可以使用了,session的使用主要有两个操作:set和get。在sessions库中,有对应的session.Set(key, value)和session.Get(key)方法来实现set和get操作。
为了方便session的set和get操作,在初始化完session后,另外封装session的set和get函数。
tool/SessionStore.go
package tool
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin"
"log"
)
func InitSession(engine *gin.Engine) {
config := GetConfig().RedisConfig
SessionStore, err := redis.NewStore(10, "tcp", config.Addr+":"+config.Port, config.Password, []byte("secret"))
if err != nil {
log.Fatal(err.Error())
}
engine.Use(sessions.Sessions("MySession", SessionStore))
}
func SetSess(ctx *gin.Context, key interface{}, value interface{}) error {
session := sessions.Default(ctx)
if session == nil {
return nil
}
session.Set(key, value)
return session.Save()
}
func GetSess(ctx *gin.Context, key interface{}) interface{} {
session := sessions.Default(ctx)
return session.Get(key)
}
用户登录添加session
cic 当用户进行登录,并登录成功后,选择将用户的信息保存在session中。在项目的需要登录才能使用的地方,可以进行权限的判断和拦截。
因此,在之前已经完成的登录功能方法中,添加登录操作成功后,将用户数据保存到sesion的操作。在MemberController中的nameLogin和smsLogin方法中,添加如下设置session的代码操作,具体调用如下所示:
...
//将用户信息保存到session中
sess, err := json.Marshal(member)
if err != nil {
fmt.Println("json Marshal error!")
return
}
if err := tool.SetSess(context, "user_16"+string(member.Id), sess); err != nil {
tool.Failed(context, "session set error")
return
}
...
集成session操作
在项目的入口main.go文件的main函数中,通过中间件调用开启session集成。main函数修改如下:
...
//集成session
app.Use(tool.InitSession())
...
文件上传Contoller实现
在MemberController中,创建uploadAvator方法,用于实现用户头像上传的业务流程控制。该方法其实主要有几个步骤:第一步是获取到用户端上传的文件,接下来将文件保存到对应的目录中,因为要知道该文件对应的是哪位用户的数据,因此需要将文件路径更新到用户数据库中的对应记录中。
//头像文件上传
func (mc *MemberController) UploadAvator(context *gin.Context) {
// 1.解析上传的参数:image-file , user-ID
userID := context.PostForm("user_id")
fmt.Println("userID >>", userID)
file, err := context.FormFile("avatar")
if err != nil {
tool.Failed(context, "avator decode error")
return
}
// 2.通过session判断用户是否已经登录
sess := tool.GetSess(context, "user_"+userID)
if sess == nil {
tool.Failed(context, "get user session error")
return
}
var member model.Member
json.Unmarshal(sess.([]byte), &member) //将session的内容设置为member的内容
// 3.将file保存到本地
fileName := "./uploadfile/" + strconv.FormatInt(time.Now().Unix(), 10) + file.Filename
if err := context.SaveUploadedFile(file, fileName); err != nil {
tool.Failed(context, "save avator error")
return
}
tool.Failed(context, "upload avator error")
// 5.返回结果
}
Dao层实现
//更新用户头像信息
func (md *MemberDao) UpdateMemberAvatar(userID int64, fileName string) int64 {
member := model.Member{Avatar: fileName}
res, err := md.Where("id = ?", userID).Update(&member)
if err != nil {
fmt.Println(err.Error())
return 0
}
return res
}
FastDFS分布式文件系统
在实际的开发中,涉及到文件上传的功能,往往单独搭建一个文件服务器用于文件的存储。因此我们接下来就搭建一个分布式的文件系统,并将已完成的文件上传功能进行优化,将文件存储到分布式文件系统中。
FastDFS介绍
目前已经有很多开源的分布式文件系统。而fastDFS正是其中一员,fastDFS是基于http协议的分布式文件系统,其设计理念是一切从简。也正是因为其简单至上的设计理念,使其运维和扩展变得更加简单,同时还具备高性能,高可靠,无中心以及免维护等特点。
FastDFS的特点
fastDFS分布式文件存储系统主要解决了海量数据的存储问题,利用fastDFS系统,特别适合系统中的中小文件的存储和在线服务。中小文件的范围大致为4KB ~ 500MB大小之间。
FastDFS的组件构成及工作原理
在fastDFS分布式文件存储系统中,由三中角色的组件组成,分别称之为:跟踪服务器(Tracker Server)、存储服务器(Storage Server)和客户端(Client)。
每个角色组件都有各自的作用和功能:
- 存储服务器:即Storage Server。存储服务器顾名思义就是用于存储数据的服务器,主要提供存储容量和备份服务。存储服务器为了保证数据安全不丢失,会多台服务器组成一个组,又称group。同一个组内的服务器上的数据互为备份。
- 跟踪服务器:英文称之为Tracker Server。跟踪服务器的主要作用是做调度工作,担负起均衡的作用;跟踪服务器主要负责管理所有的存储服务器,以及服务器的分组情况。存储服务器启动后会与跟踪服务器保持链接,并进行周期性信息同步。
- 客户端:主要负责上传和下载文件数据。客户端所部署的机器就是实际项目部署所在的机器。
FastDFS文件上传过程
- Storage Server会定期的向Tracker Server发送自己的存储信息。
- 当Tracker Server Cluster中的Tracker Server不止一个时,各个Tracker之间的关系是对等的,因此客户端上传时可以选择任意一个Tracker。
- 当Tracker收到客户端上传文件的请求时,会为该文件分配一个可以存储文件的group,当选定了group后就要决定给客户端分配group中的哪一个storage server。
- 当分配好storage server后,客户端向storage发送写文件请求,storage将会为文件分配一个数据存储目录。
- 然后为文件分配一个fileid,最后根据以上的信息生成文件名存储文件。
FastDFS文件下载过程
- 在下载文件时,客户端可以选择任意tracker server。
- tracker发送下载请求给某个tracker,并携带着文件名信息。
- tracker从文件名中解析出文件所存储的group、文件大小、创建时间等信息,然后为该请求选择一个storage用来服务下载的请求。
集成到Gin
安装fastdfs的golang库
在项目中进行文件上传编程实现,需要安装一个go语言库,该库名称为fdfs_client,通过如下命令进行安装。
go get github.com/tedcy/fdfs_client
编写fdfs.conf配置文件
在fdfs_client库中,提供对文件的上传和下载方法,其中文件上传支持两种方式。
要使用文件上传功能方法,首先需要构造一个fdfsClient实例。如同我们前文讲的fastDFS的组件构成一样,client需要连接tracker服务器,然后进行上传。
在构造fdfsClient实例时,首先需要编写配置文件fdfs.conf,在fdfs.conf文件中进行配置选项的设置:
tracker_server=127.0.0.1:22122
http_port=http://127.0.0.1:80
maxConns=100
在fdfs.conf配置文件中,配置了三个选项,分别是:
- tracker_server:跟踪服务器的ip和跟踪服务的端口
- http_port:配置了nginx服务器后的http访问地址和端口
- maxConns:最大连接数为100,默认值即可
在构造fdfsClient对象实例化时,会使用该文件。
文件上传编程实现
将文件上传功能作为全局的一个工具函数进行定义,实现文件上传功能,并返回保存后的文件的id。编程实现如下:
func UploadFile(fileName string) string {
client, err := fdfs_client.NewClientWithConfig("./config/fastdfs.conf")
defer client.Destory()
if err != nil {
fmt.Println(err.Error())
}
fileId, err := client.UploadByFilename(fileName)
if err != nil {
fmt.Println(err.Error())
}
return fileId
}
修改Controller文件上传方法
现在,已经接入了fastDFS文件系统,因此,对MemberController的uploadAvator方法进行修改。
修改思路:将客户端上传的文件,先保存在服务器目录下的uploadfile目录中,然后将文件的路径和名称作为参数传递到UploadFile函数中,进行上传。上传成功后,将保存到本地的uploadfile文件删除,并把保存到fastDFS系统的fileId更新到对应用户记录的数据库。最后拼接文件访问的全路径,返回给客户端。
用户信息查询
该获取用户信息的接口请求类型为GET类型,在该接口访问时,是根据当前登录用户的会话状态进行请求的。
所谓会话,其实就是客户端会存储登录用户信息的cookie。Cookie可以在服务端通过代码进行设置。
Cookie的设置
在用户调用登录功能时,完成cookie设置。
context.SetCookie("cookie_id", "login_success", 10*60, "/", "localohost", true, true)
- cookie_id:第一个参数表示的要即将设置的cookie的名称。
- login_success:第二个字段表示的是cookie里面设置的具体的值的内容。在实际的使用中,该值开发者可以自己生成。
- 10*60:第三个值表示的是cookie的有效时间,当该值<0时,会立刻清除cookie,当该值>0时,表示cookie数据的有效期会持续n秒。
- “/":该参数是一个可选项,表示cookie所在的目录。
- “localhost”:该参数表示cookie所在的域,可以理解为cookie的有效范围。
Cookie中间件
在设置了Cookie值以后,可以在服务端编写中间件获取Cookie信息。服务端编写的cookie处理逻辑如下。
const CookieName = "cookie_user"
const CookieTimeLength = 10 * 60
func CookieAuth(context *gin.Context) (*http.Cookie, error) {
cookie, err := context.Request.Cookie(CookieName)
if err == nil {
context.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
} else {
return nil, err
}
return cookie, nil
}
在需要登录才能访问的接口处理中,调用如上函数进行用户权限判断,以此来判断用户是否已经登录。
项目总结
参考链接: