Featured image of post JWT使用

JWT使用

描述内容描述内容

本文阅读量

JWT介绍

JWT是 ISON Web Token的缩写,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC7519)。JWT本身没有定义任何技术实现,它只是定义了一种基于 Token的会话管理的规则,涵盖 Token需要包含的标准內容和 Token的生成过程,特别适用于分布式站点的单点登录(SSO)场景。

组成

一个JWT Token 有三部分组成

  • 头部(Header)
  • 负载(Payload)
  • 签名(Signature)

头部和负载以JSON形式存在,这就是JWT中的JSON,三部分的内容都分别单独经过了Base64编码,以.拼接成一个 JWT Token

header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。

1
2
3
4
{
    "alg": "HS256",
    "typ":"JWT"
}

然后,用Base64对这个JSON编码就得到JWT的第一部分

Payload

也是一个json对象,JWT规定了7个官方字段供使用

1
2
3
4
5
6
7
iss issuer):签发人
exp expiration time):过期时间
sub subject):主题
aud audience):受众
nbf Not Before):生效时间
iat Issued At):签发时间
jtiJWT ID):编号

除了官方字段们,我们也可以自己指定字段和内容

1
2
3
4
5
{
    "sub": "12312312",
    "name": "John Doe",
    "admin": true
}

JWT默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个JSON对象也要使用Base64URL算法转成字符串。

Signature

对前两部分起那么,防止数据篡改。

1
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
  • secret 秘钥保存在服务端,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了

工作流程

  1. 应用(或者客户端)想授权服务器请求授权。例如,如果用授权码流程的话,就是/oauth/authorize
  2. 当授权被许可以后,授权服务器返回一个access token给应用
  3. 应用使用access token访问受保护的资源(比如:API)

优缺点

JWT拥有基于 Token的会话管理方式所拥有的一切优势,不依赖 Cookie,使得其可以防止CSRF攻击,也能在禁用 Cookie的浏览器环境中正常运行。 而JWT的最大优势是服务端不再需要存储 Session,使得服务端认证鉴权业务可以方便扩展,避免存储Session所需要引入的 Redis等组件,降低了系统架构复杂度。但这也是JWT最大的劣势,由于有效期存储在 Token中, JWT Token一旦签发,就会在有效期内一直可用,无法在服务端废止,当用户进行登岀操作,只能侬赖客户端删除掉本地存储的JwTtoken,如果需要禁用用户,单纯使用JWT就无法做到。

实战

php

定义需求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//头部
private static $header=array(
  'alg'=>'HS256', //生成signature的算法
  'typ'=>'JWT'    //类型
);

//使用HMAC生成信息摘要时所使用的密钥
private static $secret='123456';

//过期时间
private static $out_time = 7200;

生成jwt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
 * 获取jwt token
 * @param array $payload jwt载荷   格式如下非必须
 * [
 *  'iss'=>'jwt_admin',  //该JWT的签发者
 *  'iat'=>time(),  //签发时间
 *  'jti'=>md5(uniqid('JWT').time())  //该Token唯一标识
 * ]
 * @return bool|string
 */
public static function getToken(array $payload){
  if(is_array($payload))
  {
    $base64header=self::base64UrlEncode(json_encode(self::$header,JSON_UNESCAPED_UNICODE));
    $base64payload=self::base64UrlEncode(json_encode($payload,JSON_UNESCAPED_UNICODE));	        			   $token=$base64header.'.'.$base64payload.'.'.self::signature($base64header.'.'.$base64payload,self::$secret,self::$header['alg']);
    return $token;
  }else{
    return false;
  }
}

/**
 * base64UrlEncode   https://jwt.io/  中base64UrlEncode编码实现
 * @param string $input 需要编码的字符串
 * @return string
 */
private static function base64UrlEncode(string $input)
{
  return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
}

/**
 * HMACSHA256签名   https://jwt.io/  中HMACSHA256签名实现
 * @param string $input 为base64UrlEncode(header).".".base64UrlEncode(payload)
 * @param string $key
 * @return mixed
 */
private static function signature(string $input, string $key)
{
  return self::base64UrlEncode(hash_hmac('sha256', $input, $key,true));
}

解析jwt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static function verifyToken(string $Token)
{
  $tokens = explode('.', $Token);
  if (count($tokens) != 3)
    return false;

  list($base64header, $base64payload, $sign) = $tokens;

  //获取jwt算法
  $base64decodeheader = json_decode(self::base64UrlDecode($base64header), JSON_OBJECT_AS_ARRAY);
  if (empty($base64decodeheader['alg']))
    return false;

  //签名验证
  if (self::signature($base64header . '.' . $base64payload, self::$key, $base64decodeheader['alg']) !== $sign)
    return false;

  $payload = json_decode(self::base64UrlDecode($base64payload), JSON_OBJECT_AS_ARRAY);

  //根据签发时间判断是否过期
  if (isset($payload['iat']) && $payload['iat'] > time()+self::out_time)
    return false;

  return $payload;
}

/**
 * base64UrlEncode  https://jwt.io/  中base64UrlEncode解码实现
 * @param string $input 需要解码的字符串
 * @return bool|string
 */

private static function base64UrlDecode(string $input)
{
  $remainder = strlen($input) % 4;
  if ($remainder) {
    $addlen = 4 - $remainder;
    $input .= str_repeat('=', $addlen);
  }
  return base64_decode(strtr($input, '-_', '+/'));
}

golang

使用jwt-go这个库来实现我们生成JWT和解析JWT的功能。

定义需求

我们需要规定在JWT中要存储username信息,那么我们就定义一个MyClaims结构体如下

1
2
3
4
5
6
7
8
// MyClaims 自定义声明结构体并内嵌jwt.StandardClaims
// jwt包自带的jwt.StandardClaims只包含了官方字段
// 我们这里需要额外记录一个username字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type MyClaims struct {
	Username string `json:"username"`
	jwt.StandardClaims
}

然后我们定义JWT的过期时间,这里以2小时为例:

1
const TokenExpireDuration = time.Hour * 2

接下来还需要定义Secret:

1
var MySecret = []byte("夏天夏天悄悄过去")

生成JWT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// GenToken 生成JWT
func GenToken(username string) (string, error) {
	// 创建一个我们自己的声明
	c := MyClaims{
		"username", // 自定义字段
		jwt.StandardClaims{
			ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
			Issuer:    "my-project",                               // 签发人
		},
	}
	// 使用指定的签名方法创建签名对象
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
	// 使用指定的secret签名并获得完整的编码后的字符串token
	return token.SignedString(MySecret)
}

解析JWT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// ParseToken 解析JWT
func ParseToken(tokenString string) (*MyClaims, error) {
	// 解析token
	var mc = new(MyClaims)
	token, err := jwt.ParseWithClaims(tokenString, mc, func(token *jwt.Token) (i interface{}, err error) {
		return mySecret, nil
	})
	if err != nil {
		return nil, err
	}
	if token.Valid { // 校验token
		return mc, nil
	}
	return nil, errors.New("invalid token")
}

gin中使用

首先我们注册一条路由/auth,对外提供获取Token的渠道:

1
r.POST("/auth", authHandler)

我们的authHandler定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func authHandler(c *gin.Context) {
	// 用户发送用户名和密码过来
	var user UserInfo
	err := c.ShouldBind(&user)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 2001,
			"msg":  "无效的参数",
		})
		return
	}
	// 校验用户名和密码是否正确
	if user.Username == "q1mi" && user.Password == "q1mi123" {
		// 生成Token
		tokenString, _ := GenToken(user.Username)
		c.JSON(http.StatusOK, gin.H{
			"code": 2000,
			"msg":  "success",
			"data": gin.H{"token": tokenString},
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"code": 2002,
		"msg":  "鉴权失败",
	})
	return
}

用户通过上面的接口获取Token之后,后续就会携带着Token再来请求我们的其他接口,这个时候就需要对这些请求的Token进行校验操作了,很显然我们应该实现一个检验Token的中间件,具体实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// JWTAuthMiddleware 基于JWT的认证中间件
func JWTAuthMiddleware() func(c *gin.Context) {
	return func(c *gin.Context) {
		// 客户端携带Token有三种方式 1.放在请求头 2.放在请求体 3.放在URI
		// 这里假设Token放在Header的Authorization中,并使用Bearer开头
		// 这里的具体实现方式要依据你的实际业务情况决定
		authHeader := c.Request.Header.Get("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusOK, gin.H{
				"code": 2003,
				"msg":  "请求头中auth为空",
			})
			c.Abort()
			return
		}
		// 按空格分割
		parts := strings.SplitN(authHeader, " ", 2)
		if !(len(parts) == 2 && parts[0] == "Bearer") {
			c.JSON(http.StatusOK, gin.H{
				"code": 2004,
				"msg":  "请求头中auth格式有误",
			})
			c.Abort()
			return
		}
		// parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
		mc, err := ParseToken(parts[1])
		if err != nil {
			c.JSON(http.StatusOK, gin.H{
				"code": 2005,
				"msg":  "无效的Token",
			})
			c.Abort()
			return
		}
		// 将当前请求的username信息保存到请求的上下文c上
		c.Set("username", mc.Username)
		c.Next() // 后续的处理函数可以用过c.Get("username")来获取当前请求的用户信息
	}
}

注册一个/home路由,发个请求验证一下吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
r.GET("/home", JWTAuthMiddleware(), homeHandler)

func homeHandler(c *gin.Context) {
	username := c.MustGet("username").(string)
	c.JSON(http.StatusOK, gin.H{
		"code": 2000,
		"msg":  "success",
		"data": gin.H{"username": username},
	})
}
使用 Hugo 构建
主题 StackJimmy 设计