在Gin下实现简单的注册登录和状态管理

在Gin下实现简单的注册登录和状态管理

概述

在gin框架下实现简单的注册登录和登录后的状态管理功能。项目使用viper管理配置文件,使用RESTful风格的API接口,注册功能会涉及基础的随机ID生成,密码加密,开发时会遇到的前后端跨域问题;登录成功后会涉及到登录的状态管理,技术上会使用到session和jwt两种方式,并作为中间件使用在项目上。

实现

使用viper管理配置并初始化

Viper是适用于Go应用程序的完整配置解决方案。它可以处理各种类型的配置需求和格式,包括设置默认值、从多种配置文件和环境变量中读取配置信息。viper可以读取JSONTOMLYAMLHCLenvfile等格式的配置文件;这一步我们想要做的,是读取配置文件信息,用这些配置信息连接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

我们在新建完项目后安装vipergin

go get github.com/spf13/viper

go get github.com/gin-gonic/gin
安装好viper后写好配置文件并填好配置文件的位置并读取内容。写好之后在main函数里启动一下就可以了。
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
}

在Gin下实现简单的注册登录和状态管理


注册功能的实现

.
├── 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包含一系列模型对象实体,通常用于定义数据表中的字段。

在Gin下实现简单的注册登录和状态管理

实现注册功能的思路是这样的,从前端发送过来注册请求,里面包含邮箱、密码和确认密码。在controller层校验注册请求的参数,确认请求参数后交给logic层,logic层需要检查两个密码是否相同,需要调用DAO层确认邮箱是否已经存在;确认好没问题后就可以开始注册了。注册这里会用到Snowflake生成用户ID,使用bcrypt加密用户的原始密码。
Snowflake是Twitter开源的分布式ID生成算法,其结果是一个long型的ID。它的核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是01。这样可以确保每个ID都是唯一的。

在Gin下实现简单的注册登录和状态管理

Bcrypt是一种密码哈希算法,由Niels Provos和David Mazières基于Blowfish密码设计。它的名称“bcrypt”由两部分组成:b代表Blowfish,crypt是Unix密码系统使用的哈希函数的名称。Bcrypt的加密流程主要有以下几个步骤:
  1. 1. 生成盐(Salt);首先Bcrypt会生成一个随机数作为盐。盐在密码学中指的是在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程叫做“加盐”。

  2. 2. 哈希密码;Bcrypt会将这个盐与用户的明文密码进行组合,通过哈希算法进行多次散列,生成一个哈希值。哈希的次数由saltRounds参数确定,参数越高,加密的安全性越高,但相应加密的时间也越长。

  3. 3. 生成最终的加密密码。最后bcrypt会将盐和哈希值组合在一起形成最终密码。就是将这个密码存储在数据库中。

我们的目录结构如下,安装bcryptsonwflake

go get github.com/bwmarrin/snowflake
go get golang.org/x/crypto/bcrypt
├── pkg
│   ├── bcrypt
│   │   └── bcrypt.go
│   └── snowflake
│       └── gen_id.go
首先看看生成随机ID的snowflakesnowflake.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这边主要实现两个功能,一是实现密码加密,注册时对用户原始密码进行加密,二是实现密码对比,用于用户登录时,通过对比加密请求的密码与数据库存储的密码是否一致验证正确性。密码加密需要用到bcryptGenerateFormPassword方法,密码对比需要用到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,这是一个用于处理跨域问题的中间件。

在Gin下实现简单的注册登录和状态管理

使用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)
}

在Gin下实现简单的注册登录和状态管理

登录和登录状态的管理

登录功能实际上只是将前端请求的密码通过相同的方式加密一遍,使用CompareHashAndPassword方法对比是否一致。思路也很简单所以不再阐述,这里要说说登录后的状态管理。登录状态管理可以让用户在一段时间内使用网站不需要每次都登录。实现登录状态一般有两种思路,一种是使用session,另外一种是jwt
Session也称为“会话控制”,是服务器为了保存用户状态而创建的一个特殊的对象。当浏览器第一次访问服务器时,服务器会创建一个session对象,这个对象有一个唯一的id,通常被称为sessionID。服务器会将sessionIDcookie的方式发送给浏览器。当浏览器再次访问服务器时,会将sessionID发送过来,服务器根据sessionID就找到对应的session对象来验证登录状态。但session一般存储在服务器内存中,当用户增多时可能会对服务器造成压力。
另外一种方式叫做JWT(JSON Web Token),它定义了一种紧凑的、自包含的方式,用于在各方之间作为JSON对象安全地传输信息;JWT可以被验证和信任,因为它是数字签名的。JWT由三部分组成:Header(头部)、Payload(载荷)和Signature(签名)。JWT的一个优点就是它不需要在服务器上存储任何用户状态信息,因此服务器不需要为每个用户保存session,从而减轻服务器的负担。
这里使用cookie session的方式实现,首先初始化cookie存储引擎,使用”my_secret”作为加密的密钥。现在设置路由,"/getSession"负责给浏览器返回设置cookie session,"/login"使用了session验证的中间件,通过中间件认证后返回welcome字样,若没通过中间件校验则终止返回请求错误。我们使用cookie editor插件看到没有设置cookie,访问api/v2/login返回没有登录。

在Gin下实现简单的注册登录和状态管理

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就能成功登录了。

在Gin下实现简单的注册登录和状态管理

在Gin下实现简单的注册登录和状态管理

然后是JWT的方式,需要定义加密的key,定义jwt的对象,要有生成token的方法,以及解析token的方法。在前端发送请求时要验证头部是否带有token,若携带了token还需要认证token格式是否正确。之后再对Token的第二部分进行解析验证。如果验证通过,则将解析过后用户的email放到上下文中给下面的程序继续使用。如下图没有携带token中间件不会通过。

在Gin下实现简单的注册登录和状态管理

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)
}
程序在登录后会返回一个jwt token,用户拿着这个token就能访问资源了。

在Gin下实现简单的注册登录和状态管理

在Gin下实现简单的注册登录和状态管理

写在最后

本人是新手小白,如果这篇笔记中有任何错误或不准确之处,真诚地希望各位读者能够给予批评和指正。谢谢!
练习的代码放在这里
https://github.com/FengZeHe/LearnGolang/tree/main/project/BasicProject

原文始发于微信公众号(ProgrammerHe):在Gin下实现简单的注册登录和状态管理

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/207902.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!