2022-10-19 使用GoLang语言Gin+Gorm+JWT+Redis实现简单的登录

基本项目结构

image.png

配置解析

package config

import (
    "admin/com.piaomiao/common/constants"
    "admin/com.piaomiao/common/log"
    "fmt"
    "github.com/garyburd/redigo/redis"
    "github.com/spf13/viper"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "gorm.io/gorm/schema"
    "math/rand"
    "os"
    "strconv"
    "time"
)

var DB *gorm.DB
var RDB redis.Conn
var conf *Conf
var env *Env

func IniEnv() {
    log.NewLog(constants.INFO).Info("初始化开发环境..")
    //获取项目的执行路径
    path, err := os.Getwd()
    if err != nil {
        panic(err)
    }
    config := viper.New()
    //设置读取的文件路径
    config.AddConfigPath(path + "/com.piaomiao/config")
    //设置读取的文件名
    config.SetConfigName("env")
    viper.SetConfigType("yml")
    log.NewLog(constants.INFO).Info("开始加载配置信息...")
    //尝试进行配置读取
    if err := config.ReadInConfig(); err != nil {
        panic(err)
    }
    // 将配置文件读取到结构体中
    err = config.Unmarshal(&env)
    if err != nil {
        fmt.Println(err.Error())
    }
    //获取全部文件内容
    log.NewLog(constants.INFO).Info("当前开发环境为:[" + env.Server.Environment + "]")
    iniConf()
}

// 加载配置信息
func iniConf() {
    log.NewLog(constants.INFO).Info("初始化配置信息..")
    //获取项目的执行路径
    path, err := os.Getwd()
    if err != nil {
        panic(err)
    }
    config := viper.New()
    //设置读取的文件路径
    config.AddConfigPath(path + "/com.piaomiao/config")
    //设置读取的文件名
    env := "conf-" + env.Server.Environment
    config.SetConfigName(env)
    viper.SetConfigType("yml")
    log.NewLog(constants.INFO).Info("开始加载配置信息...")
    //尝试进行配置读取
    if err := config.ReadInConfig(); err != nil {
        panic(err)
    }
    // 将配置文件读取到结构体中
    err = config.Unmarshal(&conf)
    if err != nil {
        fmt.Println(err.Error())
    }
    //获取全部文件内容
    //fmt.Println("all settings: ", config.AllSettings())
    //根据内容类型,解析出不同类型
    //fmt.Printf("conf: %#vn", conf.Settings.Server.Port)
    initMySQL()
    initRedis()
    log.NewLog(constants.INFO).Info("配置信息加载结束...")
}

func initMySQL() {
    log.NewLog(constants.INFO).Info("初始化数据库...")
    var err error
    DB, err = gorm.Open(mysql.New(mysql.Config{
        // url
        DSN: conf.Settings.Database.Source,
        // string 类型字段的默认长度
        DefaultStringSize: 191,
        // 禁用datetime经度,MySQL 5.6之前的数据库不支持
        DisableDatetimePrecision: true,
        // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
        DontSupportRenameIndex: true,
        // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
        DontSupportRenameColumn: true,
        // 根据当前 MySQL 版本自动配置
        SkipInitializeWithVersion: false,
    }),
        &gorm.Config{
            // 日志的输出级别
            Logger: logger.Default.LogMode(logger.Info),
            NamingStrategy: schema.NamingStrategy{
                // 使用单数表名
                SingularTable: true,
            },
            //在 AutoMigrate 或 CreateTable 时,GORM 会自动创建外键约束
            DisableForeignKeyConstraintWhenMigrating: false,
        })
    if err != nil {
        log.NewLog(constants.ERROR).Info("MSQL数据库连接失败..", err)
        panic(err)
    }
    log.NewLog(constants.INFO).Info("初始化数据库结束...")
    // 设置连接池
    if DB != nil {
        log.NewLog(constants.INFO).Info("初始化连接池...")
        sqlDB, err := DB.DB()
        if err != nil {
            panic(err)
        }
        sqlDB.SetMaxIdleConns(conf.Settings.Database.MaxIdleCount)
        sqlDB.SetMaxOpenConns(conf.Settings.Database.MaxOpenConns)
        // 连接可复用的最大时间
        sqlDB.SetConnMaxLifetime(60 * time.Minute)
        err = sqlDB.Ping()
        if err != nil {
            panic(err)
        }
        log.NewLog(constants.INFO).Info("初始化连接池结束...", DB)
    }
}

// 初始化redis配置信息
func initRedis() {
    log.NewLog("info").Info("初始化Redis...")
    addr := conf.Settings.Redis.Host + ":" + strconv.Itoa(conf.Settings.Redis.Port)
    RD, err := redis.Dial("tcp", addr, redis.DialPassword(conf.Settings.Redis.Password))
    if err != nil {
        log.NewLog(constants.ERROR).Error("Redis连接失败...", err)
        panic(err)
    }
    RDB = RD
    rand.Seed(time.Now().UnixNano())
    log.NewLog(constants.INFO).Info("初始化Redis结束...", RDB)
}

路由配置

package router

import (
    "admin/com.piaomiao/middle"
    loginController "admin/com.piaomiao/sys/controller"
    labelController "admin/com.piaomiao/ysqu/controller"
    swaggerFiles "github.com/swaggo/files"
    gs "github.com/swaggo/gin-swagger"
    // 生成doc
    _ "admin/docs"

    "github.com/gin-gonic/gin"
)

func Router() {
    router := gin.Default()
    router.Use(
        // 全局异常
        middle.Recover,
    )
    router.GET("/swagger/*any", gs.WrapHandler(swaggerFiles.Handler))
    v1 := router.Group("v1")
    {
        v1.GET("/captcha", loginController.Captcha)
        v1.POST("/login", loginController.Login)
        labelRouter := v1.Group("/label")
        {
            labelRouter.POST("/list", labelController.LabelPageList)
            labelRouter.POST("/add", labelController.Add)
        }
    }
    // 需要认证的路由
    router.Use(middle.JWTAuth())
    router.Run(":8201")
}

// 每个路由都对应一个具体的函数操作,从而实现了对user的增,删,改,查操作
//func InitPage(c *gin.Context) {
//  c.JSON(http.StatusOK, gin.H{
//      "message": "OK!",
//  })
//}

登录请求对象

package vo

type LoginVO struct {
    Username string `form:"UserName" json:"username" binding:"required"`
    Password string `form:"Password" json:"password" binding:"required"`
}

登录接口

package controller

import (
    "admin/com.piaomiao/common/constants"
    e "admin/com.piaomiao/common/error"
    "admin/com.piaomiao/common/log"
    "admin/com.piaomiao/sys/service"
    "admin/com.piaomiao/util"
    "github.com/gin-gonic/gin"
    "net/http"
)

// Login 登录
// @Tags 登陆
// @Tag 登录
// @Author piaomiao
// @Description 登录
// @Summary 登录
// @Produce  json
// @Param loginVO body string true "用户名密码"
// @Success 200 {string} string json "{"code":200,"msg":"success","token":token}"
// @Router /login [post]
func Login(c *gin.Context) {
    token, _ := service.LoginDo().Login(c)
    c.JSON(http.StatusOK, gin.H{
        "msg":   constants.SUCCESS,
        "token": token,
    })
}

业务层

package service

import (
    "admin/com.piaomiao/base/service"
    "admin/com.piaomiao/common/constants"
    e "admin/com.piaomiao/common/error"
    "admin/com.piaomiao/sys/domain"
    "admin/com.piaomiao/sys/model/vo"
    "admin/com.piaomiao/util"
    "github.com/gin-gonic/gin"
)

type SysLoginFunc interface {
    Login(c *gin.Context) domain.SysUser
}

type SysLoginFuncImpl struct {
    service.BaseService
}

func LoginDo() SysLoginFuncImpl {
    return SysLoginFuncImpl{}
}

func (l SysLoginFuncImpl) Login(c *gin.Context) (token string, err error) {
    var loginVO vo.LoginVO
    c.ShouldBindJSON(&loginVO)
    if !util.Verify(loginVO.UUID, loginVO.Code, true) {
        err = l.NewErr(e.CAPTCHA_ERR_CODE, e.CAPTCHA_ERR)
        panic(err)
    }
    var user domain.SysUser
    tx := l.LoadDB().Model(&user)
    tx.Where("username = ?", loginVO.Username)
    tx.Where("status = ?", 2)
    err = tx.Find(&user).Error
    // 查询
    if err != nil {
        err = l.NewErr(e.USER_NIL_OR_STOP_ERR_CODE, e.USER_NIL_OR_STOP_ERR)
        panic(err)
    }
    // 校验密码
    _, err = domain.CompareHashAndPassword(user.Password, loginVO.Password)
    if err != nil {
        err = l.NewErr(e.USER_PASSWORD_ERR_CODE, e.USER_PASSWORD_ERR)
        panic(err)
    }
    // 生成JWT  token
    token, err = util.GenToken(user.UserId, user.Username)
    // 保存到redis
    cmd, _ := l.Cache().Get(constants.LOGIN_TOKEN_KEY + token)
    if cmd != nil {
        l.Cache().Del(constants.LOGIN_TOKEN_KEY + token)
    }
    l.Cache().SetEx(constants.LOGIN_TOKEN_KEY+token, user, 1440)
    return token, err
}

JWT配置来生成token

package util

import (
    "errors"
    "github.com/golang-jwt/jwt"
    "time"
)

// MyClaims 自定义声明结构体并内嵌jwt.StandardClaims
// jwt包自带的jwt.StandardClaims只包含了官方字段
// 我们这里需要额外记录一个username字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type MyClaims struct {
    UserID   int    `json:"user_id"`
    Username string `json:"username"`
    jwt.StandardClaims
}

const TokenExpireDuration = time.Hour * 2

var mySecret = []byte("qsxzceujnh")

// GenToken 生成JWT
func GenToken(userID int, username string) (string, error) {
    // 创建一个我们自己的声明的数据
    c := MyClaims{
        userID,
        // 自定义字段
        username,
        jwt.StandardClaims{
            // 过期时间
            ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
            // 签发人
            Issuer: "piaomiao",
        },
    }
    // 使用指定的签名方法创建签名对象
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
    // 使用指定的secret签名并获得完整的编码后的字符串token
    return token.SignedString(mySecret)
}

// ParseToken 解析JWT
func ParseToken(tokenString string) (*MyClaims, error) {
    // 解析token
    var mc = new(MyClaims)
    token, err := jwt.ParseWithClaims(tokenString, mc, func(token *jwt.Token) (i interface{}, err error) {
        return mySecret, nil
    })
    if err != nil {
        return nil, err
    }
    if token.Valid { // 校验token
        return mc, nil
    }
    return nil, errors.New("invalid token")
}

// 续签
func RenewToken(claims *MyClaims) (string, error) {
    // 若token过期不超过10分钟则给它续签
    if withinLimit(claims.ExpiresAt, 600) {
        return GenToken(claims.UserID, claims.Username)
    }
    return "", errors.New("登录已过期")
}

// 计算过期时间是否超过l
func withinLimit(s int64, l int64) bool {
    e := time.Now().Unix()
    return e-s < l
}

Redis缓存信息

package util

import (
    "admin/com.piaomiao/config"
    "bytes"
    "encoding/gob"
    "github.com/garyburd/redigo/redis"
    "strconv"
)

type RedisFunc interface {
    RedisCache() RedisCache
    Set(key string, val interface{}, exTime int)
    Get(key string) (interface{}, error)
    CacheToStruct(key string, toStruct interface{})
}

type RedisCache struct {
}

//func Redis() RedisCache {
//  return RedisCache{}
//}

func (r RedisCache) RedisCache() RedisCache {
    return RedisCache{}
}

// 设置缓存
func (r RedisCache) Set(key string, val interface{}) {
    _, err := config.RDB.Do("Set", key, val)
    if err != nil {
        return
    }
}

// 设置缓存+过期时间
func (r RedisCache) SetEx(key string, val interface{}, exTime int) {
    _, err := config.RDB.Do("setex", key, val, strconv.Itoa(exTime))
    if err != nil {
        return
    }
}

// 获取缓存
func (r RedisCache) Get(key string) (interface{}, error) {
    result, err := config.RDB.Do("Get", string)
    return result, err
}

// 删除缓存
func (r RedisCache) Del(key string) {
    config.RDB.Do("Del", key)
}

// 获取缓存转结构体
func (r RedisCache) CacheToStruct(key string, toStruct interface{}) (bool, error) {
    result, err := r.Get(key)
    rebytes, _ := redis.Bytes(result, err)
    //进行gob序列化
    reader := bytes.NewReader(rebytes)
    dec := gob.NewDecoder(reader)
    err = dec.Decode(&toStruct)
    return true, err
}

通过GIN自带中间件进行token拦截校验

package middle

import (
    "admin/com.piaomiao/common/constants"
    e "admin/com.piaomiao/common/error"
    "admin/com.piaomiao/common/log"
    "admin/com.piaomiao/sys/domain"
    util "admin/com.piaomiao/util"
    "net/http"
    "runtime/debug"
    "strings"

    "github.com/gin-gonic/gin"
)

// 免登录接口列表
var notAuthArr = map[string]string{
    "/swagger/*any": "1",
    "/v1/login":     "1",
    "/v1/captcha":   "1",
}

func JWTAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        log.NewLog(constants.INFO).Info("登录校验..")
        // 释放接口
        inWhite := notAuthArr[c.Request.URL.Path]
        if inWhite == "1" {
            return
        }
        auth := c.Request.Header.Get(constants.JWT_TOKEN)
        if len(auth) == 0 {
            c.Abort()
            log.NewLog(constants.INFO).Info(e.USER_NOT_LOGIN_NOT_AUTH)
            c.String(http.StatusOK, e.USER_NOT_LOGIN_NOT_AUTH)
            return
        }
        // 校验token,只要出错直接拒绝请求
        claims, err := util.ParseToken(auth)
        if err != nil {
            if strings.Contains(err.Error(), "expired") {
                // 若过期,调用续签函数
                newToken, _ := util.RenewToken(claims)
                if newToken != "" {
                    // 续签成功給返回头设置一个newtoken字段
                    c.Header(constants.NEW_JWT_TOKEN, newToken)
                    c.Request.Header.Set(constants.JWT_TOKEN, newToken)
                    // 重新缓存
                    log.NewLog(constants.INFO).Info("[刷新缓存Token]----:", auth)
                    var user domain.SysUser
                    util.RedisCache{}.CacheToStruct(constants.LOGIN_TOKEN_KEY+auth, user)
                    util.RedisCache{}.RedisCache().Del("loginUser")
                    util.RedisCache{}.RedisCache().Del(constants.LOGIN_TOKEN_KEY + auth)
                    util.RedisCache{}.RedisCache().SetEx(constants.LOGIN_TOKEN_KEY+newToken, user, 1440)
                    // 缓存当前登录用户信息
                    util.RedisCache{}.RedisCache().Set("loginUser", user)
                    c.Next()
                    return
                }
            }
            // Token验证失败或续签失败直接拒绝请求
            c.Abort()
            log.NewLog(constants.ERROR).Error(e.USER_LOGIN_EXPIRE)
            c.JSON(http.StatusOK, e.USER_LOGIN_EXPIRE)
            return
        } else {
            if find := strings.Contains(c.Request.URL.Path, "/login"); find {
                log.NewLog(constants.INFO).Info("[用户登录成功]----登录用户为:", claims.Username)
            } else {
                log.NewLog(constants.INFO).Info("[用户请求接口验证通过]----访问接口为:", c.Request.URL.Path)
            }
        }
        c.Next()
    }
}
func Recover(c *gin.Context) {
    defer func() {
        if r := recover(); r != nil {
            //打印错误堆栈信息
            log.NewLog(constants.ERROR).Error("panic: %v\n", r)
            debug.PrintStack()
            //封装通用json返回
            code, msg := errorToString(r)
            c.JSON(http.StatusOK, gin.H{
                "code": code,
                "msg":  msg,
            })
            //终止后续接口调用,不加的话recover到异常后,还会继续执行接口里后续代码
            c.Abort()
        }
    }()
    //加载完 defer recover,继续后续接口调用
    c.Next()
}

// recover错误,转string
func errorToString(r interface{}) (int32, string) {
    switch v := r.(type) {
    case error:
        return e.ES().DecodeErr(v)
    default:
        return 1001, r.(string)
    }
}

使用Swagger文档访问

image.png

登录成功

image.png
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容