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

news/2024/10/18 0:35:52/

前言

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

1、环境准备

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

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

临时设置环境变量: 

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

 永久设置环境变量:

再次生成 go.mod 文件:

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

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

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.ModelName     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 的源码如下:

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

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

func InitDB() *gorm.DB {driverName := "mysql"host := "127.0.0.1"port := 3306database := "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 mainimport ("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.ModelName     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) < 6 {// gin.H 等同于 map[string]anyctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{"code": 422,"msg":  "密码不能少于6位!",})return}if len(name) == 0 {name = RandomString(10)}fmt.Println(name, phone, password)// 判断手机号是否存在if isPhoneExist(*db, phone) {ctx.JSON(http.StatusUnprocessableEntity, gin.H{"msg": "用户已存在,不允许重复注册",})return}// 创建用户newUser := User{Name:     name,Phone:    phone,Password: password,}db.Create(&newUser)// 返回结果ctx.JSON(200, gin.H{"msg": "注册成功",})})panic(engine.Run()) // 默认端口 8080
}func isPhoneExist(db gorm.DB, phone string) bool {user := User{}db.Where("phone = ?", phone).First(user)if user.ID != 0 {return true}return false
}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)
}func InitDB() *gorm.DB {driverName := "mysql"host := "127.0.0.1"port := 3306database := "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.4、测试

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

3、项目重构

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

3.1、util 层

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

package utilimport ("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 modelimport "github.com/jinzhu/gorm"type User struct {gorm.ModelName     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 commonimport ("com.lyh/goessential/model""fmt""github.com/jinzhu/gorm"
)var DB *gorm.DBfunc InitDB() *gorm.DB {driverName := "mysql"host := "127.0.0.1"port := 3306database := "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 = dbreturn db
}func GetDB() *gorm.DB {return DB
}

3.4、controller 层

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

package controllerimport ("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) < 6 {// gin.H 等同于 map[string]anyctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{"code": 422,"msg":  "密码不能少于6位!",})return}if len(name) == 0 {name = util.RandomString(10)}fmt.Println(name, phone, password)// 判断手机号是否存在if isPhoneExist(DB, phone) {ctx.JSON(http.StatusUnprocessableEntity, gin.H{"msg": "用户已存在,不允许重复注册",})return}// 创建用户newUser := model.User{Name:     name,Phone:    phone,Password: password,}DB.Create(&newUser)// 返回结果ctx.JSON(200, gin.H{"msg": "注册成功",})
}func isPhoneExist(db *gorm.DB, phone string) bool {user := model.User{}db.Where("phone = ?", phone).First(&user) // 这里的参数是地址if user.ID != 0 {return true}return false
}

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

3.5、routes.go

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

package mainimport ("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 mainimport ("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 包,所以我们最好使用命令启动:

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) < 6 {// gin.H 等同于 map[string]anyctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{"code": 422,"msg":  "密码不能少于6位!",})return}// 判断手机号是否存在user := model.User{}DB.Where("phone = ?", phone).First(&user)if user.ID == 0 {ctx.JSON(http.StatusUnprocessableEntity, gin.H{"msg": "用户不存在",})return}// 判断密码是否正确if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {ctx.JSON(http.StatusBadRequest, gin.H{"code": 400,"msg":  "密码错误",})return}// 返回 token 给前端token := "11"// 返回结果ctx.JSON(200, gin.H{"code": 200,"data": gin.H{"token": token,},"msg": "登录成功",})}

4.3、注册请求

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

测试

查看数据库:

登录测试

5、jwt 实现用户认证

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

5.1、发放 token

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

package commonimport ("com.lyh/goessential/model""github.com/dgrijalva/jwt-go""time"
)var jwtKey = []byte("a_secret_crect")type Claims struct {UserId uintjwt.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 :

token 由三部分组成:

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

5.3、定义用户认证中间件

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

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

package middlewareimport ("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 headertokenString := 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 中的 userIduserId := claims.UserIdDB := common.GetDB()var user model.UserDB.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)中:

 

测试:

6、统一请求返回格式

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

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

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

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

6.1、数据传输对象(dto)

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

package dtoimport ("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))}})
}

再次进行用户验证:

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

6.2、封装 HTTP 返回

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

package responseimport ("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: mysqlhost: 127.0.0.1port: 3306database: go_webusername: rootpassword: Yan1029.charset: utf8mb4

7.3、使用 viper 读取配置文件

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

package mainimport ("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")

测试

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

注意事项 

1、gorm 版本问题

最新版 gorm:

使用旧版本的 gorm:

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

总结

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

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


http://www.ppmy.cn/news/1448478.html

相关文章

网盘—下载文件

本文主要讲解网盘文件操作的下载文件部分&#xff0c;具体步骤如下&#xff1a; 目录 1、实施步骤 2、代码实现 2.1、添加下载文件的协议 2.2、添加下载文件函数 2.3、添加信号槽 2.4、实现槽函数 2.5、设置download状态 2.6、添加定义 2.7、服务器接收数据 2.8、添…

C语言中的指针常量和常量指针

指针常量和常量指针是C/C编程语言中两个重要的概念&#xff0c;它们都与指针有关&#xff0c;但具有不同的含义和用途。 1. 指针常量&#xff08;Pointer to Constant&#xff09; 指针常量指的是一个指针的值&#xff08;即它所指向的地址&#xff09;在初始化之后不能再被改…

vue 组件组件通信方法

1、父组件props传值给子组件。 子组件中定义props字段&#xff0c;类型为Array&#xff08;如需限制字段值类型&#xff0c;也可以定义为Object的形式&#xff09;。如下例子&#xff0c;父组件挂载子组件helloWorld&#xff0c;在组件标签上给title赋值&#xff0c;子组件hel…

【项目纪实】某国有航空公司人力资源系统诊断咨询项目

公司的人力资源管理问题一直都比较严重&#xff0c;比如人员冗余、员工工作积极性差等问题&#xff0c;虽然经过多次的管理尝试&#xff0c;存在的问题仍然没有缓解。华恒智信人力资源咨询公司的老师特别专业&#xff0c;帮我们系统、全面的诊断了人力资源管理上存在的问题&…

LabVIEW智能变电站监控系统设计与实现

LabVIEW智能变电站监控系统设计与实现 随着电力系统和智能化技术的快速发展&#xff0c;建立一个高效、可靠的变电站监控系统显得尤为重要。通过分析变电站监控系统的需求&#xff0c;设计了一个基于LabVIEW软件的监控平台。该平台利用虚拟仪器技术、传感器技术和无线传输技术…

Django后台项目开发实战六

日志记录 第六阶段 日志处理教程 Django 日志处理 我这里实现一个简单的日志&#xff0c;在 setting.py 文件添加日志 LOGGING {# 版本version: 1,# 是否禁止默认配置的记录器disable_existing_loggers: False,formatters: {simple: {format: %(asctime)s %(name)-12s %(linen…

JavaScript 的基本术语大全

文章目录 1、概述2、基本术语2.1、有效负载 (Payload)2.2、ReadableStream2.3、模块系统2.4、DOM (Document Object Model)2.5、事件 (Events)2.6、活动委托 (Event Delegation)2.7、内容安全策略 (CSP)2.8、渐进增强和优雅降级2.9、JSON (JavaScript Object Notation)2.10、AJ…

Web后端开发中对三层架构解耦之控制反转与依赖注入

内聚与耦合 内聚 比如说我们刚刚书写的员工的实现类 在这里我们仅仅书写的是和员工相关的代码 而与员工无关的代码都没有放到这里 说明内聚程度较高 耦合 以后软件开发要高内聚 低耦合 提高程序灵活性 扩拓展性 分析代码 如何解耦 创建容器 提供一个容器 存储东西 存储E…