背景: 在如今前后端分离开发的大环境中,我们需要解决一些登陆,后期身份认证以及鉴权相关的事情,通常的方案就是采用请求头携带token的方式进行实现。本篇文章主要分享下在Golang语言下使用jwt-go来实现后端的token认证逻辑。

JSON Web Token(JWT)是一个常用语HTTP的客户端和服务端间进行身份认证和鉴权的标准规范,使用JWT可以允许我们在用户和服务器之间传递安全可靠的信息。

在开始学习JWT之前,我们可以先了解下早期的几种方案。

token、cookie、session的区别

Cookie

Cookie总是保存在客户端中,按在客户端中的存储位置,可分为内存Cookie硬盘Cookie

内存Cookie由浏览器维护,保存在内存中,浏览器关闭后就消失了,其存在时间是短暂的。硬盘Cookie保存在硬盘里,有一个过期时间,除非用户手工清理或到了过期时间,硬盘Cookie不会被删除,其存在时间是长期的。所以,按存在时间,可分为非持久Cookie和持久Cookie

cookie 是一个非常具体的东西,指的就是浏览器里面能永久存储的一种数据,仅仅是浏览器实现的一种数据存储功能。

cookie由服务器生成,发送给浏览器,浏览器把cookie以key-value形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该cookie发送给服务器。由于cookie是存在客户端上的,所以浏览器加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间,所以每个域的cookie数量是有限的。

Session

Session字面意思是会话,主要用来标识自己的身份。比如在无状态的api服务在多次请求数据库时,如何知道是同一个用户,这个就可以通过session的机制,服务器要知道当前发请求给自己的是谁

为了区分客户端请求,服务端会给具体的客户端生成身份标识session,然后客户端每次向服务器发请求的时候,都带上这个“身份标识”,服务器就知道这个请求来自于谁了。

至于客户端如何保存该标识,可以有很多方式,对于浏览器而言,一般都是使用cookie的方式

服务器使用session把用户信息临时保存了服务器上,用户离开网站就会销毁,这种凭证存储方式相对于cookie来说更加安全,但是session会有一个缺陷: 如果web服务器做了负载均衡,那么下一个操作请求到了另一台服务器的时候session会丢失。

因此,通常企业里会使用redis,memcached缓存中间件来实现session的共享,此时web服务器就是一个完全无状态的存在,所有的用户凭证可以通过共享session的方式存取,当前session的过期和销毁机制需要用户做控制。

Token

token的意思是“令牌”,是用户身份的验证方式,最简单的token组成: uid(用户唯一标识)+time(当前时间戳)+sign(签名,由token的前几位+盐以哈希算法压缩成一定长度的十六进制字符串),同时还可以将不变的参数也放进token

这里我们主要想讲的就是Json Web Token,也就是本篇的主题:JWT

Json-Web-Token(JWT)介绍

一般而言,用户注册登陆后会生成一个jwt token返回给浏览器,浏览器向服务端请求数据时携带token,服务器端使用signature中定义的方式进行解码,进而对token进行解析和验证。

JWT Token组成部分

  • header: 用来指定使用的算法(HMAC SHA256 RSA)和token类型(如JWT)
  • payload: 包含声明(要求),声明通常是用户信息或其他数据的声明,比如用户id,名称,邮箱等. 声明可分为三种: registered,public,private
  • signature: 用来保证JWT的真实性,可以使用不同的算法

header

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

对上面的json进行base64编码即可得到JWT的第一个部分

payload

  • registered claims: 预定义的声明,通常会放置一些预定义字段,比如过期时间,主题等(iss:issuer,exp:expiration time,sub:subject,aud:audience)
  • public claims: 可以设置公开定义的字段
  • private claims: 用于统一使用他们的各方之间的共享信息
{"sub": "xxx-api","name": "bgbiao.top","admin": true
}

对payload部分的json进行base64编码后即可得到JWT的第二个部分

注意: 不要在header和payload中放置敏感信息,除非信息本身已经做过脱敏处理

signature

为了得到签名部分,必须有编码过的header和payload,以及一个秘钥,签名算法使用header中指定的那个,然后对其进行签名即可

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

签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。

jwt.io网站中,提供了一些JWT token的编码,验证以及生成jwt的工具。

下图就是一个典型的jwt-token的组成部分。

什么时候用JWT

  • Authorization(授权): 典型场景,用户请求的token中包含了该令牌允许的路由,服务和资源。单点登录其实就是现在广泛使用JWT的一个特性
  • Information Exchange(信息交换): 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式.因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改

JWT(Json Web Tokens)是如何工作的

所以,基本上整个过程分为两个阶段,第一个阶段,客户端向服务端获取token,第二阶段,客户端带着该token去请求相关的资源.

通常比较重要的是,服务端如何根据指定的规则进行token的生成。

在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。

此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。

一般而言,你保存令牌的时候不应该超过你所需要它的时间。

无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema: Authorization: Bearer <token>

服务器上的受保护的路由将会检查Authorization header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。如果JWT包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。

如果token是在授权头(Authorization header)中发送的,那么跨源资源共享(CORS)将不会成为问题,因为它不使用cookie.

  • 客户端向授权接口请求授权
  • 服务端授权后返回一个access token给客户端
  • 客户端使用access token访问受保护的资源

基于Token的身份认证和基于服务器的身份认证

1.基于服务器的认证

前面说到过session,cookie以及token的区别,在之前传统的做法就是基于存储在服务器上的session来做用户的身份认证,但是通常会有如下问题:

  • Sessions: 认证通过后需要将用户的session数据保存在内存中,随着认证用户的增加,内存开销会大
  • 扩展性: 由于session存储在内存中,扩展性会受限,虽然后期可以使用redis,memcached来缓存数据
  • CORS: 当多个终端访问同一份数据时,可能会遇到禁止请求的问题
  • CSRF: 用户容易受到CSRF攻击

2.Session和JWT Token的异同

都可以存储用户相关信息,但是session存储在服务端,JWT存储在客户端

3.基于Token的身份认证如何工作

基于Token的身份认证是无状态的,服务器或者session中不会存储任何用户信息.(很好的解决了共享session的问题)

  • 用户携带用户名和密码请求获取token(接口数据中可使用appId,appKey)
  • 服务端校验用户凭证,并返回用户或客户端一个Token
  • 客户端存储token,并在请求头中携带Token
  • 服务端校验token并返回数据

注意:

  • 随后客户端的每次请求都需要使用token
  • token应该放在header中
  • 需要将服务器设置为接收所有域的请求: Access-Control-Allow-Origin: *

4.用Token的好处

  • 无状态和可扩展性
  • 安全: 防止CSRF攻击;token过期重新认证

5.JWT和OAuth的区别

  • 1.OAuth2是一种授权框架 ,JWT是一种认证协议
  • 2.无论使用哪种方式切记用HTTPS来保证数据的安全性
  • 3.OAuth2用在使用第三方账号登录的情况(比如使用weibo, qq, github登录某个app),而JWT是用在前后端分离, 需要简单的对后台API进行保护时使用

使用Gin框架集成JWT

在Golang语言中,jwt-go库提供了一些jwt编码和验证的工具,因此我们很容易使用该库来实现token认证。

另外,我们也知道gin框架中支持用户自定义middleware,我们可以很好的将jwt相关的逻辑封装在middleware中,然后对具体的接口进行认证。

自定义中间件

在gin框架中,自定义中间件比较容易,只要返回一个gin.HandlerFunc即完成一个中间件定义。

接下来,我们先定义一个用于jwt认证的中间件.

// 定义一个JWTAuth的中间件
func JWTAuth() gin.HandlerFunc {return func(c *gin.Context) {// 通过http header中的token解析来认证token := c.Request.Header.Get("token")if token == "" {c.JSON(http.StatusOK, gin.H{"status": -1,"msg":    "请求未携带token,无权限访问","data":   nil,})c.Abort()return}log.Print("get token: ", token)// 初始化一个JWT对象实例,并根据结构体方法来解析tokenj := NewJWT()// 解析token中包含的相关信息(有效载荷)claims, err := j.ParserToken(token)if err != nil {// token过期if err == TokenExpired {c.JSON(http.StatusOK, gin.H{"status": -1,"msg":    "token授权已过期,请重新申请授权","data":   nil,})c.Abort()return}// 其他错误c.JSON(http.StatusOK, gin.H{"status": -1,"msg":    err.Error(),"data":   nil,})c.Abort()return}// 将解析后的有效载荷claims重新写入gin.Context引用对象中c.Set("claims", claims)}
}

定义jwt编码和解码逻辑

根据前面提到的jwt-token的组成部分,以及jwt-go中相关的定义,我们可以使用如下方法进行生成token.

// 定义一个jwt对象
type JWT struct {// 声明签名信息SigningKey []byte
}// 初始化jwt对象
func NewJWT() *JWT {return &JWT{[]byte("bgbiao.top"),}
}// 自定义有效载荷(这里采用自定义的Name和Email作为有效载荷的一部分)
type CustomClaims struct {Name  string `json:"name"`Email string `json:"email"`// StandardClaims结构体实现了Claims接口(Valid()函数)jwt.StandardClaims
}// 调用jwt-go库生成token
// 指定编码的算法为jwt.SigningMethodHS256
func (j *JWT) CreateToken(claims CustomClaims) (string, error) {// https://gowalker.org/github.com/dgrijalva/jwt-go#Token// 返回一个token的结构体指针token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)return token.SignedString(j.SigningKey)
}// token解码
func (j *JWT) ParserToken(tokenString string) (*CustomClaims, error) {// https://gowalker.org/github.com/dgrijalva/jwt-go#ParseWithClaims// 输入用户自定义的Claims结构体对象,token,以及自定义函数来解析token字符串为jwt的Token结构体指针// Keyfunc是匿名函数类型: type Keyfunc func(*Token) (interface{}, error)// func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {}token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {return j.SigningKey, nil})if err != nil {// https://gowalker.org/github.com/dgrijalva/jwt-go#ValidationError// jwt.ValidationError 是一个无效token的错误结构if ve, ok := err.(*jwt.ValidationError); ok {// ValidationErrorMalformed是一个uint常量,表示token不可用if ve.Errors&jwt.ValidationErrorMalformed != 0 {return nil, fmt.Errorf("token不可用")// ValidationErrorExpired表示Token过期} else if ve.Errors&jwt.ValidationErrorExpired != 0 {return nil, fmt.Errorf("token过期")// ValidationErrorNotValidYet表示无效token} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {return nil, fmt.Errorf("无效的token")} else {return nil, fmt.Errorf("token不可用")}}}// 将token中的claims信息解析出来并断言成用户自定义的有效载荷结构if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {return claims, nil}return nil, fmt.Errorf("token无效")}

定义登陆验证逻辑

接下来的部分就是普通api的具体逻辑了,比如可以在登陆时进行用户校验,成功后未该次认证请求生成token。

// 定义登陆逻辑
// model.LoginReq中定义了登陆的请求体(name,passwd)
func Login(c *gin.Context) {var loginReq model.LoginReqif c.BindJSON(&loginReq) == nil {// 登陆逻辑校验(查库,验证用户是否存在以及登陆信息是否正确)isPass, user, err := model.LoginCheck(loginReq)// 验证通过后为该次请求生成tokenif isPass {generateToken(c, user)} else {c.JSON(http.StatusOK, gin.H{"status": -1,"msg":    "验证失败" + err.Error(),"data":   nil,})}} else {c.JSON(http.StatusOK, gin.H{"status": -1,"msg":    "用户数据解析失败","data":   nil,})}
}// token生成器
// md 为上面定义好的middleware中间件
func generateToken(c *gin.Context, user model.User) {// 构造SignKey: 签名和解签名需要使用一个值j := md.NewJWT()// 构造用户claims信息(负荷)claims := md.CustomClaims{user.Name,user.Email,jwtgo.StandardClaims{NotBefore: int64(time.Now().Unix() - 1000), // 签名生效时间ExpiresAt: int64(time.Now().Unix() + 3600), // 签名过期时间Issuer:    "bgbiao.top",                    // 签名颁发者},}// 根据claims生成token对象token, err := j.CreateToken(claims)if err != nil {c.JSON(http.StatusOK, gin.H{"status": -1,"msg":    err.Error(),"data":   nil,})}log.Println(token)// 封装一个响应数据,返回用户名和tokendata := LoginResult{Name:  user.Name,Token: token,}c.JSON(http.StatusOK, gin.H{"status": 0,"msg":    "登陆成功","data":   data,})return}

定义普通待验证接口

// 定义一个普通controller函数,作为一个验证接口逻辑
func GetDataByTime(c *gin.Context) {// 上面我们在JWTAuth()中间中将'claims'写入到gin.Context的指针对象中,因此在这里可以将之解析出来claims := c.MustGet("claims").(*md.CustomClaims)if claims != nil {c.JSON(http.StatusOK, gin.H{"status": 0,"msg":    "token有效","data":   claims,})}
}// 在主函数中定义路由规则router := gin.Default()v1 := router.Group("/apis/v1/"){v1.POST("/register", controller.RegisterUser)v1.POST("/login", controller.Login)}// secure v1sv1 := router.Group("/apis/v1/auth/")// 加载自定义的JWTAuth()中间件,在整个sv1的路由组中都生效sv1.Use(md.JWTAuth()){sv1.GET("/time", controller.GetDataByTime)}router.Run(":8081")

验证使用JWT后的接口

# 运行项目
$ go run main.go
127.0.0.1
13306
root:bgbiao.top@tcp(127.0.0.1:13306)/test_api?charset=utf8mb4&parseTime=True&loc=Local
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.- using env:   export GIN_MODE=release- using code:   gin.SetMode(gin.ReleaseMode)[GIN-debug] POST   /apis/v1/register         --> warnning-trigger/controller.RegisterUser (3 handlers)
[GIN-debug] POST   /apis/v1/login            --> warnning-trigger/controller.Login (3 handlers)
[GIN-debug] GET    /apis/v1/auth/time        --> warnning-trigger/controller.GetDataByTime (4 handlers)
[GIN-debug] Listening and serving HTTP on :8081# 注册用户
$ curl -i -X POST -H "Content-Type:application/json" -d
'{"name": "hahaha1","password": "hahaha1","email": "hahaha1@bgbiao.top","phone": 10000000000
}' 'http://localhost:8081/apis/v1/register'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 15 Mar 2020 07:09:28 GMT
Content-Length: 41{"data":null,"msg":"success ","status":0}%# 登陆用户以获取token
$ curl -i -X POST -H "Content-Type:application/json" -d
'{"name":"hahaha1","password":"hahaha1"
}' 'http://localhost:8081/apis/v1/login'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 15 Mar 2020 07:10:41 GMT
Content-Length: 290{"data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6ImhhaGFoYTEiLCJlbWFpbCI6ImhhaGFoYTFAYmdiaWFvLnRvcCIsImV4cCI6MTU4NDI1OTg0MSwiaXNzIjoiYmdiaWFvLnRvcCIsIm5iZiI6MTU4NDI1NTI0MX0.HNXSKISZTqzjKd705BOSARmgI8FGGe4Sv-Ma3_iK1Xw","name":"hahaha1"},"msg":"登陆成功","status":0}# 访问需要认证的接口
# 因为我们对/apis/v1/auth/的分组路由中加载了jwt的middleware,因此该分组下的api都需要使用jwt-token认证
$ curl http://localhost:8081/apis/v1/auth/time
{"data":null,"msg":"请求未携带token,无权限访问","status":-1}%# 使用token认证
$ curl http://localhost:8081/apis/v1/auth/time -H 'token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6ImhhaGFoYTEiLCJlbWFpbCI6ImhhaGFoYTFAYmdiaWFvLnRvcCIsImV4cCI6MTU4NDI1OTg0MSwiaXNzIjoiYmdiaWFvLnRvcCIsIm5iZiI6MTU4NDI1NTI0MX0.HNXSKISZTqzjKd705BOSARmgI8FGGe4Sv-Ma3_iK1Xw'
{"data":{"userName":"hahaha1","email":"hahaha1@bgbiao.top","exp":1584259841,"iss":"bgbiao.top","nbf":1584255241},"msg":"token有效","status":0}%

gin-jwt-go源码


https://t.zsxq.com/qNvVfQv (二维码自动识别)

http://weixin.qq.com/r/80hmfo3EAtB9rckd9x3h (二维码自动识别)

delphi接口带上请求头是什么意思_Gin框架中使用JWT进行接口验证相关推荐

  1. delphi接口带上请求头是什么意思_Python接口自动化之Token详解及应用

    ------·今天距2021年258天·------ 这是ITester软件测试小栈第113次推文 在上一篇Python接口自动化测试系列文章:Python接口自动化之cookie.session应用 ...

  2. delphi接口带上请求头是什么意思_python接口自动化(二十)--token登录(详解)...

    简介 为了验证用户登录情况以及减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮.有些登录不是用 cookie 来验证的,是用 token 参数来判断是否登录.token 传参有两种一种是放在请 ...

  3. delphi 使用cef3谷歌浏览器内核加载带header请求头的网页

    上一篇博文介绍到用IE加载带Header请求头的网页,本篇介绍使用cef3实现同样的功能. 我使用的谷歌浏览器内核是49版,支持XP系统的最新版,所以,版本有够低的了. procedure TForm ...

  4. vue怎么让接口带上cookie_在Vue中怎么使用cookie 之 vue-cookies

    cookie 在工作中比较常用, 可以自行封装一些 添加/删除/获取cookie的方法, 可参考这个 在Vue中有个很好用的插件 vue-cookies github地址:https://github ...

  5. python中scrapy加请求头_Python爬虫之scrapy框架随机请求头中间件的设置

    方法一,定义一个存放请求头的列表,并从中随机获取请求头: 获取请求头的网址http://www.useragentstring.com/pages/useragentstring.php?name=A ...

  6. 在Hyperf框架中使用中间件实现接口参数混淆,防止重放攻击拒绝薅羊毛

    前言 前面分享了两篇关于Hyperf框架中RPC 同语言框架跨语言框架的调用,今天来点有意思,具有实战意义的技术分享,hyperf框架和laravel非常相似的PHP框架,按理来说下面的思路可以应用于 ...

  7. 牛客网Java刷题知识点之Java 集合框架的构成、集合框架中的迭代器Iterator、集合框架中的集合接口Collection(List和Set)、集合框架中的Map集合...

    不多说,直接上干货! 集合框架中包含了大量集合接口.这些接口的实现类和操作它们的算法. 集合容器因为内部的数据结构不同,有多种具体容器. 不断的向上抽取,就形成了集合框架. Map是一次添加一对元素. ...

  8. vue怎么让接口带上cookie_在Vue中如何使用Cookie操作实例

    大家好,由于公司忙着赶项目,导致有段时间没有发布新文章了.今天我想跟大家谈谈Cookie的使用.同样,这个Cookie的使用方法是我从公司的项目中抽出来的,为了能让大家看懂,我会尽量写详细点.废话少说 ...

  9. layui table reload post请求_如何实现在Layui框架中完成父窗口刷新(更新)

    最近有个项目使用Layui框架进行后端系统的界面搭建,其中Layui框架中的Layer组件在项目中会用得非常多.且有些功能会遇到子窗口完成数据的提交后,进行关闭同时会刷新父窗口的数据,所以此文章分享一 ...

最新文章

  1. CORS-跨域资源共享 解决跨域问题
  2. 剑指offer:面试题17. 打印从1到最大的n位数
  3. hudson linux节点,Linux 环境下搭建 Jenkins(Hudson)平台
  4. docker desktop 镜像_Spring Boot实战工具4 - Docker
  5. 页面静态化2 --- 使用PHP缓存机制来完成页面静态化(上)(ob_flush和flush函数区别用法)...
  6. HoloLens开发手记 - Unity之Tracking loss
  7. junit5和junit4_JUnit 5 –架构
  8. linux网络编程 华清,Linux网络编程之套接字
  9. 201312-5 I’m stuck!
  10. IOT(20)---2018年有哪些值得期待的物联网应用领域?
  11. 虚拟机和主机通信的机制
  12. micropython入门教程-Micropython入门实操心得
  13. 进化论是个假说,目前解释生物现象最合理
  14. 手电筒安卓_王思聪吃热狗数字时钟安卓apk下载安装ios现在
  15. 大学生计算机知识竞赛,大学生计算机基础知识竞赛题库(试题附答案).docx
  16. 大学计算机信息技术教程电子书资源,教与学中用好教材《大学计算机信息技术教程》.pdf...
  17. Tensorflow2.0---SSD网络原理及代码解析(一)
  18. 【Watir Webdriver】自动化上传照片到百度相册
  19. Origin 图像复制到Word后字体变形
  20. python读写文件的语句_用Python读写文件指南

热门文章

  1. python使用-Pyhton 单行、多行注释符号使用方法及规范
  2. 学完python能做什么-学完Python开发可以从事哪些行业?
  3. 智能机器人语音ic丨智能玩具语音识别芯片丨机器人语音播报芯片丨AI语音提示芯片...
  4. 语音识别(ASR)基础介绍第四篇——当今流行做法与CTC-阿里云开发者社区
  5. Unity VR开发出语音识别功能
  6. 抓包神器之Charles
  7. c++ map是有序还是无序的_go 学习笔记之数组还是切片都没什么不一样
  8. 使用vue创建自己的项目
  9. vue中配置不同的代理同时访问不同的后台
  10. 【王道计组笔记】I/O查询方式