gin框架学习(三)

0.前言

在本节中,首先完成了登录接口,然后在项目中添加了基于jwt的身份鉴权实现。

1.登录接口

1.1bcrypt包的介绍

之前我们完成的注册接口中,将用户密码明文保存在后端数据库中,这是对用户隐私的一种侵犯。接下来我们要通过bcrypt包,将用户密码加密后,再保存在数据库中。

bycrypt是go官方包之一,是一个加密包,其进行的加密是非可逆的,也就是说从解密结果无法对密码进行推测,非常符合对用户密码加密的逻辑。

具体使用请看下面的两个方法。

//加密方法
//此方法传入用户密码,和一个“cost”(加盐),返回一个加密后的密码和错误信息。
func GenerateFromPassword(password []byte, cost int) ([]byte, error) 

//比较方法
//此方法传入用户密码和数据库中保存的密码哈希,进行比较后,如果错误,会返回error,如果密码正确,会返回Nil的error.
func CompareHashAndPassword(hashedPassword, password []byte) error 

1.2 代码实现

在实现登录接口之前,我们先修改注册接口的逻辑,将原本明文保存的密码,改为加密存储。

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,
      },
   })
}

2.JWT简介

本部分参考博客 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正确性,如果正确即通过验证。

2.1 JWT组成解析

JWS实际上就是一个字符串,由三部分组成,头部(Header)、载荷(Payload)、签名(Signature),并以.进行拼接。其中头部和载荷都是以JSON格式存放数据,只是进行了编码。

2.1.1. 头部(Header)

每个JWT都会带有头部信息,这里主要声明使用的算法。声明算法的字段名为alg,同时还有一个typ的字段,默认JWT即可。以下示例中算法为HS256。

{
  "alg": "HS256",
  "typ": "JWT"
}

因为JWT是字符串,所以我们还需要对以上内容进行Base64编码,编码后字符串如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2.1.2. 载荷(Payload)

载荷即消息体,这里会存放实际的内容,也就是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

2.1.3. 签名(Signature)

签名是对头部和载荷内容进行签名,一旦前面两部分数据被篡改,只要服务器加密用的密钥没有泄露,得到的签名肯定和之前的签名不一致。

签名的过程:

  1. 对header的json数据进行Base64URL编码,得到一个字符串str1
  2. 对payload的json数据进行Base64URL编码,得到一个字符串str2
  3. 使用.对以上两个字符串进行拼接,得到字符串str3
  4. 使用header中声明的算法,以及服务端的密钥,对拼接字符串进行加密,生成签名

如果用伪代码表示就是(以HS256为例):

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)

将三组字符串,以.相连,就得到了一个完整的token,例如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.OFHM3R8PSyHDT_vuzRF5fYkYWdhExM_9pE81kG05qAk

2.2 JWT工作流程

用户鉴权,在之前的传统的方法时,会在服务端存储一个session,并给客户端返回一个cookie。而如果是使用jwt来做身份鉴定的话,当用户登录成功,会给用户一个token,前端只需要在本地保存该token即可(通常使用localStorage,也可以使用cookie)。

当用户需要访问一个受保护的资源时,需要再Header中使用Bearer模式的Authorization头。其内容看起来是下面这样:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.OFHM3R8PSyHDT_vuzRF5fYkYWdhExM_9pE81kG05qAk

2.3 jwt的优缺点

优点:

  • json具有通用性,所以可以跨语言。
  • 组成简单,字节占用小,便于传输
  • 服务端无需保存会话信息,很容易进行水平扩展
  • 一处生成,多处使用,可以在分布式系统中,解决单点登录问题
  • 可防护CSRF攻击

缺点:

  • payload部分仅仅是进行简单编码,所以只能用于存储逻辑必需的非敏感信息
  • 需要保护好加密密钥,一旦泄露后果不堪设想
  • 为避免token被劫持,最好使用https协议
  • 针对已经办法的令牌,无法作废,不容易应对数据过期的问题。

3.项目中集成jwt实现返回用户信息接口

3.1 编写jwt工具包

3.1.1在项目目录下,输入如下命令,导入jwt-go包。

go get github.com/dgrijalva/jwt-go

3.1.2在common文件夹下创建jwt.go文件,用于编写jwt工具包。

3.1.3根据以上的介绍,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

}

3.3补全上面的登录逻辑

在上面的登录逻辑中,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,
   },
})

3.4 编写中间件

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在下面实现

3.5 编写返回用户信息的逻辑

用户信息是敏感信息,因此调用此接口需要鉴权。

func Info(c *gin.Context) {
   //从上下文中获取信息
   user,_:=c.Get("user")
   c.JSON(200,gin.H{
      "data":user,
   })
}

5.总结

在之后会使用git进行版本的控制,然后每个章节都进行一次版本迭代更新,才会让整个系列更好懂。

你可能感兴趣的:(GO语言,gin,学习)