2.DiTing聊天室-用户登录和注册 发表于 2024-04-01 | 更新于 2024-05-19
| 阅读量:
前言 Hello,我是单木。接下来我将会开启一个新的博客系列,使用 GoLang 从 0 到 1 实现一个IM聊天室项目。在上一篇文章中,我们已经实现了一个简单的聊天室 Demo ,接下来我们就要开始具体业务的实现部分。在这篇文章中,我们将会完成用户的登录和注册。
技术选型 HTTP 作为一个无状态的协议,是无法区别这个请求的发起者是谁的。为了能够区分不同的客户端,我们需要给每一个客户端设计一个标识,然后让客户端在请求的时候携带对应的标识,来达到区分不同用户的目标。 同时,对于每一个客户端,我们可能还需要维护与它相关的一些信息,比如用户名,状态等信息。在这个基础上衍生出了许多技术,现在比较主流的技术主要是三种:cookies、session 以及 JWT
Cookies cookie 是服务器发送到用户浏览器并保存在本地 的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。cookie 是不可跨域的, 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)。 优点:
结构简单。cookie 是一种基于文本的轻量结构,包含简单的键值对。
数据持久。虽然客户端计算机上 cookie 的持续时间取决于客户端上的 cookie 过期处理和用户干预, cookie 通常是客户端上持续时间最长的数据保留形式。
缺点:
大小受限。大多数浏览器对 cookie 的大小有5096的字节限制,尽管在当今新的浏览器和客户端设备版本中,支持8192字节的 cookie 大小已愈发常见。
非常不安全。 cookie 将数据裸露在浏览器中,这样大大增大了数据被盗取的风险,所以我们不应该将重要的数据放在 cookie 中,或者将数据加密处理。
容易被 csrf 攻击。可以设置 csrf_token 来避免攻击。
Session 在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。 优点:
Session 的信息存储在服务器,相比于 cookie 应在一定程度上加大了数据的完全性,相比于 JWT 方便进行管理,也就是说当用户登录和主动注销,只需要添加删除对应的 Session 就可以,这样管理起来很方便
缺点:
session 存储在服务端,这就增大了服务器的开销,当用户多的情况下,服务器性能应付大大降低。
因为是基于 cookie 来进行用户识别的 cookie 如果被截获,用户就会很容易受到跨站请求伪造的攻击。
用户认证之后,服务端做认证记录,如果认证的记录被保存的内存中的话, 这意味着用户下次请求还必须要请求在这台服务器上的,这样才能拿到授权的资源,这样在分布式的应用上,会限制负载均衡和集群水平拓展的能力。
JWT JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。 优点:
因为 JSON 的通用性,jwt可支持跨语言请求,像 Java、JavaScript、PHP 等很多语言都可以使用。
因为有了 payload 部分,所以 JWT 可以在自身存储一些其他业务逻辑所必要的非敏感信息。
便于传输,JWT 的构成非常简单,字节占用很小,所以它是非常便于传输的。
不需要在服务端保存会话信息,篮球服务器横向拓展。
缺点:
登录状态续签问题。比如设置 token 的有效期为一个小时,那么一个小时后,如果用户仍然在这个 web 应用上,这个时候当然不能指望用户再登录一次。目前可用的解决办法是在每次用户发现请求都返回一个新的token,前端再用这个 token 来替代旧的,这样每一次请求都会刷新 token 的有效期。但是这样需要频繁的生成 token 。另外一种方案是判断还有多久这个 token 会过期,在 token 快要过期时,返回一个新的 token 。
用户主动注销。 JWT 并不支持用户主动退出登录,客户端在别处使用 token 仍然可以正常访问。为了支持注销。有一个解决方案可用,就是在注销时将该 token 加入到服务器的 redis 黑名单中。
在现在的时间中,JWT 已经成为实际上的主要标准,因此 DiTing 中同样将采用 JWT 实现授权功能。
表结构设计 用户的登录和注册这个功能实现起来并不复杂,实际上就是给定前端提交用户的用户名和密码,在注册时把它记录到数据库中,在登录时根据用户名和密码来进行查询就行了。因此我们根据这个简单的创建一张表
1 2 3 4 5 6 7 8 9 10 11 12 CREATE TABLE `user` ( `id` bigint(20 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户id' , `name` varchar(20 ) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户昵称' , `password` varchar(20 ) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户密码' , `status` int (11 ) DEFAULT '0' COMMENT '使用状态 0.正常 1拉黑' , `create_time` datetime(3 ) NOT NULL DEFAULT CURRENT_TIMESTAMP(3 ) COMMENT '创建时间' , `update_time` datetime(3 ) NOT NULL DEFAULT CURRENT_TIMESTAMP(3 ) ON UPDATE CURRENT_TIMESTAMP(3 ) COMMENT '修改时间' , PRIMARY KEY (`id` ) USING BTREE, UNIQUE KEY `uniq_name` (`name` ) USING BTREE, KEY `idx_create_time` (`create_time` ) USING BTREE, KEY `idx_update_time` (`update_time` ) USING BTREE, ) ENGINE=InnoDB AUTO_INCREMENT=20000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户表' ;
但是,我希望在DiTing聊天室在之后不仅仅能通过用户的账号和密码进行登录,还可以通过微信进行扫码登录,以及在之后每个用户可能可以携带不同的头衔,徽章,归属地等等信息,因此我对这些字段做了一些拓展,最后的表结构为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 CREATE TABLE `user` ( `id` bigint(20 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户id' , `password` varchar(20 ) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户密码' , `name` varchar(20 ) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户昵称' , `avatar` varchar(255 ) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户头像' , `sex` int (11 ) DEFAULT NULL COMMENT '性别 1为男性,2为女性' , `open_id` char(32 ) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '微信openid用户标识' , `active_status` int (11 ) DEFAULT '2' COMMENT '在线状态 1在线 2离线' , `last_opt_time` datetime(3 ) NOT NULL DEFAULT CURRENT_TIMESTAMP(3 ) COMMENT '最后上下线时间' , `ip_info` json DEFAULT NULL COMMENT 'ip信息' , `item_id` bigint(20 ) DEFAULT NULL COMMENT '佩戴的徽章id' , `status` int (11 ) DEFAULT '0' COMMENT '使用状态 0.正常 1拉黑' , `create_time` datetime(3 ) NOT NULL DEFAULT CURRENT_TIMESTAMP(3 ) COMMENT '创建时间' , `update_time` datetime(3 ) NOT NULL DEFAULT CURRENT_TIMESTAMP(3 ) ON UPDATE CURRENT_TIMESTAMP(3 ) COMMENT '修改时间' , PRIMARY KEY (`id` ) USING BTREE, UNIQUE KEY `uniq_open_id` (`open_id` ) USING BTREE, UNIQUE KEY `uniq_name` (`name` ) USING BTREE, KEY `idx_create_time` (`create_time` ) USING BTREE, KEY `idx_update_time` (`update_time` ) USING BTREE, KEY `idx_active_status_last_opt_time` (`active_status` ,`last_opt_time` ) ) ENGINE=InnoDB AUTO_INCREMENT=20000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户表' ;
实现 配置管理 在实现具体业务之前,让我们先配置一下应用中必要的配置信息。还记得之前的目录结构吗,不记得也没关系,看这里
1 2 3 4 5 6 7 8 9 10 ├── conf #项目配置文件目录 │ └── config.yaml #例如:toml、yaml等等 ├── controllers #控制层目录 ├── services #服务层目录 ├── models #模型层目录,和数据库表的映射保存在这里 ├── routes #路由目录,负责分发请求 ├── logs #日志文件目录,保存项目运行过程中产生的日志。 ├── main.go #项目入口 ├── README.md ├── .gitignore
在config.yaml中我们先配置一下数据库的地址以及账号密码
1 2 3 4 5 6 7 8 mysql: host: localhost port: 4900 username: diting password: diting jwt: secret: 123456
接下来,我们还需要能够读取配置文件中的信息。在 DiTing 中采用viper来进行配置文件的读取
安装 1 2 go get github.com/spf13/viper
使用 接下来,我们在项目根目录中新建一个文件夹pkg,这个包用于存放公用的模块。在pkg下新建setting/setting.go,对viper进行初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package settingimport ( "github.com/spf13/viper" "log" ) func init () { viper.SetConfigName("config" ) viper.SetConfigType("yaml" ) viper.AddConfigPath("./conf" ) err := viper.ReadInConfig() if err != nil { log.Fatalf("Fail to parse 'conf/config.yml': %v" , err) } }
接下来我们只需要通过简单的get就可以读取配置文件中的内容
1 2 viper.GetString("mysql.username" )
使用Gen框架减少简单CURD 在项目中有大量简单的CURD操作,全部自己写未免太浪费时间,因此DiTing中采用Gen框架自动生成简单的CURD代码,加快开发效率。 什么是Gen框架呢?
Gen是一个基于GORM的安全ORM框架,其主要通过代码生成方式实现GORM代码封装。使用Gen框架能够自动生成Model结构体和类型安全的CRUD代码,极大提升CRUD效率。
安装
自动生成CURD代码 在项目根目录下新建cmd/gen这两个文件夹,在其中新建generate.go用于编写自动生成的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package mainimport ( _ "DiTing-Go/pkg/setting" "fmt" "github.com/spf13/viper" "gorm.io/driver/mysql" "gorm.io/gen" "gorm.io/gorm" ) var MySQLDSN = fmt.Sprintf("%s:%s@tcp(%s:%s)/diting?charset=utf8mb4&parseTime=True" , viper.GetString("mysql.username" ), viper.GetString("mysql.password" ), viper.GetString("mysql.host" ), viper.GetString("mysql.port" ))func connectDB (dsn string ) *gorm.DB { db, err := gorm.Open(mysql.Open(dsn)) if err != nil { panic (fmt.Errorf("connect db fail: %w" , err)) } return db } func main () { println (MySQLDSN) g := gen.NewGenerator(gen.Config{ OutPath: "./dal/query" , Mode: gen.WithDefaultQuery | gen.WithQueryInterface, }) g.UseDB(connectDB(MySQLDSN)) g.ApplyBasic(g.GenerateAllTable()...) g.Execute() }
执行这段代码,你会发现在你的项目根目录下多出了一个dal文件夹,其中包含两个子文件夹model和query,到这里,我们所需要的代码就已经自动生成好了。 接下来我们在dal下新建一个db.go,在这里初始化数据库连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package dalimport ( "fmt" "gorm.io/driver/mysql" "gorm.io/gorm" ) var DB *gorm.DBfunc ConnectDB (dsn string ) *gorm.DB { db, err := gorm.Open(mysql.Open(dsn)) if err != nil { panic (fmt.Errorf("connect db fail: %w" , err)) } return db }
JWT 一样的,先安装一下依赖的包
1 go get github.com/dgrijalva/jwt-go
在这里,需要实现 JWT 的生成和解析。在pkg下新建util/jwt.go,JWT 相关的逻辑都会在这里实现。这一块大家可以直接拷贝就完事了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package utilsimport ( "fmt" "github.com/dgrijalva/jwt-go" "github.com/spf13/viper" "time" ) var jwtSecret = []byte (viper.GetString("jwt.secret" ))type Claims struct { Username string `json:"username"` jwt.StandardClaims } func GenerateToken (username string ) (string , error ) { nowTime := time.Now() expireTime := nowTime.Add(3 * time.Hour) claims := Claims{ username, jwt.StandardClaims{ ExpiresAt: expireTime.Unix(), Issuer: "diting-go" , }, } tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token, err := tokenClaims.SignedString(jwtSecret) if err != nil { fmt.Errorf("generate token failed: %v" , err) } return token, err } func ParseToken (token string ) (*Claims, error ) { tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func (token *jwt.Token) (interface {}, error ) { return jwtSecret, nil }) if tokenClaims != nil { if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid { return claims, nil } } return nil , err }
统一返回值 为了便于和前端进行交互,我们规定一下 DiTing 中的响应格式
1 2 3 4 5 { "code" : xxx, "message" : "返回信息" , "data" : 返回的数据 }
为此我们简单实现封装几个方法,提供可以复用的代码。在pkg下新建resp/response.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package respimport ( "DiTing-Go/pkg/e" "github.com/gin-gonic/gin" ) type ResponseData struct { Code int `json:"code"` Message string `json:"message"` Data interface {} `json:"data"` } func ErrorResponse (c *gin.Context, message string ) { c.JSON(e.ERROR, ResponseData{ Code: e.ERROR, Message: message, Data: nil , }) } func SuccessResponse (c *gin.Context, data interface {}) { c.JSON(e.SUCCESS, ResponseData{ Code: e.SUCCESS, Message: "success" , Data: data, }) } func SuccessResponseWithMsg (c *gin.Context, msg string ) { c.JSON(e.SUCCESS, ResponseData{ Code: e.SUCCESS, Message: msg, Data: nil , }) }
实现登录和注册 编写路由 我们需要为登录和注册分别指定对应的连接地址,并绑定上对应的处理函数,我将这块逻辑统一放在initGin中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func initGin () { router := gin.Default() router.GET("/swagger/*any" , ginSwagger.WrapHandler(swaggerFiles.Handler)) apiPublic := router.Group("/api/public" ) { apiPublic.POST("/register" , service.Register) apiPublic.POST("/login" , service.Login) } err := router.Run(":5000" ) if err != nil { return } }
编写业务逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package serviceimport ( "DiTing-Go/dal" "DiTing-Go/dal/model" "DiTing-Go/dal/query" "DiTing-Go/pkg/resp" _ "DiTing-Go/pkg/setting" "DiTing-Go/pkg/utils" "context" "fmt" "github.com/gin-gonic/gin" "github.com/spf13/viper" ) var MySQLDSN = fmt.Sprintf("%s:%s@tcp(%s:%s)/diting?charset=utf8mb4&parseTime=True" , viper.GetString("mysql.username" ), viper.GetString("mysql.password" ), viper.GetString("mysql.host" ), viper.GetString("mysql.port" ))func init () { dal.DB = dal.ConnectDB(MySQLDSN).Debug() query.SetDefault(dal.DB) } func Register (c *gin.Context) { user := model.User{} if err := c.ShouldBind(&user); err != nil { resp.ErrorResponse(c, "参数错误" ) return } u := query.User exist, _ := u.WithContext(context.Background()).Where(u.Name.Eq(user.Name)).First() if exist != nil { resp.ErrorResponse(c, "用户名已存在" ) return } err := u.WithContext(context.Background()).Omit(u.OpenID).Create(&user) if err != nil { resp.SuccessResponseWithMsg(c, "注册成功" ) return } } func Login (c *gin.Context) { login := model.User{} if err := c.ShouldBind(&login); err != nil { resp.ErrorResponse(c, "参数错误" ) return } u := query.User user, _ := u.WithContext(context.Background()).Where(u.Name.Eq(login.Name), u.Password.Eq(login.Password)).First() if user == nil { resp.ErrorResponse(c, "用户名或密码错误" ) return } token, _ := utils.GenerateToken(user.Name) resp.SuccessResponse(c, token) }
到这里,登录和注册功能就已经全部实现好了
集成Swagger 为了便于和前端交互,我们可以集成一下 Swagger 框架,来帮助我们高效生成接口文档
安装 1 2 3 go get -u github.com/swaggo/swag/cmd/swaggo get -u -v github.com/swaggo/gin-swaggergo get -u -v github.com/swaggo/files
使用 接下来我们只需要按照规定给代码添加上对应的注释,在前面的登录和注册中也有对应的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func LoginHandler (c *gin.Context) { }
然后我们还需要再路由中添加上渲染 swagger 的路径,在前面的路由中同样有对应的例子。
1 r.GET("/swagger/*any" , gs.WrapHandler(swaggerFiles.Handler))
接下来我们需要再控制台执行以下命令
这个时候可能会遇到,见问题
1 swag : 无法将“swag”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
重启项目然后我们就可以通过对应的地址访问到我们的 swagger 文档了。
测试 测试用户注册,这里同样采用 Postman。 如果再次注册,则会提示用户已存在 测试用户登录
问题 swag : 无法将“swag”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。 这是因为GOPATH没有被加入到系统路径,只需要添加即可。 首先查看GOPATH,然后把它添加到环境变量中
1 2 3 4 5 PS D:\code\DiTing-Go> go env ... set GOPATH=xxxxxxx ...
点关注,不迷路 好了,以上就是这篇文章的全部内容了,如果你能看到这里,非常感谢你的支持! 如果你觉得这篇文章写的还不错, 求点赞 👍 求关注 ❤️ 求分享 👥 对暖男我来说真的 非常有用!!! 白嫖不好,创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见! 如果本篇博客有任何错误,请批评指教,不胜感激 ! 本文的完整代码可以在 https://github.com/danmuking/DiTing-Go 中查看,欢迎各位人才 Star⭐。如果想要加入这个项目或者有任何建议,欢迎联系