在本节中,首先完成了登录接口,然后在项目中添加了基于jwt的身份鉴权实现。
之前我们完成的注册接口中,将用户密码明文保存在后端数据库中,这是对用户隐私的一种侵犯。接下来我们要通过bcrypt包,将用户密码加密后,再保存在数据库中。
bycrypt是go官方包之一,是一个加密包,其进行的加密是非可逆的,也就是说从解密结果无法对密码进行推测,非常符合对用户密码加密的逻辑。
具体使用请看下面的两个方法。
//加密方法
//此方法传入用户密码,和一个“cost”(加盐),返回一个加密后的密码和错误信息。
func GenerateFromPassword(password []byte, cost int) ([]byte, error)
//比较方法
//此方法传入用户密码和数据库中保存的密码哈希,进行比较后,如果错误,会返回error,如果密码正确,会返回Nil的error.
func CompareHashAndPassword(hashedPassword, password []byte) error
在实现登录接口之前,我们先修改注册接口的逻辑,将原本明文保存的密码,改为加密存储。
hashedPassword,err:=bcrypt.GenerateFromPassword([]byte(password),bcrypt.DefaultCost)
if err!=nil{
c.JSON(500,gin.H{
"error":"there are something wrong when encrypting password!",
})
}
db.Create(&model.User{Name: name,Password: string(hashedPassword),Telephone: telephone})
在router.go中加入登录路由:
router.GET("/login",controller.Login)
实现登录接口。
func Login(c *gin.Context) {
//获取参数
telephone:=c.Query("telephone")
password:=c.Query("password")
//查询是否存在
db:=common.GetDB()
var user model.User
db.Where("telephone=?",telephone).First(&user)
if len(telephone)!=11{
c.JSON(400,gin.H{
"error":"the telephone is not correct",
})
return
}
//不存在
if user.ID==0{
c.JSON(400,gin.H{
"error": "the telephone is not registered!",
})
return
}
//判断密码是否正确
//使用bcrypt包进行密码控制
err:=bcrypt.CompareHashAndPassword([]byte(user.Password),[]byte(password))
if err!=nil {
c.JSON(400,gin.H{
"error":"the password is wrong!",
})
return
}
//发放token
//这里的token直接设定的11,后面会设立真正的token
token:="11"
//返回登录成功
c.JSON(200,gin.H{
"msg":"login succeed",
"data":gin.H{
"token":token,
},
})
}
本部分参考博客 https://blog.wangjunfeng.com/post/golang-jwt/#3-%E7%AD%BE%E5%90%8D-signature
JWT (JSON Web Token)是一个开放标准(RFC 7519),指基于JSON的、用于在WEB上声明某种特定的令牌(token),以保证各方之间安全的传输信息。
JWT通过将用户信息加密到token中,服务端不需要保存任何用户信息。服务端只需要通过保存的密钥来验证token正确性,如果正确即通过验证。
JWS实际上就是一个字符串,由三部分组成,头部(Header)、载荷(Payload)、签名(Signature),并以.
进行拼接。其中头部和载荷都是以JSON格式存放数据,只是进行了编码。
每个JWT都会带有头部信息,这里主要声明使用的算法。声明算法的字段名为alg
,同时还有一个typ
的字段,默认JWT
即可。以下示例中算法为HS256。
{
"alg": "HS256",
"typ": "JWT"
}
因为JWT是字符串,所以我们还需要对以上内容进行Base64编码,编码后字符串如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
载荷即消息体,这里会存放实际的内容,也就是Token的数据声明(Claim)。这一段有一些是标准字段,当然也可以根据自己需要添加自己需要的字段。标准字段如下:
iss
: Token签发者。格式是区分大小写的字符串或者uri,用于唯一标识签发token的一方。sub
: Token的主体,即它的所有人。格式是区分大小写的字符串或者uri。aud
: 接收Token的一方。格式为区分大小写的字符串或uri,或者这两种的数组。exp
: Token的过期时间,格式为时间戳。nbf
: 指定Token在nbf时间之前不能使用,即token开始生效的时间,格式为时间戳。iat
: Token的签发时间,格式为时间戳。jti
: 指此Token的唯一标识符字符串。主要用于实现唯一性保证,防止重放。下面是一个示例:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
同样进行Base64编码后,字符串如下:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
签名是对头部和载荷内容进行签名,一旦前面两部分数据被篡改,只要服务器加密用的密钥没有泄露,得到的签名肯定和之前的签名不一致。
签名的过程:
.
对以上两个字符串进行拼接,得到字符串str3如果用伪代码表示就是(以HS256为例):
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
将三组字符串,以.
相连,就得到了一个完整的token,例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.OFHM3R8PSyHDT_vuzRF5fYkYWdhExM_9pE81kG05qAk
用户鉴权,在之前的传统的方法时,会在服务端存储一个session,并给客户端返回一个cookie。而如果是使用jwt来做身份鉴定的话,当用户登录成功,会给用户一个token,前端只需要在本地保存该token即可(通常使用localStorage,也可以使用cookie)。
当用户需要访问一个受保护的资源时,需要再Header中使用Bearer模式的Authorization头。其内容看起来是下面这样:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.OFHM3R8PSyHDT_vuzRF5fYkYWdhExM_9pE81kG05qAk
优点:
缺点:
go get github.com/dgrijalva/jwt-go
1.指定使用加密秘钥。
2.设定Claims结构体,里面保存着token里面携带的载荷信息,注意里面需要包含jwt.StandardClaims。
3.编写产生token函数,编写解析token函数。
代码如下:
package common
import (
"errors"
"gin_test/model"
"github.com/dgrijalva/jwt-go"
"time"
)
//设定加密秘钥
var jwtKey =[]byte("my key")
//定义声明结构体
type Claims struct {
jwt.StandardClaims
UserId uint
}
//生成token
func GenerateToken(user model.User)(string,error){
//过期时间设定为24小时
expiresationtime:=time.Now().Add(7*24*time.Hour)
claims:=Claims{
UserId: user.ID,
StandardClaims:jwt.StandardClaims{
ExpiresAt:expiresationtime.Unix(),
IssuedAt: time.Now().Unix(),
Issuer: "yzy",
Subject: "user token",
},
}
//创建新的声明
tokenClaims:=jwt.NewWithClaims(jwt.SigningMethodHS256,claims)
tokenstr,err:=tokenClaims.SignedString(jwtKey)
if err != nil {
return "",err
}
return tokenstr,nil
}
//解析token
func ParseToken(tokenstr string)(*Claims,error){
claims:=Claims{}
token,err:=jwt.ParseWithClaims(tokenstr,&claims,func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err!=nil {
return nil,err
}
//如果token无效
if !token.Valid{
return nil,errors.New("the token is invalid")
}
return &claims,nil
}
在上面的登录逻辑中,token是直接指定的,现在我们要使用上面编写的jwt工具包在登录文件中生成正确的token。
只需要修改发放token部分的代码即可:
//发放token
token,err:=common.GenerateToken(user)
if err != nil {
c.JSON(500,gin.H{
"error":"system wrong",
})
log.Printf("token generate error:%v",err)
}
//返回登录成功
c.JSON(200,gin.H{
"msg":"login succeed",
"data":gin.H{
"token":token,
},
})
gin中间件的编写只需要两步:
1.编写中间件函数(有格式规范)
2.在路由中使用中间件函数。
本次编写的鉴权中间件:
func AuthMiddleware()gin.HandlerFunc {
return func(c *gin.Context) {
//获取参数
tokenstr:=c.GetHeader("Authorization")
if len(tokenstr)==0||!strings.HasPrefix(tokenstr,"Bearer "){
c.JSON(400,gin.H{
"error":"Insufficient permissions",
})
c.Abort()//丢弃请求
return
}
tokenstr=tokenstr[7:]
//解析token
claims,err:=common.ParseToken(tokenstr)
if err != nil {
c.JSON(400,gin.H{
"error":"Insufficient permissions",
})
log.Println(err)
c.Abort()//丢弃请求
return
}
//对用户进行鉴权
userid:=claims.UserId
var user model.User
db:=common.GetDB()
db.First(&user,userid)
if user.ID==0{
c.JSON(400,gin.H{
"error":"Insufficient permissions",
})
c.Abort()//丢弃请求
return
}
//如果用户存在,将用户信息传入上下文
c.Set("user",user)
// before request
c.Next()
// after request
log.Println("完成一次鉴权的使用")
}
}
在路由中使用中间件:
router.GET("/info",middleware.AuthMiddleware(),controller.Info)
注意中间添加的是中间件函数,最后面的controller.info在下面实现
用户信息是敏感信息,因此调用此接口需要鉴权。
func Info(c *gin.Context) {
//从上下文中获取信息
user,_:=c.Get("user")
c.JSON(200,gin.H{
"data":user,
})
}
在之后会使用git进行版本的控制,然后每个章节都进行一次版本迭代更新,才会让整个系列更好懂。