背景: 在现在先后端分离开发的大环境中,咱们须要解决一些登录,后期身份认证以及鉴权相关的事情,一般的方案就是采用请求头携带token的方式进行实现。本篇文章主要分享下在Golang语言下使用jwt-go来实现后端的token认证逻辑。git
JSON Web Token(JWT)
是一个经常使用语HTTP的客户端和服务端间进行身份认证和鉴权的标准规范,使用JWT能够容许咱们在用户和服务器之间传递安全可靠的信息。github
在开始学习JWT以前,咱们能够先了解下早期的几种方案。web
Cookieredis
Cookie老是保存在客户端中,按在客户端中的存储位置,可分为内存Cookie
和硬盘Cookie
。算法
内存Cookie由浏览器维护,保存在内存中,浏览器关闭后就消失了,其存在时间是短暂的。硬盘Cookie保存在硬盘里,有一个过时时间,除非用户手工清理或到了过时时间,硬盘Cookie不会被删除,其存在时间是长期的。因此,按存在时间,可分为非持久Cookie和持久Cookie
。数据库
cookie 是一个很是具体的东西,指的就是浏览器里面能永久存储的一种数据,仅仅是浏览器实现的一种数据存储功能。json
cookie由服务器生成,发送给浏览器
,浏览器把cookie以key-value形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该cookie发送给服务器。因为cookie是存在客户端上的,因此浏览器加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间,因此每一个域的cookie数量是有限的。后端
Sessionapi
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
通常而言,用户注册登录后会生成一个jwt token返回给浏览器,浏览器向服务端请求数据时携带token
,服务器端使用signature
中定义的方式进行解码,进而对token进行解析和验证。
header
{
"alg": "HS256",
"typ": "JWT"
}
复制代码
对上面的json进行base64编码便可获得JWT的第一个部分
payload
{
"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的组成部分。
因此,基本上整个过程分为两个阶段,第一个阶段,客户端向服务端获取token,第二阶段,客户端带着该token去请求相关的资源.
一般比较重要的是,服务端如何根据指定的规则进行token的生成。
在认证的时候,当用户用他们的凭证成功登陆之后,一个JSON Web Token将会被返回。
此后,token就是用户凭证了,你必须很是当心以防止出现安全问题。
通常而言,你保存令牌的时候不该该超过你所须要它的时间。
不管什么时候用户想要访问受保护的路由或者资源的时候,用户代理(一般是浏览器)都应该带上JWT,典型的,一般放在Authorization header中,用Bearer schema: Authorization: Bearer <token>
服务器上的受保护的路由将会检查Authorization header中的JWT是否有效,若是有效,则用户能够访问受保护的资源。若是JWT包含足够多的必需的数据,那么就能够减小对某些操做的数据库查询的须要,尽管可能并不老是如此。
若是token是在受权头(Authorization header)中发送的,那么跨源资源共享(CORS)将不会成为问题,由于它不使用cookie.
1.基于服务器的认证
前面说到过session,cookie以及token的区别,在以前传统的作法就是基于存储在服务器上的session来作用户的身份认证,可是一般会有以下问题:
2.Session和JWT Token的异同
均可以存储用户相关信息,可是session存储在服务端,JWT存储在客户端
3.基于Token的身份认证如何工做
基于Token的身份认证是无状态的,服务器或者session中不会存储任何用户信息.(很好的解决了共享session的问题)
注意:
Access-Control-Allow-Origin: *
4.用Token的好处
5.JWT和OAuth的区别
使用第三方帐号登陆的状况
(好比使用weibo, qq, github登陆某个app),而
JWT是用在先后端分离
, 须要简单的对后台API进行保护时使用
在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对象实例,并根据结构体方法来解析token
j := 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-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.LoginReq
if c.BindJSON(&loginReq) == nil {
// 登录逻辑校验(查库,验证用户是否存在以及登录信息是否正确)
isPass, user, err := model.LoginCheck(loginReq)
// 验证经过后为该次请求生成token
if 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)
// 封装一个响应数据,返回用户名和token
data := 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 v1
sv1 := router.Group("/apis/v1/auth/")
// 加载自定义的JWTAuth()中间件,在整个sv1的路由组中都生效
sv1.Use(md.JWTAuth())
{
sv1.GET("/time", controller.GetDataByTime)
}
router.Run(":8081")
复制代码
# 运行项目
$ 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}%
复制代码
本文使用 mdnice 排版