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):签发时间
jti(JWT 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了
工作流程
- 应用(或者客户端)想授权服务器请求授权。例如,如果用授权码流程的话,就是/oauth/authorize
- 当授权被许可以后,授权服务器返回一个access token给应用
- 应用使用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},
})
}
|