当前位置: 首页 > article >正文

2.登录业务

项目地址:https://github.com/liwook/PublicReview.git

登录有两种方式:

  • 通过手机号码发送验证码登录。
  • 另一种是通过密码进行登录。

通过验证码登录的话,服务端就要存储该手机号码的验证码,这就是典型的键值对(一个号码对应一个验证码),还有要给验证码设置过期时间,所以可以存储在Redis中。

Go语言连接使用Redis

在config.yaml添加Redis的内容

Redis:
  Host: 43.139.27.107:6379
  Password: wook1847
  PoolSize: 20
#.yaml文件添加的时候要留意,可能添加的格式不对导致程序访问不到配置的
#通过颜色来区分是否有错误。Host: 这个后面是需要空一格,颜色才正确,格式才对

在config.go文件添加Redis配置的结构体。

var (
	RedisOption  *RedisSetting
)

type RedisSetting struct {
	Host     string
	Password string
	PoolSize int
}

//InitConfig函数添加读取redis的配置
func InitConfig(path string) {
    ....................
	err = ReadSection("redis", &RedisOption)
	if err != nil {
		panic(err)
	}
}

在db目录创建redis.go文件。使用一个常用的go Redis客户端 go-redis来连接Redis。

//redis.go
var RedisDb *redis.Client

func NewRedisClient(config *config.RedisSetting) (*redis.Client, error) {
	client := redis.NewClient(&redis.Options{
		Addr:     config.Host,     //自己的redis实例的ip和port
		Password: config.Password, //密码,有设置的话,就需要填写
		PoolSize: config.PoolSize, //最大的可连接数量
	})
	val, err := client.Ping(context.Background()).Result() //测试ping
	if err != nil {
		return nil, err
	}
	fmt.Println("redis测试: ", val)
	return client, err
}

在main.go中进行创建redis客户端。

func init() {
    ............................
	//初始化redis
	db.RedisClient, err = db.NewRedisClient(config.RedisOption)
	if err != nil {
		panic(err)
	}
}

添加关于登录的函数

在internal目录创建user目录,添加login.go文件。

1.获取验证码的函数

步骤:

  1. 判断手机号是否合法
  2. 生成验证码,并使用redis的string类型保存在redis中,需设置过期时间
  3. 把验证码发送给客户
const (
	UserNickNamePrefix = "user"
	phoneKey           = "phone:"
	loginMethod        = "loginMethod"
)

// 得到验证码
// get /user/verificationcode/:phone
func GetVerificationCode(c *gin.Context) {
	phone := c.Param("phone")
	if phone == "" || !isPhoneInvalid(phone) {
		code.WriteResponse(c, code.ErrValidation, "phone is empty or invalid")
		return
	}

	//生成验证码,6位数
	num := rand.Intn(1000000) + 100000
	//用redis的string类型保存
	key := phoneKey + phone
	success, err := db.RedisClient.SetNX(context.Background(), key, num, 4*time.Minute).Result()
	if !success || err != nil {
		code.WriteResponse(c, code.ErrDatabase, nil)
		return
	}

	code.WriteResponse(c, code.ErrSuccess, gin.H{"VerificationCode": num})
}

func isPhoneInvalid(phone string) bool {
	// 匹配规则: ^1第一位为一, [345789]{1} 后接一位345789 的数字
	// \\d \d的转义 表示数字 {9} 接9位 ,   $ 结束符
	regRuler := "^1[123456789]{1}\\d{9}$"
	reg := regexp.MustCompile(regRuler) // 正则调用规则
	// 返回 MatchString 是否匹配
	return reg.MatchString(phone)
}

2.登录

现在的登录/注册,基本都是通过手机号码进行的。而登录的时候选择密码登录,也是通过手机号码和密码一同登录的。

登录的数据是json格式,存储在请求体中。

const (
	UserNickNamePrefix = "user"
	phoneKey           = "phone:"
)

type LoginRequest struct {
	Phone       string `json:"name" binding:"required"`
	CodeOrPwd   string `json:"codeOrPwd" binding:"required"`
	LoginMethod string `json:"loginMethod" binding:"required"`
}

// post /user/login
func Login(c *gin.Context) {
	var login LoginRequest
	err := c.BindJSON(&login)
	if err != nil {
		slog.Error("codelogin bind bad", "err", err)
		code.WriteResponse(c, code.ErrBind, nil)
		return
	}
	if !isPhoneInvalid(login.Phone) {
		code.WriteResponse(c, code.ErrValidation, "phone is invalid")
		return
	}

	switch login.LoginMethod {
	case "code":
		loginCode(c, login, token)
	case "password":
		loginPassword(c, login, token)
	default:
		code.WriteResponse(c, code.ErrValidation, "loginMethod bad")
	}
}

 验证码登录

  1. 从redis中得到phone保存的验证码进行对比
  2. 之后从MySQL中判断该用户是否是新用户,若是新用户,就需要创建用户,存储到数据库中
  3. 发送给客户端登录成功。
func loginCode(c *gin.Context, login LoginRequest) {
	//为空是返回error中的,值为redis.Nil
	//对比号码是否有验证码
	val, err := db.RedisClient.Get(context.Background(), phoneKey+login.Phone).Result()
	if err == redis.Nil {
		code.WriteResponse(c, code.ErrExpired, "验证码过期或没有该验证码")
		return
	}
	if err != nil {
		slog.Error("redis get bad", "err", err)
		code.WriteResponse(c, code.ErrDatabase, nil)
		return
	}
	if val != login.CodeOrPwd {
		code.WriteResponse(c, code.ErrExpired, "验证码错误")
		return
	}

	//之后判断是否是新用户,若是新用户,就创建
	u := query.TbUser
	count, err := u.Where(u.Phone.Eq(login.Phone)).Count()
	if err != nil {
		slog.Error("find by phone bad", "err", err)
		code.WriteResponse(c, code.ErrDatabase, nil)
		return
	}
	if count == 0 {
		err := u.Create(&model.TbUser{Phone: login.Phone, NickName: UserNickNamePrefix + strconv.Itoa(rand.Intn(100000))})
		if err != nil {
			slog.Error("create user failed", "err", err)
			code.WriteResponse(c, code.ErrDatabase, "create user failed")
			return
		}
	}

	code.WriteResponse(c, code.ErrSuccess, "login success")
}

账号密码登录

在数据库中判断发送过来的phone和password是否正确,若正确,回复登录成功;否则回复登录失败

func loginPassword(c *gin.Context, login LoginRequest) {
	if login.Password == "" {
		code.WriteResponse(c, code.ErrValidation, "password is empty")
		return
	}
	//从mysql中判断账号和密码是否正确
	u := query.TbUser
	count, err := u.Where(u.Phone.Eq(login.Phone), u.Password.Eq(login.CodeOrPwd)).Count()
	if err != nil {
		slog.Error("find by phone and password bad", "err", err)
		code.WriteResponse(c, code.ErrDatabase, nil)
		return
	}
	if count == 0 {
		code.WriteResponse(c, code.ErrPasswordIncorrect, "phone or password is Incorrect")
		return
	}
	code.WriteResponse(c, code.ErrSuccess, "login success")
}

对接口进行访问控制,保存登录状态

大家在使用软件的时候,一般是登录一次,以后多次使用或者在一段时间内是不用再次登录的。这个是怎么做到的呢?在网页登录后,每次请求都会带有可以证明该客户端身份的token。服务端会进行判断,从而每次请求正常。

还有,在完成了相关的业务接口的开发后,我们正打算放到服务器上给其他同事查看时,你又想到了一个问题,这些 API 接口,没有鉴权功能,那就是所有知道地址的人都可以请求该项目的 API 接口,甚至有可能会被网络上的端口扫描器扫描到后滥用,这非常的不安全,怎么办呢。实际上,我们应该要考虑做纵深防御,对 API 接口进行访问控制。

这里就可以用到JWT。

JWT

JSON Web 令牌(JWT)是一个开放标准(RFC7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。 由于此信息是经过数字签名的,因此可以被验证和信任。 可以使用使用 RSA 或 ECDSA 的公用/专用密钥对对 JWT 进行签名。

jwt的结构体

假设jwt原始的payload如下,username,exp为过期时间,nbf为生效时间,iat为签发时间。第一个是业务非敏感参数,后三者是jwt标准的参数。

{
  "username": "zhangsan",
  "exp": 1681869394,
  "nbf": 1681782994,
  "iat": 1681782994
}

创建internal/middleware文件夹,在该文件夹添加jwt.go。添加如下结构体

type UserClaims struct {
	Phone                string
	jwt.RegisteredClaims // v5版本新加的方法
}

在config.yaml添加关于jwt的配置

JWT:
  Secret: hello
  Issuer: dianping-service
  Expire: 7200  #秒

添加关于jwt的配置结构体和变量

// config.go
var (
    ..........
	JwtOption    *JWTSetting
)

type JWTSetting struct {
	Secret string
	Issuer string
	Expire time.Duration
}

func InitConfig(path string) {
    ..................
	err = ReadSection("jwt", &JwtOption)
	if err != nil {
		panic(err)
	}
}

生成并解析jwt

入参就是上面结构体UserClaims中的Phone。

  • 避免在 JWT 的 payload 中存储敏感的用户信息。因为 JWT 通常是可解码的,虽然签名可以保证其完整性,但不能保证其保密性。如果需要存储一些用户相关的信息,可以使用加密的方式存储在服务器端,并在 JWT 中存储一个引用或标识符。
  • 所以要对号码进行加密,或者使用其他不敏感的信息。
func GetJWTSecret() []byte {
	return []byte(config.JwtOption.Secret)
}

func GenerateToken(phone string) (string, error) {
	//sha1加密phone
	hash := sha1.New()
	hash.Write([]byte(phone))
	claims := UserClaims{
		Phone: string(hash.Sum(nil)),
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(config.JwtOption.Expire)),
			Issuer:    config.JwtOption.Issuer,
			NotBefore: jwt.NewNumericDate(time.Now()), //生效时间
		},
	}

	//使用指定的加密方式(hs256)和声明类型创建新令牌
	tokenStruct := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	//获得完整的签名的令牌
	return tokenStruct.SignedString(GetJWTSecret())
}

func ParseToken(token string) (*UserClaims, error) {
	tokenClaims, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (any, error) {
		return GetJWTSecret(), nil
	})
	if err != nil {
		return nil, err
	}

	if tokenClaims != nil {
		if claims, ok := tokenClaims.Claims.(*UserClaims); ok && tokenClaims.Valid {
			return claims, nil
		}
	}
	return nil, err
}

使用形式

以中间件形式使用。要注意的一点是登录和获取验证码是不用JWT验证的。

func JWT() gin.HandlerFunc {
	return func(c *gin.Context) {
		//登录和获取验证码是不用JWT验证的
		if c.Request.RequestURI == "/user/login" || c.Request.RequestURI == "/user/getcode" {
			return
		}

		ecode := code.ErrSuccess
		token := c.GetHeader("token")

		if token == "" {
			ecode = code.ErrInvalidAuthHeader
		} else {
			_, err := ParseToken(token)
			if err != nil {
				ecode = code.ErrTokenInvalid
			}
		}
		if ecode != code.ErrSuccess {
			code.WriteResponse(c, ecode, nil)
			c.Abort()
			return
		}
		c.Next()
	}
}

使用jwt

那就需要修改登录回复的流程,登录成功,服务端就返回该token,后续该客户使用的时候都要带上该token。

func loginCode(c *gin.Context, login LoginRequest) {
    ..................
	if count == 0 {
		err := u.Create(&model.TbUser{Phone: login.Phone, NickName: UserNickNamePrefix + strconv.Itoa(rand.Intn(100000))})
		if err != nil {
			slog.Error("create user failed", "err", err)
			code.WriteResponse(c, code.ErrDatabase, "create user failed")
			return
		}
	}

	generateTokenResponse(c, login.Phone)
}

func loginPassword(c *gin.Context, login LoginRequest) {
	//从mysql中判断账号和密码是否正确
    ...................
	if count == 0 {
		code.WriteResponse(c, code.ErrPasswordIncorrect, "phone or password is Incorrect")
		return
	}

	generateTokenResponse(c, login.Phone)
}

func generateTokenResponse(c *gin.Context, phone string) {
	token, err := middleware.GenerateToken(phone)
	if err != nil {
		slog.Error("generate token bad", "err", err)
		code.WriteResponse(c, code.ErrTokenGenerationFailed, nil)
		return
	}
	code.WriteResponse(c, code.ErrSuccess, gin.H{"token": token})
}

在router.go中使用JWT中间件。

func NewRouter() *gin.Engine {
	gin.SetMode(gin.ReleaseMode)
	r := gin.Default()
	r.Use(middleware.JWT()) //使用jwt中间件

	r.GET("/ping", func(c *gin.Context) {
		code.WriteResponse(c, code.ErrSuccess, "pong")
	})

	r.GET("/user/verificationcode/:phone", user.GetVerificationCode)
	r.POST("/user/login", user.Login)
	return r
}

登录成功后,用户每次发送请求都需要在header中添加token,值是服务器端返回的token。


http://www.kler.cn/news/355977.html

相关文章:

  • 安科瑞/ACREL能源管理软件能耗管理软件
  • 解决低版本pytorch和onnx组合时torch.atan2()不被onnx支持的问题
  • 适配器模式演示(C++)三分钟读懂
  • 前端工程启动工具
  • ozon测评安全攻略:自养号技巧
  • iptables规则
  • Git的原理和使用(二)
  • Python 代码使用 OpenCV 库实现了从摄像头获取视频流,并在视频中检测特定颜色区域的边缘线条
  • kaggle中如何更新上传自定义的数据集dataset?
  • 短视频矩阵源码搭建解析,支持OEM~
  • 编程实战:利用API接口轻松获取数据
  • 尚硅谷rabbitmq2024 第15-18节 springboot整合与可靠性答疑
  • 影响安装光伏电站的因素
  • 必看干货|等保测评(网络安全等级保护)五问五答
  • 在Openshift上安装MetalLB
  • 告别ELK,APO提供基于ClickHouse开箱即用的高效日志方案——APO 0.6.0发布
  • git submodule add用法
  • 鸿蒙网络编程系列21-使用HttpRequest上传任意文件到服务端示例
  • leetcode hot100 之【LeetCode 15. 三数之和】 java实现
  • Ubuntu如何显示pcl版本