JWT是JSON Web Token的缩写,是为了在网络应用环境间传递声明而执行的- -种基于JSON的开放标准((RFC 7519)。JWT本身没有定义任何技术实现,它只是定义了一种基于Token的会话管理的规则,涵盖Token需要包含的标准内容和Token的生成过程,特别适用于分布式站点的单点登录(SSO) 场景。
提示:以下是本篇文章正文内容,下面案例可供参考
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:
因此,一个典型的JWT看起来是这个样子的:
xxxxx.yyyyy.zzzzz
接下来,具体看一下每一部分:
Header
header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。
例如:
然后,用Base64对这个JSON编码就得到JWT的第一部分
Payload
JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。
Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
Public claims : 可以随意定义。
Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。
下面是一个例子:
对payload进行Base64编码就得到JWT的第二部分
注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的。
Signature
为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。
例如:
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
官方连接:https://jwt.io/
看一张官网的图就明白了:
在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。
无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema。
header应该看起来是这样的:
Authorization: Bearer
服务器上的受保护的路由将会检查Authorization header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。如果JWT包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。
如果token是在授权头(Authorization header)中发送的,那么跨源资源共享(CORS)将不会成为问题,因为它不使用cookie。
下面这张图显示了如何获取JWT以及使用它来访问APIs或者资源:
在讨论基于Token的身份认证是如何工作的以及它的好处之前,我们先来看一下以前我们是怎么做的:
HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证
传统的做法是将已经认证过的用户信息存储在服务器上,比如Session。用户下次请求的时候带着Session ID,然后服务器以此检查用户是否认证过。
这种基于服务器的身份认证方式存在一些问题:
基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。
没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。
虽然这一实现可能会有所不同,但其主要流程如下:
注意:
Payload主要内容:
package models
import (
"github.com/dgrijalva/jwt-go"
)
type CustomClaims struct {
//加入自己的信息
ID uint
NickName string
AuthorityId uint
//jwt的信息
jwt.StandardClaims
}
package middlewares
import (
"errors"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"mxshop-api/user-web/global"
"mxshop-api/user-web/models"
"net/http"
"time"
)
//中间件
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localSstorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
token := c.Request.Header.Get("x-token")
if token == "" {
c.JSON(http.StatusUnauthorized, map[string]string{
"msg": "请登录",
})
c.Abort()
return
}
j := NewJWT()
// parseToken 解析token包含的信息
claims, err := j.ParseToken(token)
if err != nil {
if err == TokenExpired {
if err == TokenExpired {
c.JSON(http.StatusUnauthorized, map[string]string{
"msg": "授权已过期",
})
c.Abort()
return
}
}
c.JSON(http.StatusUnauthorized, "未登陆")
c.Abort()
return
}
c.Set("claims", claims)
c.Set("userId", claims.ID)
c.Next()
}
}
type JWT struct {
SigningKey []byte
}
var (
TokenExpired = errors.New("Token is expired")
TokenNotValidYet = errors.New("Token not active yet")
TokenMalformed = errors.New("That's not even a token")
TokenInvalid = errors.New("Couldn't handle this token:")
)
func NewJWT() *JWT {
zap.S().Info(global.ServerConfig.JWTInfo.SigningKey)
return &JWT{
//自己的key(VERIFY SIGNATURE your-256-bit-secret)
[]byte(global.ServerConfig.JWTInfo.SigningKey), //可以设置过期时间
}
}
// 创建一个token
func (j *JWT) CreateToken(claims models.CustomClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey)
}
// 解析 token
func (j *JWT) ParseToken(tokenString string) (*models.CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &models.CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) {
return j.SigningKey, nil
})
if err != nil {
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, TokenMalformed
} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
// Token is expired
return nil, TokenExpired
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, TokenNotValidYet
} else {
return nil, TokenInvalid
}
}
}
if token != nil {
if claims, ok := token.Claims.(*models.CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, TokenInvalid
} else {
return nil, TokenInvalid
}
}
// 更新token
func (j *JWT) RefreshToken(tokenString string) (string, error) {
jwt.TimeFunc = func() time.Time {
return time.Unix(0, 0)
}
token, err := jwt.ParseWithClaims(tokenString, &models.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(*models.CustomClaims); ok && token.Valid {
jwt.TimeFunc = time.Now
claims.StandardClaims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix()
return j.CreateToken(*claims)
}
return "", TokenInvalid
}
主要实在登录完之后生成
如果用Ajax这种非简单请求等等还得解决跨域问题 这里用中间件解决 其实前端后端都可以解决
再设计一个中间件用来判断是不是管理员
判断管理员中间件
package middlewares
import (
"awesomeProject3/models"
"github.com/gin-gonic/gin"
"net/http"
)
func IsAdminAuth() gin.HandlerFunc {
return func(ctx *gin.Context) {
//在jwt组件中已经将信息加入到context里了
claims, _ := ctx.Get("claims")
currentUser := claims.(*models.CustomClaims)
if currentUser.AuthorityId != 2 {
ctx.JSON(http.StatusForbidden, gin.H{
"msg": "无权限",
})
ctx.Abort()
return
}
ctx.Next()
}
}
生成token&验证
package main
import (
"awesomeProject3/middlewares"
"awesomeProject3/models"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func main() {
r := gin.Default()
r.GET("/mm", func(ctx *gin.Context) {
j := middlewares.NewJWT()
claims := models.CustomClaims{
ID: uint(123),
NickName: "张三",
AuthorityId: uint(2), //这个我用来识别管理员和普通用户
StandardClaims: jwt.StandardClaims{
NotBefore: time.Now().Unix(), //签名的生效时间
ExpiresAt: time.Now().Unix() + 60*60*24*30, //30天过期
Issuer: "nihao",
},
}
token, err := j.CreateToken(claims)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
"msg": "生成token失败",
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"id": "123",
"nick_name": "张三",
"token": token,
"expiresd_at": (time.Now().Unix() + 60*60*24*30) * 1000,
})
})
//验证token 添加中间件
r.GET("/tok", middlewares.JWTAuth(), middlewares.IsAdminAuth(), func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{
"msg": "ok",
})
})
_ = r.Run(":8056")
}
运行之后用测试工具测试 比如:Apifox
在非简单请求且跨域的情况下,浏览器会发起options预检请求。
Preflighted Requests是CORS中一种透明服务器验证机制。预检请求首先需要向另外一个域名的资源发送一个 HTTP OPTIONS 请求头,其目的就是为了判断实际发送的请求是否是安全的。
简单请求需满足以下两个条件
(1). 请求方法是以下三种方法之一:
(2). HTTP 的头信息不超出以下几种字段:
非简单请求即是复杂请求
常见的复杂请求有:
在跨域的情况下,非简单请求会先发起一次空body的OPTIONS请求,称为"预检"请求,用于向服务器请求权限信息,等预检请求被成功响应后,才发起真正的http请求。
浏览器的预检请求结果可以通过设置Access-Control-Max-Age进行缓存
来一段复杂请求
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js">script>
head>
<body>
<button type="button" id="query">请求数据button>
<div id="content" style="background-color: aquamarine; width: 300px;height: 500px">div>
body>
<script type="text/javascript">
$("#query").click(function () {
$.ajax(
{
url:"http://127.0.0.1:8021/u/v1/user/list",
dataType: "json",
type: "get",
beforeSend: function(request) {
request.setRequestHeader("x-token", "awslawslawslawslawslawsl")
},
success: function (result) {
console.log(result.data);
$("#content").text(result.data)
},
error: function (data) {
alert("请求出错")
}
}
);
});
script>
html>
设计一个中间件即可 中间件应该都会用
package middlewares
import (
"github.com/gin-gonic/gin"
"net/http"
)
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
c.Header("Access-Control-Allow-Origin", "*")
//哪些Headers可以加
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token,Authorization,Token,x-token")
c.Header("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PATCH,PUT")
//暴露Headers
c.Header("Access-Control-Expose-Headers", "Content-Length,Access-Control-Allow-Origin,Access-Control-Allow-Headers,Content-Type")
//以上可以添
c.Header("Access-Control-Allow-Credentials", "true")
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
}
}