在Gin下实现简单的注册登录和状态管理
概述
实现
使用viper管理配置并初始化
JSON
、TOML
、YAML
、HCL
、envfile
等格式的配置文件;这一步我们想要做的,是读取配置文件信息,用这些配置信息连接MySQL数据库、初始化路由、指定端口后启动gin框架等这些初始化操作。如下这个配置文件dev.config.yaml
,内容大致就是项目基本信息跟Mysql配置。name: "basicproject"
mode: "dev"
port: 8085
version: "v0.0.1"
mysql:
host: "127.0.0.1"
port: 3306
user: "root"
password: "12345678"
dbname: "test"
max_open_conns: 200
max_idle_conns: 50
我们在新建完项目后安装viper
和gin
。
go get github.com/spf13/viper
go get github.com/gin-gonic/gin
var Conf = new(AppConfig)
type AppConfig struct {
Mode string `mapstructure:"mode"`
Port int `mapstructure:"port"`
Name string `mapstructure:"name"`
Version string `mapstructure:"version"`
*Mysql `mapstructure:"mysql"`
}
type Mysql struct {
Host string `mapstructure:"host"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DB string `mapstructure:"dbname"`
Port int `mapstructure:"port"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
}
func Init() error {
// 设置viper读取配置文件的路径
viper.SetConfigFile("./config/dev.config.yaml")
// WatchConfig用于实时监控配置文件的变化
viper.WatchConfig()
// OnConfigChange是一个回调函数,在配置文件发生变化时被调用
viper.OnConfigChange(func(in fsnotify.Event) {
log.Println("配置文件发生了变化")
if err := viper.Unmarshal(&Conf); err != nil {
return
}
})
//ReadInConfig 将从磁盘和键/值存储中发现并加载配置文件,在定义的路径之一中进行搜索
if err := viper.ReadInConfig(); err != nil {
panic(fmt.Errorf("Read Config failed err %v", err))
}
// Unmarshal 将配置解组到结构体中。确保结构字段上的标签设置正确
if err := viper.Unmarshal(&Conf); err != nil {
panic(fmt.Errorf("unmarshal to Conf failed ,err %v", err))
}
log.Println("load config file success!")
return nil
}
注册功能的实现
.
├── config
│ └── dev.config.yaml
├── controller
│ └── user.go
├── dao
│ └── mysql
│ ├── code.go
│ ├── mysql.go
│ └── user.go
├── go.mod
├── go.sum
├── logic
│ └── user.go
├── main.go
├── models
│ └── user.go
├── router
│ └── routers.go
└── setting
└── setting.go
controller
层用于接收处理前端发送过来的请求,并解析请求参数;若请求参数无误则继续调用logic(Service)
层的接口来控制业务逻辑,最终返回响应。logic层会调用对与数据库直接交互的DAO
层实现CRUD功能。Model包含一系列模型对象实体,通常用于定义数据表中的字段。controller
层校验注册请求的参数,确认请求参数后交给logic
层,logic
层需要检查两个密码是否相同,需要调用DAO
层确认邮箱是否已经存在;确认好没问题后就可以开始注册了。注册这里会用到Snowflake
生成用户ID,使用bcrypt
加密用户的原始密码。-
1. 生成盐(Salt);首先Bcrypt会生成一个随机数作为盐。盐在密码学中指的是在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程叫做“加盐”。
-
2. 哈希密码;Bcrypt会将这个盐与用户的明文密码进行组合,通过哈希算法进行多次散列,生成一个哈希值。哈希的次数由saltRounds参数确定,参数越高,加密的安全性越高,但相应加密的时间也越长。
-
3. 生成最终的加密密码。最后bcrypt会将盐和哈希值组合在一起形成最终密码。就是将这个密码存储在数据库中。
我们的目录结构如下,安装bcrypt
和sonwflake
。
go get github.com/bwmarrin/snowflake
go get golang.org/x/crypto/bcrypt
├── pkg
│ ├── bcrypt
│ │ └── bcrypt.go
│ └── snowflake
│ └── gen_id.go
snowflake
,snowflake.NewNode(1)
中1
是节点的ID,使用node.Generate()
方法,在此节点上生成一个新的唯一ID,这个ID是一个2的64次方位的整数,换算成10进制就是19位数字。func GenId() (id string) {
node, err := snowflake.NewNode(1)
if err != nil {
fmt.Println(err)
return
}
id = node.Generate().String()
// 1702147424044191744
return id
}
bcrypt
这边主要实现两个功能,一是实现密码加密,注册时对用户原始密码进行加密,二是实现密码对比,用于用户登录时,通过对比加密请求的密码与数据库存储的密码是否一致验证正确性。密码加密需要用到bcrypt
的GenerateFormPassword
方法,密码对比需要用到CompareHashAndPassword
。// 密码加密
func GetPwd(password string) (hashStr string, err error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
hashStr = string(hash)
return hashStr, err
}
// 密码比对
func ComparePwd(PwdBeforeEncryption, EncryptedPassword string) (result bool) {
// PwdBeforeEncryption for the databases
if err := bcrypt.CompareHashAndPassword(
[]byte(PwdBeforeEncryption), []byte(EncryptedPassword)); err != nil {
return false
} else {
return true
}
}
跨域
问题。跨域是由浏览器同源策略造成的。[同源策略是浏览器对JavaScript实施的一种安全限制,只允许脚本在同一源(协议、域名、端口三者都相同)下进行读写操作。这是出于安全考虑,防止恶意脚本对用户数据进行操作。这需要用到第三方的包github.com/gin-contrib/cors
,这是一个用于处理跨域问题的中间件。
go get github.com/gin-contrib/cors
下载安装,在执行到需要的路由前使用这个中间件。设置完成就可以了。这里设置了Default
默认的允许策略,也可以根据自定义设置允许的策略。使用跨域插件之后就能顺利注册了,可以看到id使用snowflake
生成的,原始密码使用过bcrypt
加密。r := gin.New()
// 使用跨域的中间件
r.Use(cors.Default()
v1 := r.Group("/api/v1")
{
xxx
}
- - - - - - - - - - - - - -
func Cors() gin.HandlerFunc {
c := cors.Config{
AllowAllOrigins: true,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH"}, //允许的方法
AllowHeaders: []string{"Content-Type", "Access-Token", "Authorization"}, //允许设置的头部
MaxAge: 6 * time.Hour, // 设置过期时间
}
return cors.New(c)
}
登录和登录状态的管理
CompareHashAndPassword
方法对比是否一致。思路也很简单所以不再阐述,这里要说说登录后的状态管理。登录状态管理可以让用户在一段时间内使用网站不需要每次都登录。实现登录状态一般有两种思路,一种是使用session
,另外一种是jwt
。sessionID
。服务器会将sessionID
以cookie
的方式发送给浏览器。当浏览器再次访问服务器时,会将sessionID
发送过来,服务器根据sessionID
就找到对应的session对象来验证登录状态。但session
一般存储在服务器内存中,当用户增多时可能会对服务器造成压力。"/getSession"
负责给浏览器返回设置cookie session,"/login"
使用了session验证的中间件,通过中间件认证后返回welcome
字样,若没通过中间件校验则终止返回请求错误。我们使用cookie editor插件看到没有设置cookie,访问api/v2/login
返回没有登录。func InitSession(r *gin.Engine) {
//初始化cookie存储引擎,还有其他不同的存储引擎如redis...
//"my_secret"是用于加密的密钥
store := cookie.NewStore([]byte("my_secret"))
r.Use(sessions.Sessions("user-session", store))
}
- - - - - - - - - - -
session.InitSession(r)
v2 := r.Group("/api/v2")
{
v2.GET("/getsession", controller.HandleGetSession)
v2.GET("/login", session.SessionMiddleware(), controller.HandleTestSession)
}
// controller的HandleGetSession函数,设置cookie session
func HandleGetSession(c *gin.Context) {
session := sessions.Default(c)
// 设置cookie session
session.Set("user-session", "username")
session.Save()
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
}
- - - - - - - - -
// middleware
func SessionMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("user-session")
if user == nil {
c.JSON(http.StatusBadRequest, gin.H{"message": "没有登录"})
c.Abort()
return
}
c.Next()
}
}
这时候我们请求一次/getsession
获取cookie session,再打开cookie editor
插件就看到设置到cookie user-session了,此时再请求一次/login
就能成功登录了。
package JWT
import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"github.com/pkg/errors"
"net/http"
"strings"
"time"
)
// 定义加密的key
var jwtKey = []byte("my_secret")
// MyClaims 定义jwt对象struct
type MyClaims struct {
Email string `json:"email"`
jwt.RegisteredClaims
}
// 定义jwt中间件
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
/*
1. 检查token是否为空
2. 检查token的格式是否正确
3. 分割token并解析是否正确
*/
token := c.Request.Header.Get("Authorization")
if token == "" {
c.JSON(http.StatusOK, gin.H{
"message": "请求未携带token, 无法访问",
})
c.Abort()
return
}
parts := strings.SplitN(token, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusOK, gin.H{
"message": "token请求格式有误",
})
c.Abort()
return
}
claims, err := ParseToken(parts[1])
if err != nil {
c.JSON(http.StatusOK, gin.H{
"message": "无效的token",
})
c.Abort()
return
}
//将用户名设置到上下文
c.Set("email", claims.Email)
c.Next()
}
}
// 解析token
func ParseToken(tokenStr string) (myclaims *MyClaims, err error) {
token, err := jwt.ParseWithClaims(tokenStr, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
return nil, err
}
if myclaims, ok := token.Claims.(*MyClaims); ok && token.Valid {
return myclaims, nil
}
return nil, errors.New("invalid token")
}
// 生成token
func GenToken(email string) (tokenStr string, err error) {
expirationTime := time.Now().Add(10 * time.Hour)
claims := &MyClaims{Email: email, RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
}}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtKey)
}
写在最后
https://github.com/FengZeHe/LearnGolang/tree/main/project/BasicProject
原文始发于微信公众号(ProgrammerHe):在Gin下实现简单的注册登录和状态管理
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/207902.html