2.登录业务

news2024/10/23 19:46:10

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

登录有两种方式:

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

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

JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。
JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
可以看出,JWT 更符合设计 RESTful API 时的「Stateless(无状态)」原则 。

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.coloradmin.cn/o/2221824.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络安全有关法律法规

1. 前言 在当今数字化高速发展的时代,网络安全已成为关乎国家、企业和个人的重要议题。为了应对日益复杂的网络安全挑战,一系列网络安全法律法规应运而生,它们如同坚实的盾牌,守护着我们的数字世界。现在是2024年10月&#xff0c…

军团服务QA角度总结

需求背景: 军团业务诞生的时候承接家族群组功能,玩法邀请成员做任务->积分升级->发送奖励。还是拉收入的一个业务载体。收入才是王道。 军团服务端核心就三个:军团创建,人员管理和军团奖励。 军团创建: 创建…

每天练打字8:今日状况——常用字后五百击键4.5第1遍进行中,赛文速度105.75

今日跟打:738字 总跟打:125701字 记录天数:2459天 (实际没有这么多天,这个是注册账号的天数) 平均每天:50字 本周目标完成进度: 练习常用单字后500,击键3.5,…

kernel32.dll的功能、作用,教大家几种修复kernel32.dll错误的办法

当这个文件出现问题时,用户可能会遇到各种错误消息,例如“缺失kernel32.dll”或“kernel32.dll发生错误”。这些错误不仅令人困扰,还可能威胁到您的数据安全和系统性能。接下来,本文将教大家几种修复kernel32.dll错误的有效方法&a…

群控系统服务端开发模式-业务流程图补充

进天有读者给我反馈,业务流程图看的不是很明确,所以我把未画完的业务流程图补充完毕。也希望以后更多的读者给我评论及意见。 一、业务流程梳理 1、非业务流程 a、添加部门、添加级别、添加执行方式。因为这些参数都是要被其他地方调用的,更…

word中的内容旋转90度

在vsto、Aspose.Words 中,默认没有直接的 API 可以让表格整体旋转 90 度。然而,我们可以通过一些方式来实现类似的效果,具体思路如下: 将表格插入到一个形状(Shape)或文本框中,然后旋转该形状。…

影刀RPA实战番外:excel函数应用指南

Excel函数是用于执行特定计算、分析和数据处理任务的预定义公式。它们可处理数学计算、文本处理、逻辑判断、日期和时间运算、查找和引用数据等。例如,SUM函数可以计算一系列数字的总和,IF函数进行逻辑测试,VLOOKUP函数在表格中查找数据&…

Oracle CONNECT BY、PRIOR和START WITH关键字详解

Oracle CONNECT BY、PRIOR和START WITH关键字详解 1. 基本概念2. 数据示例3. SQL示例3.1. 查询所有员工及其上级3.2. 显示层次结构3.3. 查询特定员工的子级 4. 结论 在Oracle数据库中,CONNECT BY、PRIOR和START WITH关键字主要用于处理层次结构数据,例如…

我悟了,华为FreeBuds 6i这样戴更稳了!

谁懂啊,才知道华为FreeBuds 6i标配多种尺寸的耳塞(如S、M、L),方便大家根据自己耳道大小自由选择。如果耳机戴上后感到耳朵不适或松动,可能是耳塞尺寸不匹配。 小提示: 耳塞过大 可能会造成压迫感&#xf…

kafka自定义配置信息踩坑

org.apache.kafka.common.config.ConfigException: Invalid value 0 for configuration acks: Expected value to be a string, but it was a java.lang.Integer 场景描述: 单个kafka使用springboot框架自带的 yml 配置完全OK(因为底层会帮我们处理好类…

CSS 设置网页的背景图片

背景 最近正好在写一个个人博客网站“小石潭记”,需要一张有水,有鱼的图片。正好玩原神遇到了类似场景,于是截图保存,添加到网站里面。以下是效果图: css 写个class,加到整个网页的body上 .bodyBg {ba…

接口测试 —— 如何测试加密接口?

接口加密是指在网络传输过程中,将数据进行加密,以保护数据的安全性。接口加密可以采用多种加密算法,如AES、DES、RSA等。测试接口加密的目的是验证接口加密算法的正确性和安全性。以下是一些详细的测试方法和注意事项: 接口加密字…

A-【项目开发知识管理】Android AIDL跨进程通信

Android AIDL跨进程通信 文章目录 Android AIDL跨进程通信0.我为啥要写这篇文章1.AIDL是干啥的?1.1简述1.2官方话 2.在AndroidStudio中怎么干?2.1准备工作2.2在项目A中创建AIDL文件夹2.3在项目A中创建一个aidl文件2.4将项目A进行一次Rebuild操作2.5在项目…

计算机专业大学四年的学习路线(非常详细),零基础入门到精通,看这一篇就够了

前言 许多学子选择踏上计算机这条充满挑战与机遇的道路。但在大学四年中,如何规划自己的学习路线,才能在毕业时脱颖而出,成为行业的佼佼者呢? 第一学年:基础知识的奠基 1.1 课程安排 在大学的第一年,重…

超详解C++类与对象(下)

目录 1. 初始化列表 1.1. 定义 2.2. 注意 2. 隐式类型转换 2.1. 内置类型 2.2. 自定义类型 2.3. explicit关键字 3.类的静态成员 2.1. 定义 2.2. 注意 4.const成员函数 5. 友元 5. 1友元函数 5.2. 友元类 6. 内部类 6.1. 定义 6.2. 注意 7. 匿名对象 7…

手撕布隆过滤器:原理解析与面试心得

前言 说来话长,话来说长。前些天我投了一些日常实习的简历,结果足足等了两个礼拜才收到面试通知,看来如今的行情确实是挺紧张的。当时我是满怀信心去的,心想这次一定要好好拷打面试官一番,结果没想到,自我…

一、python基础

python基础 认识Python1. Python介绍1.1 为什么学习Python1.2 Python发展历史 2. 语言分类简介2.1 编译型2.2 解释型 Python环境搭建1. Python 解释器1.1 Python解释器下载1.2 Python解释器安装 2. 解释器运行Python脚本2.1 演练步骤 PyCharm1. PyCharm介绍2. PyCharm安装3. Py…

15分钟学Go 第6天:变量与常量

第6天:变量与常量 在Go语言中,变量和常量是编程的基础概念。理解如何定义和使用它们不仅能帮助我们管理数据,还能增强代码的可读性和可维护性。在本章中,我们将详细探讨Go语言中的变量和常量,涵盖它们的定义、使用、作…

机器学习建模分析

机器学习 5.1 机器学习概述5.1.1 机器学习与人工智能5.1.2 python机器学习方法库 5.2 回归分析5.2.1 回归分析原理5.2.2 回归分析实现 5.3 分类分析5.3.1 分类学习原理5.3.2 决策树5.5.3 支持向量机 5.4 聚类分析5.4.1 聚类任务5.4.2 K-means算法 5.5 神经网络和深度学习5.5.1神…

python配合yolov11开发分类训练软件

上一篇文件写了用yolo分类模型开发分类软件,这边文章在上个分类软件的基础上加入训练功能环境配置:pycharm,PySide6 6.6.1 ,PySide6-Addons 6.6.1,PySide6-Essentials 6.6.1,torch 2.3.1cu121,torchaudio 2…