本项目代码详细参考地址 https://github.com/jiangbo66666/gin-vue-microBlog
为了实现我们的登录功能,我们需要存储用户的信息,考虑到用户包含信息过多的问题,我决定常见两张表,一张账号信息表,一张用户信息表。表结构如下
。
//性别类型
type Gender string
const (
Male Gender = "男"
Female Gender = "女"
other Gender = "女"
)
// 用户信息表
type UserInfo struct {
ID uint `gorm:"primaryKey;autoIncrement;comment:'用户id';uniqueIndex"` //id主键,自增
Name string `gorm:"comment:'用户姓名'"`
Sex Gender `gorm:"type:enum('男', '女', '其他');comment:'用户性别'"` //创建枚举类型
BirthDay time.Time `gorm:"comment:'用户生日'"`
PhoneNumber string `gorm:"comment:'用户手机号码'"`
Email string `gorm:"comment:'用户邮箱'"`
Address string `gorm:"comment:'用户地址'"`
CreateBy int `gorm:"comment:'用户由谁创建'"`
CreateAt time.Time `gorm:"default:(NOW());comment:'创建账号时间'"` //创建默认时间
UpdateAt time.Time `gorm:"comment:'更新时间'"`
RecentLogin time.Time `gorm:"comment:'最近登陆时间'"`
HeaderImage string `gorm:"comment:'头像地址'"`
Profile string `gorm:"comment:'个人简介'"`
}
type AccountInfo struct {
ID uint `gorm:"primaryKey;autoIncrement;comment:'账号id'"` //id主键,自增
UserId uint `gorm:"comment:'用户id'"`
PhoneNumber string `gorm:"comment:'用户手机号码';uniqueIndex;type:varchar(20)"` //账号密码表带入手机号码,方便登录流程,减少登录查询
AccountName string `gorm:"comment:'账号名';uniqueIndex;type:varchar(20)"`
Password string `gorm:"comment:'账号密码'"`
CreateAt time.Time `gorm:"comment:'创建时间'"`
UpdateAt time.Time `gorm:"comment:更新时间"`
RecentLogin time.Time `gorm:"comment:最近登录时间"`
Status int `gorm:"default:0;comment:'用户账号状态'"`
User UserInfo `gorm:"foreignKey:UserId"` //外键关联userInfo表
}
详细功能实现见 https://github.com/jiangbo66666/gin-vue-microBlog
并且使用gorm的DB.AutoMigrate(AccountInfo{})
对上述账号信息表执行操作,利用AccountInfo 的userid
字段作为外键关联用户信息表,创建两张空表
注:
首先我考虑到,登录的情况下可能会使用账号名称,手机号码做查询操作,于是对账号名,手机号码增加了唯一索引,优化查询的速度。
至此我们的表结构就建立完毕。
在账号信息表中我们需要存储用户的账号密码。但是密码不能明文存储,有被盗取的风险。于是考虑使用bcrypt
随机盐值加密账号的密码并存储起来。
首先我们在终端执行下述方法加载bcrypt包
go get -u golang.org/x/crypto/bcrypt
并在项目中引入,并且创建如下两个方法:实现对明文密码的加密,与校验
import "golang.org/x/crypto/bcrypt"
// 密码加密
func PasswordHash(pwd string) (string, error) {
// GenerateFromPassword 方法对密码进行加密操作
bytes, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(bytes), err
}
// 密码验证
func PasswordVerify(pwd, hash string) bool {
// CompareHashAndPassword 方法将加密的数据与原始数据进行对比
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pwd))
return err == nil
}
详细功能实现见 https://github.com/jiangbo66666/gin-vue-microBlog
注:
上述的PasswordHash
方法接受一个明文密码的[]byte类型传入,并且返回一个字符串类型的加密过后的密码。
PasswordVerify
方法接受一个明文密码字符串和一个加密后的密码字符串,函数中会将将两哥密码字符串执行对比,返回一个布尔类型校验的结果
至此我们的账号密码的加密解密功能就此实现。
jwt全名json web token,是一种身份令牌,他本身由三个部分组成
header
:令牌头部,记录了整个令牌的类型和签名算法
payload
:令牌载荷,记录了保存的主体信息,在本项目中可以保存账号信息。
signature
:令牌签名,按照头部的签名算法对整个令牌进行签名,签名可以保证令牌不被伪造和篡改
如何在代码中使用?
首先我们需要下载jwt包,该包是最新的包,用法可能与过往有所不同,此项目代码参考自官方文档
go get -u github.com/golang-jwt/jwt/v5
代码实现
type MyClaims struct {
AccountName string `json:"accountName"`
jwt.RegisteredClaims
}
// 自定义秘钥
var mySecret = []byte("jiangbo")
// 生成token
func GenerateToken(Name string) (string, error) {
claims := MyClaims{
Name,
jwt.RegisteredClaims{
// 过期时间
ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)),
//签名
Issuer: "microBlog",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 自定义秘钥
tokenStr, err := token.SignedString(mySecret)
if err == nil {
return tokenStr, err
}
return "", errors.New("出错了")
}
func VarifyToken(tokenStr string) (string, error) {
token, err := jwt.ParseWithClaims(tokenStr, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
// 校验秘钥
return mySecret, nil
})
// 校验失败的时候即返回失败
if err != nil {
return "", err
}
// 解析账号名称
claims, ok := token.Claims.(*MyClaims)
if ok {
return claims.AccountName, nil
} else {
return "", errors.New("解析账号名称失败")
}
}
详细功能实现见 https://github.com/jiangbo66666/gin-vue-microBlog
GenerateToken
接收一个字符串形式的name,即账号密码作为自定义载荷,用于执行查询时的账号信息对比
VarifyToken
接收一个字符串形式的token,并返回一个字符串形式的账号名和一个err,err用于校验验证的失败与否
业务逻辑中,我们在登陆请求以后返回当前的账号的令牌,代码如下
//router包
r.POST("/login", api.LoginByName)
//api user包
// 账号名登录路由函数
func LoginByName(c *gin.Context) {
var loginInfo account_service.LoginInfo
bindJson(c, &loginInfo)
//账号名登录校验,并且返回一个token
token, err := loginInfo.LoginByNameAndToken()
if err == nil {
c.JSON(200, gin.H{
"code": 200,
"data": gin.H{"token": token},
"msg": "登陆成功",
})
} else {
c.JSON(200, gin.H{
"code": 500,
"msg": "登陆失败",
})
}
}
详细功能实现见 https://github.com/jiangbo66666/gin-vue-microBlog
我们可以在路由中间件中对需要校验token的场景执行token校验,并且解析账号名,用于该场景下的信息查询
路由处理代码如下
// 路由分组,读取userdetail handler
user := r.Group("/api/user")
{
user.Use(func(ctx *gin.Context) {
token := ctx.GetHeader("Token")
//校验账号信息
AccountName, err := util.VarifyToken(token)
if err != nil {
ctx.JSON(200, gin.H{
"code": 501,
"msg": "登录失效",
})
ctx.Abort()
return
} else {
// token校验通过,将token中的账号信息存储起来
ctx.Set("AccountName", AccountName)
ctx.Next()
}
})
user.POST("/info", api.UserDetail)
}
详细功能实现见 https://github.com/jiangbo66666/gin-vue-microBlog