有了基本脚手架之后,我们在上节很容易的就完成了注册功能。登录功能与之类似,按部就班实现即可。
登录后,期望在一段时间内不需要继续登录也能访问其他一些需要登录的接口,这时候就涉及到鉴权啦,我们可以使用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
作为鉴权,可以写一个工具包,用于生成和校验token
。jwt
的详细介绍可以参考本人其他博客:
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
中添加对应字段,而是自接使用viper
的GetInt
方法获取,是故意表明这种读取配置的方式和结构体方式可以同时使用的。
name: "bluebell"
mode: "release"
port: 8084
version: "v0.0.1"
start_time: "2024-03-09"
machine_id: 1
auth:
jwt_expire: 8760
六、编译运行测试
首先输入密码错误的情况
密码正确的情况,登录成功,返回了token
并自动跳转到了登录成功后的界面