14. 登录功能实现

本文详细介绍了如何在Go语言的Web应用中实现登录功能,包括添加路由、Controller处理登录请求、进行业务逻辑验证(包括密码验证和用户存在性检查)、使用JWT进行鉴权以及DAO层的数据库操作。最后展示了如何编译和测试这一过程。
摘要由CSDN通过智能技术生成

有了基本脚手架之后,我们在上节很容易的就完成了注册功能。登录功能与之类似,按部就班实现即可。

登录后,期望在一段时间内不需要继续登录也能访问其他一些需要登录的接口,这时候就涉及到鉴权啦,我们可以使用jwt实现。
在这里插入图片描述

一、添加路由

router/route.go

v1 := r.Group("/api/v1")

// 注册
v1.POST("/signup", controller.SignUpHandler)
// 登录
v1.POST("/login", controller.LoginHandler)

二、controller添加处理Handler

路由过来之后,自然是需要有相应的controller层处理handler的,我们这里定义为LoginHandler,由于与user相关,因此放到controller/user.go中,与注册的逻辑在同一个文件中

controller/user.go

// LoginHandler 登录
func LoginHandler(c *gin.Context) {
	// 1.获取请求参数及参数校验
	p := new(models.ParamLogin)
	if err := c.ShouldBindJSON(p); err != nil {
		// 请求参数有误,直接返回响应
		zap.L().Error("Login with invalid param", zap.Error(err))
		// 判断err是不是validator.ValidationErrors 类型
		errs, ok := err.(validator.ValidationErrors)
		if !ok {
			ResponseError(c, CodeInvalidParam)
			return
		}
		ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans)))
		return
	}
	// 2.业务逻辑处理
	user, token, err := logic.Login(p)
	if err != nil {
		zap.L().Error("logic.Login failed", zap.String("username", p.Username), zap.Error(err))
		if errors.Is(err, mysql.ErrorUserNotExist) {
			ResponseError(c, CodeUserNotExist)
			return
		}
		ResponseError(c, CodeInvalidPassword)
		return
	}

	// 3.返回响应
	ResponseSuccess(c, gin.H{
		"user_id":   fmt.Sprintf("%d", user.UserId), // id值大于1<<53-1  int64类型的最大值是1<<63-1
		"user_name": user.Username,
		"token":     token,
	})
}

其中参数校验就不必多说了,和注册时一模一样的,要注意的是注册接口的参数是不需要re_password的,所以我们新定义了一个model,用于登录接口所需的参数

models/params.go

// ParamLogin 登录请求参数
type ParamLogin struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

此外,需要注意的点是业务逻辑处理,以及返回响应,可以看到在响应中我们加入了token,这个后续会介绍。

三、logic增加业务逻辑处理

如果用户登录成功,我们会返回用户信息和token,因为需要将用户名返回给前端。同时登录成功后,我们会颁发一个token给前端,前端放于请求头中,后续请求时带上,就可以被鉴权通过,然后访问一些需要登录的接口啦。

logic/user.go

func Login(p *models.ParamLogin) (user *models.User, token string, err error) {
	user = &models.User{
		Username: p.Username,
		Password: p.Password,
	}
	// 传递的是指针,就能拿到user.UserID
	if err := mysql.FindUserByUserNameAndPassword(user); err != nil {
		return nil, "", err
	}
	// 生成JWT
	token, err = jwt.GenToken(user.UserId, user.Username)
	if err != nil {
		return
	}

	return user, token, nil
}

四、dao层增加通过用户名和密码验证用户是否能够登录

首先通过用户名查出用户,然后比对加密后的密码是否相同即可

dao/mysql/user.go

func FindUserByUserNameAndPassword(user *models.User) (err error) {
	u := &models.User{}
	err = db.Where("username= ?", user.Username).First(u).Error
	if err == gorm.ErrRecordNotFound {
		return ErrorUserNotExist
	}
	if err != nil {
		// 查询数据库失败
		return err
	}

	// 判断密码是否正确
	oPassword := user.Password // 用户登录的密码
	password := encryptPassword(oPassword)
	if password != u.Password {
		return ErrorInvalidPassword
	}

	user.UserId = u.UserId // 将用户ID返回回去

	return
}

五、鉴权工具包

我们使用jwt作为鉴权,可以写一个工具包,用于生成和校验tokenjwt的详细介绍可以参考本人其他博客:
44.jwt在go中的使用及原理
45.双token无感熟悉以及解决sso单点登录限制

pkg/jwt/jwt.go

package jwt

import (
	"errors"
	"time"

	"github.com/spf13/viper"

	"github.com/dgrijalva/jwt-go"
)

var mySecret = []byte("lym")

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

// GenToken 生成JWT
func GenToken(userID int64, username string) (string, error) {
	// 创建一个我们自己的声明的数据
	c := MyClaims{
		UserID:   userID,
		Username: username, // 自定义字段
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: time.Now().Add(
				time.Duration(viper.GetInt("auth.jwt_expire")) * time.Hour).Unix(), // 过期时间
			Issuer: "bluebell", // 签发人
		},
	}
	// 使用指定的签名方法创建签名对象
	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")
}

注:其中token的过期时间我们是用viper配置的,这里没有在配置结构体AppConfig中添加对应字段,而是自接使用viperGetInt方法获取,是故意表明这种读取配置的方式和结构体方式可以同时使用的。

name: "bluebell"
mode: "release"
port: 8084
version: "v0.0.1"
start_time: "2024-03-09"
machine_id: 1

auth:
  jwt_expire: 8760

六、编译运行测试

首先输入密码错误的情况

在这里插入图片描述

密码正确的情况,登录成功,返回了token并自动跳转到了登录成功后的界面

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值