Go Web 开发 Demo【用户登录、注册、验证】

06-27 1010阅读

前言

        这篇文章主要是学习怎么用 Go 语言(Gin)开发Web程序,前端太弱了,得好好补补课,完了再来更新。

1、环境准备

新建项目,生成 go.mod 文件:

Go Web 开发 Demo【用户登录、注册、验证】

出现报错:go: modules disabled by GO111MODULE=off; see 'go help modules',说明需要开启:

Go Web 开发 Demo【用户登录、注册、验证】

临时设置环境变量: 

set GO111MODULE=on # windows
export GO111MODULE=on # linux

 永久设置环境变量:

Go Web 开发 Demo【用户登录、注册、验证】

再次生成 go.mod 文件:

Go Web 开发 Demo【用户登录、注册、验证】

执行完毕,发现项目下生成 go.mod 文件:

Go Web 开发 Demo【用户登录、注册、验证】

这里的模块名称是我们自定义的,不是说非得和哪个目录或者项目名对应上!

2、用户注册

2.1、需求

  • 用户向地址 /register 发送POST请求(表单携带着 username、password、phone)
  • 后端处理请求(检查手机号位数、手机号是否已经被注册、用户名是否为空)

    2.2、需求实现

    2.2.1、判断手机号是否存在

    func isPhoneExist(db gorm.DB, phone string) bool {
    	user := User{}
    	db.Where("phone = ?", phone).First(user)
    	if user.ID != 0 {
    		return true
    	}
    	return false
    }

    2.2.2、生成随机10位的用户名

    func RandomString(length int) string {
    	var letters = []byte("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM")
    	result := make([]byte, length)
    	rand.Seed(time.Now().Unix())
    	for i := range result {
    		result[i] = letters[rand.Intn(len(letters))]
    	}
    	return string(result)
    }

    2.2.3、设置用户表结构体

            这个结构体的字段会对应用户表中的每个字段,我们会在初始化数据库的时候,使用 gorm.DB 的  AutoMigrate 方法自动帮我进行创建这个结构体对应的表。

    type User struct {
    	gorm.Model
    	Name     string `gorm:"type:varchar(20);not null"`
    	Phone    string `gorm:"type:varchar(110);not null,unique"`
    	Password string `gorm:"size:255;not null"`
    }

     其中 gorm.Model 的源码如下:

    Go Web 开发 Demo【用户登录、注册、验证】

    我们通过嵌套 gorm.Model 来给我们的表增加四个字段。

    2.2.4、获得数据库连接(gorm)

    func InitDB() *gorm.DB {
    	driverName := "mysql"
    	host := "127.0.0.1"
    	port := 3306
    	database := "go_web"
    	username := "root"
    	password := "xxxxx"
    	charset := "utf8mb4"
    	args := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=true", username, password, host, port, database, charset)
    	db, err := gorm.Open(driverName, args)
    	if err != nil {
    		panic("failed to connect database, err : " + err.Error())
    	}
    	db.AutoMigrate(&User{}) // 如果表不存在则自动创建
    	return db
    }

    2.3、完整代码

    package main
    import (
    	"fmt"
    	"github.com/gin-gonic/gin"
    	_ "github.com/go-sql-driver/mysql"
    	"github.com/jinzhu/gorm"
    	"math/rand"
    	"net/http"
    	"time"
    )
    type User struct {
    	gorm.Model
    	Name     string `gorm:"type:varchar(20);not null"`
    	Phone    string `gorm:"type:varchar(110);not null,unique"`
    	Password string `gorm:"size:255;not null"`
    }
    func main() {
    	db := InitDB()
    	defer db.Close()
    	engine := gin.Default()
    	engine.POST("/register", func(ctx *gin.Context) {
    		// 获取参数
    		name := ctx.PostForm("username")
    		phone := ctx.PostForm("phone")
    		password := ctx.PostForm("password")
    		// 数据验证
    		if len(phone) != 11 {
    			ctx.JSON(http.StatusUnprocessableEntity, gin.H{
    				"code": 422,
    				"msg":  "手机号必须为11位!",
    			})
    			return
    		}
    		if len(password)  
    

    2.4、测试

    Go Web 开发 Demo【用户登录、注册、验证】

    使用规范的用户信息再次注册:

    Go Web 开发 Demo【用户登录、注册、验证】

    3、项目重构

            上面我们把连接数据库和响应的代码都放到了一个文件中,显然后期随着业务代码越来越多开发起来越来越难以管理,所以我们这里需要对项目进行重构:

    3.1、util 层

    存放工具,比如我们上面的随机生成用户名方法

    package util
    import (
    	"math/rand"
    	"time"
    )
    func RandomString(length int) string {
    	var letters = []byte("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM")
    	result := make([]byte, length)
    	rand.Seed(time.Now().Unix())
    	for i := range result {
    		result[i] = letters[rand.Intn(len(letters))]
    	}
    	return string(result)
    }
    

    3.2、model 层

    存放结构体

    package model
    import "github.com/jinzhu/gorm"
    type User struct {
    	gorm.Model
    	Name     string `gorm:"type:varchar(20);not null"`
    	Phone    string `gorm:"type:varchar(110);not null,unique"`
    	Password string `gorm:"size:255;not null"`
    }
    

    3.3、common 层

    存放一些公共的方法,比如连接数据库工具

    package common
    import (
    	"com.lyh/goessential/model"
    	"fmt"
    	"github.com/jinzhu/gorm"
    )
    var DB *gorm.DB
    func InitDB() *gorm.DB {
    	driverName := "mysql"
    	host := "127.0.0.1"
    	port := 3306
    	database := "go_web"
    	username := "root"
    	password := "Yan1029."
    	charset := "utf8mb4"
    	args := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=true", username, password, host, port, database, charset)
    	db, err := gorm.Open(driverName, args)
    	if err != nil {
    		panic("failed to connect database, err : " + err.Error())
    	}
    	db.AutoMigrate(&model.User{}) // 如果表不存在则自动创建
    	DB = db
    	return db
    }
    func GetDB() *gorm.DB {
    	return DB
    }
    

    3.4、controller 层

            存放控制器,因为我们使用的 Gin 框架的请求方法都是函数式编程,它的第二个参数是一个处理请求的函数,所以控制器层我们存放的是业务模块对应的方法:

    package controller
    import (
    	"com.lyh/goessential/common"
    	"com.lyh/goessential/model"
    	"com.lyh/goessential/util"
    	"fmt"
    	"github.com/gin-gonic/gin"
    	"github.com/jinzhu/gorm"
    	"net/http"
    )
    func Register(ctx *gin.Context) {
    	DB := common.GetDB()
    	// 获取参数
    	name := ctx.PostForm("username")
    	phone := ctx.PostForm("phone")
    	password := ctx.PostForm("password")
    	// 数据验证
    	if len(phone) != 11 {
    		ctx.JSON(http.StatusUnprocessableEntity, gin.H{
    			"code": 422,
    			"msg":  "手机号必须为11位!",
    		})
    		return
    	}
    	if len(password)  
    

    注意:这里判断用户是否存在需要传入一个 user 地址,因为 user 是值类型,如果不传入地址,则进入方法后的操作无效。 

    3.5、routes.go

            所有的请求都将通过这个文件中的方法再传递给 main 方法,其实就是为了简化 main 方法所在go文件的代码量,方便管理和维护。所以它的包名也是 main,相当于它俩在一个文件内。

    package main
    import (
    	"com.lyh/goessential/controller"
    	"github.com/gin-gonic/gin"
    )
    func CollectRoute(engine *gin.Engine) *gin.Engine {
    	engine.POST("/register", controller.Register)
    	return engine
    }
    

    3.6、main.go

    这是程序的入口,现在我们已经把它彻底解脱出来了:

    package main
    import (
    	"com.lyh/goessential/common"
    	"github.com/gin-gonic/gin"
    	_ "github.com/go-sql-driver/mysql"
    )
    func main() {
    	db := common.InitDB()
    	defer db.Close()
    	engine := gin.Default()
    	engine = CollectRoute(engine)
    	panic(engine.Run()) // 默认端口 8080
    }
    

    测试

    因为我们有两个文件都是 main 包,所以我们最好使用命令启动:

    Go Web 开发 Demo【用户登录、注册、验证】

    4、密码加密以及登录测试

    4.1、注册加密

            在 controller 的注册方法( Register )中修改创建用户的代码,对将要插入数据库中的代码进行加密:

    	// 创建用户
    	hasedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    	if err != nil {
    		ctx.JSON(http.StatusInternalServerError, gin.H{
    			"code": 500,
    			"msg":  "加密错误",
    		})
    		return
    	}
    	newUser := model.User{
    		Name:     name,
    		Phone:    phone,
    		Password: string(hasedPassword),
    	}
    	DB.Create(&newUser)

    4.2、登录方法

    func Login(ctx *gin.Context) {
    	DB := common.GetDB()
    	// 获取参数
    	phone := ctx.PostForm("phone")
    	password := ctx.PostForm("password")
    	// 数据验证
    	if len(phone) != 11 {
    		ctx.JSON(http.StatusUnprocessableEntity, gin.H{
    			"code": 422,
    			"msg":  "手机号必须为11位!",
    		})
    		return
    	}
    	if len(password)  
    

    4.3、注册请求

            把我们的 Login 方法注册到 /login 地址(只需要在 routes.go 文件的 CollectRoute 函数中添加一行即可):

    Go Web 开发 Demo【用户登录、注册、验证】

    测试

    Go Web 开发 Demo【用户登录、注册、验证】

    查看数据库:

    Go Web 开发 Demo【用户登录、注册、验证】

    登录测试

    Go Web 开发 Demo【用户登录、注册、验证】

    5、jwt 实现用户认证

    jwt 地址:github.com/dgrijalva/jwt-go

    Go Web 开发 Demo【用户登录、注册、验证】

    5.1、发放 token

    在 common 包下来创建一个 jwt.go 文件,定义发放 token 的函数:

    package common
    import (
    	"com.lyh/goessential/model"
    	"github.com/dgrijalva/jwt-go"
    	"time"
    )
    var jwtKey = []byte("a_secret_crect")
    type Claims struct {
    	UserId uint
    	jwt.StandardClaims
    }
    func ReleaseToken(user model.User) (string, error) {
    	expirationTime := time.Now().Add(7 * 24 * time.Hour) // 设置token有效期7天
    	claims := &Claims{
    		UserId: user.ID,
    		StandardClaims: jwt.StandardClaims{
    			ExpiresAt: expirationTime.Unix(), // 过期时间
    			IssuedAt:  time.Now().Unix(),     // 发放的时间
    			Issuer:    "lyh",                 // 发送者
    			Subject:   "user token",          // 发送主题
    		},
    	}
    	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    	tokenString, err := token.SignedString(jwtKey)
    	if err != nil {
    		return "", err
    	}
    	return tokenString, nil
    }
    // 从 tokenString 中解析出 claims 然后返回
    func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
    	claims := &Claims{}
    	token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
    		return jwtKey, nil
    	})
    	return token, claims, err
    }
    

    5.2、设置返回 token

            在之前 controller 层下用户模块中的登录请求(/login)中设置返回 token(之前随便写了个 "11"):

    	// 返回 token 给前端
    	token, err := common.ReleaseToken(user)
    	if err != nil {
    		ctx.JSON(http.StatusInternalServerError, gin.H{
    			"code": 500,
    			"msg":  "系统异常",
    		})
    		log.Printf("token generate error : %v", err)
    		return
    	}
    	// 返回结果
    	ctx.JSON(200, gin.H{
    		"code": 200,
    		"data": gin.H{
    			"token": token,
    		},
    		"msg": "登录成功",
    	})
    

    测试登录用户,拿到 token :

    Go Web 开发 Demo【用户登录、注册、验证】

    token 由三部分组成:

    • 协议头(token 使用的加密协议)
    • 我们给token中存储的信息(解密后是 JSON 格式的数据)
    • 前两部分加上key通过hash后的值

      5.3、定义用户认证中间件

              如果不加中间件的话,前端请求时携带token,返回的 user 是 null ,因为我们没有往上下文存储 user 的信息。

              中间件为的是把前端请求时,authorization 中携带的 token 信息解析出来验证并保存到上下文。在 middleware 包下创建 AuthMiddleware.go:

      package middleware
      import (
      	"com.lyh/goessential/common"
      	"com.lyh/goessential/model"
      	"github.com/gin-gonic/gin"
      	"net/http"
      	"strings"
      )
      // 自定义中间件用于用户验证:相当于SpringBoot中的拦截器
      func AuthMiddleware() gin.HandlerFunc {
      	return func(ctx *gin.Context) {
      		// 获取 authorization header
      		tokenString := ctx.GetHeader("Authorization")
      		//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIsImV4cCI6MTcxNTE1MDIyNCwiaWF0IjoxNzE0NTQ1NDI0LCJpc3MiOiJseWgiLCJzdWIiOiJ1c2VyIHRva2VuIn0.C6yH99IZDjj6_FnpHaREVPmoCX82nYWv1OZao171iPg
      		// 验证格式
      		if tokenString == "" || !strings.HasPrefix(tokenString, "Bearer ") {
      			ctx.JSON(http.StatusUnauthorized, gin.H{
      				"code": 401,
      				"msg":  "权限不足",
      			})
      			ctx.Abort()
      			return
      		}
      		tokenString = tokenString[7:] // Bearer+' '一共7位
      		token, claims, err := common.ParseToken(tokenString)
      		if err != nil || !token.Valid {
      			ctx.JSON(http.StatusUnauthorized, gin.H{
      				"code": 401,
      				"msg":  "权限不足",
      			})
      			ctx.Abort()
      			return
      		}
      		// 验证通过后获取claims 中的 userId
      		userId := claims.UserId
      		DB := common.GetDB()
      		var user model.User
      		DB.First(&user, userId)
      		// 用户不存在
      		if user.ID == 0 {
      			ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足"})
      			ctx.Abort()
      			return
      		}
      		// 用户存在 将user信息写入上下文
      		ctx.Set("user", user)
      		ctx.Next()
      	}
      }
      

      添加路由并测试

      添加到我们的管理所有路由的文件(routes.go)中:

      Go Web 开发 Demo【用户登录、注册、验证】

      测试:

      Go Web 开发 Demo【用户登录、注册、验证】

      6、统一请求返回格式

              我们在学习 SpringBoot 项目的时候也进行了请求的统一返回格式:(code,data,other),这里也是一样,为的是简化开发,比如我们前面返回前端需要这样写:

          ctx.JSON(200, gin.H{
      		"code": 200,
      		"msg":  "注册成功",
      	})

      统一格式后我们只需要写个 200 和 "注册成功" 就可以了。

              此外,我们给前端返回的数据还有一些问题:比如把用户的全部信息都返回出去了(包括密码登隐私信息)

      6.1、数据传输对象(dto)

      创建一个包 dto 并创建 user_dto.go 用来将返回给前端的 user 转换为 userDto:

      package dto
      import (
      	"com.lyh/goessential/model"
      )
      type UserDto struct {
      	Name  string `json:"name"`
      	Phone string `json:"phone"`
      }
      // 将 user 转换为 userDto
      func ToUserDto(user model.User) UserDto {
      	return UserDto{
      		Name:  user.Name,
      		Phone: user.Phone,
      	}
      }
      

      修改 controller 中的 Info 方法:

      func Info(ctx *gin.Context) {
      	// 从上下文中获得用户的信息
      	user, _ := ctx.Get("user")
      	ctx.JSON(http.StatusOK, gin.H{"code": 200, "data": gin.H{"user": dto.ToUserDto(user.(model.User))}})
      }

      再次进行用户验证:

      Go Web 开发 Demo【用户登录、注册、验证】

      可以看到,这次返回的数据没有其它敏感信息。

      6.2、封装 HTTP 返回

      创建目录 response,并创建 response.go :        

      package response
      import (
      	"github.com/gin-gonic/gin"
      	"net/http"
      )
      // 这里的 code 是我们自定义的业务code
      func Response(ctx *gin.Context, httpStatus int, code int, data gin.H, msg string) {
      	ctx.JSON(httpStatus, gin.H{"code": code, "date": data, "msg": msg})
      }
      func Success(ctx *gin.Context, data gin.H, msg string) {
      	Response(ctx, http.StatusOK, 200, data, msg)
      }
      func Fail(ctx *gin.Context, data gin.H, msg string) {
      	Response(ctx, http.StatusOK, 400, data, msg)
      }
      

              定义了统一的前端返回类型之后,我们就可以开始修改之前的返回代码了,之前我们的 HTTP 返回都是通过 ctx.JSON(httpStatus,gin.H) 来返回的,现在我们需要都替换为我们自定义的返回格式,比如下面的:

          ctx.JSON(http.StatusUnprocessableEntity, gin.H{
      			"code": 422,
      			"msg":  "手机号必须为11位!",
      	})

      统一之后就清爽多了,而且不会存在前端拿一些 JSON 的属性却拿不到的情况。 

      response.Response(ctx, http.StatusUnprocessableEntity, 422, nil, "手机号必须为11位")

      7、从文件中读取配置(viper)

              上面我们的很多配置信息都是直接定义在代码中的(比如连接数据库需要的参数),这样很不好管理和维护,所以这里我们统一下配置源。

      7.1、安装 viper

      go get github.com/spf13/viper

      如果需要使用旧版本就去 go.mod 取修改版本号重新下载。

      7.2、编写配置文件(yml)

      在 config 目录下创建 application.yml:

      server:
        port: 1016
      datasource:
        driverName: mysql
        host: 127.0.0.1
        port: 3306
        database: go_web
        username: root
        password: Yan1029.
        charset: utf8mb4

      7.3、使用 viper 读取配置文件

      在 main 方法中添加读取配置文件的函数:

      package main
      import (
      	"com.lyh/goessential/common"
      	"github.com/gin-gonic/gin"
      	_ "github.com/go-sql-driver/mysql"
      	"github.com/spf13/viper"
      	"os"
      )
      func main() {
      	InitConfig()
      	db := common.InitDB()
      	defer db.Close()
      	engine := gin.Default()
      	engine = CollectRoute(engine)
      	port := viper.GetString("server.port")
      	if port != "" {
      		panic(engine.Run(":" + port))
      	}
      	panic(engine.Run()) // 默认端口 8080
      }
      func InitConfig() {
      	workDir, _ := os.Getwd()
      	viper.SetConfigName("application")
      	viper.SetConfigType("yml")
      	viper.AddConfigPath(workDir + "/config")
      	err := viper.ReadInConfig()
      	if err != nil {
      		panic(err)
      	}
      }
      

      修改 databse.go 中的 InitDB 方法:

          driverName := viper.GetString("datasource.driverName")
      	host := viper.GetString("datasource.host")
      	port := viper.GetInt("datasource.port")
      	database := viper.GetString("datasource.database")
      	username := viper.GetString("datasource.username")
      	password := viper.GetString("datasource.password")
      	charset := viper.GetString("datasource.charset")
      

      测试

      Go Web 开发 Demo【用户登录、注册、验证】

      数据库可以查询成功,配置成功。

      注意事项 

      1、gorm 版本问题

      最新版 gorm:

      Go Web 开发 Demo【用户登录、注册、验证】

      使用旧版本的 gorm:

      require (
          github.com/jinzhu/gorm v1.9.12
      )

      总结

              至此,我大概明白了 Go 语言怎么开发一个 Web 程序,也消除了我的很多疑虑,比如Java一个类就是一个文件,那Go语言怎么对项目进行分层架构等一些简单但又特别重要的内容。

              接下来,学学前端,至少了解怎么和后端交互,写一个功能完整的Web程序。 

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]