基本项目结构
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