最近学习go语言,学习gin框架。目前已经知道gin的基本用法后,想找一个开源的项目来通过读代码的方式,详细学习下gin框架。一定要带着目的来学习,我的目的就是:学习gin后端项目布局,api设计,jwt鉴权,数据库设计,中间件使用。
开源项目链接:gin-vue-admin

在把项目部署起来后,可以通过查看前端代码/浏览器开发者模式来查看如何调用api的,以此来理解整个流程。

首先学习jwt鉴权相关。jwt鉴权就是通过用户密码,发送给服务器后,服务器在校验通过会返回一个token值。后续请求服务器时带上这个token值,服务器就可以通过身份识别。

1 查看运行流程

可以直接在登录页面进行刷新,会请求/api/base/captcha 接口,方法为GET

在这里插入图片描述

此接口的主要目的是为了获取验证码,返回的data结果如下:

{
  "captchaId": "qncrO0JmNABRyeHcxOFa",
  "picPath": "xxxxxxx(省略)",
  "captchaLength": 6
}

可见返回的信息中主要是三个值:

  • captchaId: 验证码id
  • picPath: 图片
  • captchaLength: 验证码长度

来输入用户密码验证码后,发送给服务端。在点击发送后,可以看到,服务器请求了/api/base/login接口,方法为POST

在这里插入图片描述
查看请求体为:

{
    "username":"admin",
    "password":"123456",
    "captcha":"954566",
    "captchaId":"qncrO0JmNABRyeHcxOFa"
}

这里采用了明文传输密码,在实际开发的过程中要避免这样,会引起安全风险。解决方案有很多在此不做过多赘述

里边主要包含了四个参数:

  • username:用户名
  • password: 密码
  • captcha: 验证码
  • captchaId: 验证码id

通过验证码和验证码id可以确保验证法是否输入正确,通过用户名和密码可以判定是否密码输入正确。

而此接口返回的数据为:

{
    "code":0,
    "data":{
        "user":{
            "ID":1,
            "CreatedAt":"2022-07-10T21:29:00.512+08:00",
            "UpdatedAt":"2022-07-10T21:29:00.514+08:00",
            "uuid":"2885a149-ea02-498e-9ee2-c71d5a53dfd0",
            "userName":"admin",
            "nickName":"超级管理员",
            "sideMode":"dark",
            "headerImg":"https://qmplusimg.henrongyi.top/gva_header.jpg",
            "baseColor":"#fff",
            "activeColor":"#1890ff",
            "authorityId":888,
            "authority":{
                "CreatedAt":"2022-07-10T21:29:00.35+08:00",
                "UpdatedAt":"2022-07-10T21:29:00.52+08:00",
                "DeletedAt":null,
                "authorityId":888,
                "authorityName":"普通用户",
                "parentId":0,
                "dataAuthorityId":null,
                "children":null,
                "menus":null,
                "defaultRouter":"dashboard"
            },
            "authorities":[
                {
                    "CreatedAt":"2022-07-10T21:29:00.35+08:00",
                    "UpdatedAt":"2022-07-10T21:29:00.52+08:00",
                    "DeletedAt":null,
                    "authorityId":888,
                    "authorityName":"普通用户",
                    "parentId":0,
                    "dataAuthorityId":null,
                    "children":null,
                    "menus":null,
                    "defaultRouter":"dashboard"
                },
                {
                    "CreatedAt":"2022-07-10T21:29:00.35+08:00",
                    "UpdatedAt":"2022-07-10T21:29:00.524+08:00",
                    "DeletedAt":null,
                    "authorityId":8881,
                    "authorityName":"普通用户子角色",
                    "parentId":888,
                    "dataAuthorityId":null,
                    "children":null,
                    "menus":null,
                    "defaultRouter":"dashboard"
                },
                {
                    "CreatedAt":"2022-07-10T21:29:00.35+08:00",
                    "UpdatedAt":"2022-07-10T21:29:00.522+08:00",
                    "DeletedAt":null,
                    "authorityId":9528,
                    "authorityName":"测试角色",
                    "parentId":0,
                    "dataAuthorityId":null,
                    "children":null,
                    "menus":null,
                    "defaultRouter":"dashboard"
                }
            ],
            "phone":"17611111111",
            "email":"333333333@qq.com",
            "enable":1
        },
        "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVVUlEIjoiMjg4NWExNDktZWEwMi00OThlLTllZTItYzcxZDVhNTNkZmQwIiwiSUQiOjEsIlVzZXJuYW1lIjoiYWRtaW4iLCJOaWNrTmFtZSI6Iui2hee6p-euoeeQhuWRmCIsIkF1dGhvcml0eUlkIjo4ODgsIkJ1ZmZlclRpbWUiOjg2NDAwLCJleHAiOjE2NTgwNjQ5MzIsImlzcyI6InFtUGx1cyIsIm5iZiI6MTY1NzQ1OTEzMn0.0JydtFnsbQk8GL0sHBsIBjm7tM_enzOn_m_a3MV1YfI",
        "expiresAt":1658064932000
    },
    "msg":"登录成功"
}

这个login主要返回了用户的信息和token。 其中用户信息包含了用户个人信息和权限相关信息。因此,要想了解gin服务器是怎么验证用户信息,并且返回这些信息的,就需要来查看这个/api/base/login接口是如何实现的了

2 login接口实现

首先在源代码中找到login接口实现的代码:

func (b *BaseApi) Login(c *gin.Context) {
	var l systemReq.Login
	_ = c.ShouldBindJSON(&l)
	if err := utils.Verify(l, utils.LoginVerify); err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	if store.Verify(l.CaptchaId, l.Captcha, true) {
		u := &system.SysUser{Username: l.Username, Password: l.Password}
		if user, err := userService.Login(u); err != nil {
			global.GVA_LOG.Error("登陆失败! 用户名不存在或者密码错误!", zap.Error(err))
			response.FailWithMessage("用户名不存在或者密码错误", c)
		} else {
			if user.Enable != 1 {
				global.GVA_LOG.Error("登陆失败! 用户被禁止登录!")
				response.FailWithMessage("用户被禁止登录", c)
				return
			}
			b.TokenNext(c, *user)
		}
	} else {
		response.FailWithMessage("验证码错误", c)
	}
}

首先用将传入的参数专为systemReq.Login对象,验证码检查通过后,就开始验证用户名和密码。在均验证通过后,就返回用户信息和token

在这里,我主要想了解的地方有两点:

  1. 服务端是如何针对用户传入的用户名和密码进行验证的?
  2. 如何生成token的?

针对这两个问题,就需要接着深入研究代码了

2.1 验证用户名密码是否正确

func (userService *UserService) Login(u *system.SysUser) (userInter *system.SysUser, err error) {
	if nil == global.GVA_DB {
		return nil, fmt.Errorf("db not init")
	}

	var user system.SysUser
	err = global.GVA_DB.Where("username = ?", u.Username).Preload("Authorities").Preload("Authority").First(&user).Error
	if err == nil {
		if ok := utils.BcryptCheck(u.Password, user.Password); !ok {
			return nil, errors.New("密码错误")
		}

		var SysAuthorityMenus []system.SysAuthorityMenu
		err = global.GVA_DB.Where("sys_authority_authority_id = ?", user.AuthorityId).Find(&SysAuthorityMenus).Error
		if err != nil {
			return
		}

		var MenuIds []string

		for i := range SysAuthorityMenus {
			MenuIds = append(MenuIds, SysAuthorityMenus[i].MenuId)
		}

		var am system.SysBaseMenu
		ferr := global.GVA_DB.First(&am, "name = ? and id in (?)", user.Authority.DefaultRouter, MenuIds).Error
		if errors.Is(ferr, gorm.ErrRecordNotFound) {
			user.Authority.DefaultRouter = "404"
		}
	}

	return &user, err
}

可以看出,首先通过通过查询数据库来获取到system.SysUser对象,对象内容具体如下:

type SysUser struct {
	global.GVA_MODEL
	UUID        uuid.UUID      `json:"uuid" gorm:"comment:用户UUID"`                                                           // 用户UUID
	Username    string         `json:"userName" gorm:"comment:用户登录名"`                                                        // 用户登录名
	Password    string         `json:"-"  gorm:"comment:用户登录密码"`                                                             // 用户登录密码
	NickName    string         `json:"nickName" gorm:"default:系统用户;comment:用户昵称"`                                            // 用户昵称
	SideMode    string         `json:"sideMode" gorm:"default:dark;comment:用户侧边主题"`                                          // 用户侧边主题
	HeaderImg   string         `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` // 用户头像
	BaseColor   string         `json:"baseColor" gorm:"default:#fff;comment:基础颜色"`                                           // 基础颜色
	ActiveColor string         `json:"activeColor" gorm:"default:#1890ff;comment:活跃颜色"`                                      // 活跃颜色
	AuthorityId uint           `json:"authorityId" gorm:"default:888;comment:用户角色ID"`                                        // 用户角色ID
	Authority   SysAuthority   `json:"authority" gorm:"foreignKey:AuthorityId;references:AuthorityId;comment:用户角色"`
	Authorities []SysAuthority `json:"authorities" gorm:"many2many:sys_user_authority;"`
	Phone       string         `json:"phone"  gorm:"comment:用户手机号"`                     // 用户手机号
	Email       string         `json:"email"  gorm:"comment:用户邮箱"`                      // 用户邮箱
	Enable      int            `json:"enable" gorm:"default:1;comment:用户是否被冻结 1正常 2冻结"` //用户是否被冻结 1正常 2冻结
}

func (SysUser) TableName() string {
	return "sys_users"
}

其中AuthoritiesAuthority两个字段代表sys_authorities表格,然后在代码中通过gorm框架的Preload字段来做联表查询。在Login方法的第9行就是判断密码是否正确。是通过密码明文和数据库中的密文进行对比所得的。

然后就是检测当前用户对配置的默认路由是否有权限,若无权限就返回404

最终都正常情况下,会返回system.SysUser类型对象

2.2 生成token

在此之前,需要了解下jwt的原理,可以查看此博客: JWT详解

对于生成token,主要是如下的代码:

// 登录以后签发jwt
func (b *BaseApi) TokenNext(c *gin.Context, user system.SysUser) {
	j := &utils.JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)} // 唯一签名
	claims := j.CreateClaims(systemReq.BaseClaims{
		UUID:        user.UUID,
		ID:          user.ID,
		NickName:    user.NickName,
		Username:    user.Username,
		AuthorityId: user.AuthorityId,
	})
	token, err := j.CreateToken(claims)
	if err != nil {
		global.GVA_LOG.Error("获取token失败!", zap.Error(err))
		response.FailWithMessage("获取token失败", c)
		return
	}
	if !global.GVA_CONFIG.System.UseMultipoint {
		response.OkWithDetailed(systemRes.LoginResponse{
			User:      user,
			Token:     token,
			ExpiresAt: claims.StandardClaims.ExpiresAt * 1000,
		}, "登录成功", c)
		return
	}

//省略。。。
}

首先获取配置文件中的SigningKey,然后通过这个Singkey来创建claims,即jwt中的Payload, 包含uuid,id,Nickname,Username,AuthorityId等信息。

而代码中封装的token对象为:

type Token struct {
	Raw       string                 // The raw token.  Populated when you Parse a token
	Method    SigningMethod          // The signing method used or to be used
	Header    map[string]interface{} // The first segment of the token
	Claims    Claims                 // The second segment of the token
	Signature string                 // The third segment of the token.  Populated when you Parse a token
	Valid     bool                   // Is the token valid?  Populated when you Parse/Verify a token
}

然后具体创建token代码如下:

// 创建一个token
func (j *JWT) CreateToken(claims request.CustomClaims) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(j.SigningKey)
}

//--------------------

func NewWithClaims(method SigningMethod, claims Claims) *Token {
	return &Token{
		Header: map[string]interface{}{
			"typ": "JWT",
			"alg": method.Alg(),
		},
		Claims: claims,
		Method: method,
	}
}


func (t *Token) SigningString() (string, error) {
	var err error
	var jsonValue []byte

	if jsonValue, err = json.Marshal(t.Header); err != nil {
		return "", err
	}
	header := EncodeSegment(jsonValue)

	if jsonValue, err = json.Marshal(t.Claims); err != nil {
		return "", err
	}
	claim := EncodeSegment(jsonValue)

	return strings.Join([]string{header, claim}, "."), nil
}


func (t *Token) SignedString(key interface{}) (string, error) {
	var sig, sstr string
	var err error
	if sstr, err = t.SigningString(); err != nil {
		return "", err
	}
	if sig, err = t.Method.Sign(sstr, key); err != nil {
		return "", err
	}
	return strings.Join([]string{sstr, sig}, "."), nil
}

token主要分为Header,Payload,Signature,上面代码对Header和Payload进行了赋值.然后SigningString方法对Header和Payload进行Base64编码后通过"."来连接。token.SignedString是对刚才base64编码后的Header,Payload字段组合按照HS256算法进行加密得到Signature,最终将三部分组合就是token。

Logo

前往低代码交流专区

更多推荐