JSON Web Tokens(JWT)教程

  现在API越来越流行,如何安全保护这些API? JSON Web Tokens(JWT)能提供基于JSON格式的安全认证。它有以下特点:

  • JWT是跨不同语言的,JWT可以在 .NET, Python, Node.js, Java, PHP, Ruby, Go, JavaScript和Haskell中使用
  • JWT是自我包涵的,它们包含了必要的所有信息,这就意味着JWT能够传递关于它自己的基本信息,比如用户信息和签名等。
  • JWT传递是容易的,因为JWT是自我包涵,它们能被完美用在HTTP头部中,当需要授权API时,你只要通过URL一起传送它既可。

JWT易于辨识,是三段由小数点组成的字符串:

aaaaaaaaaa.bbbbbbbbbbb.cccccccccccc

这三部分含义分别是header,payload, signature

Header

头部包含了两个方面:类型和使用的哈希算法(如HMAC SHA256):

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

对这个JSON字符进行base64encode编码,我们就有了首个JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload

JWT的第二部分是payload,也称为 JWT Claims,这里放置的是我们需要传输的信息,有多个项目如注册的claim名称,公共claim名称和私有claim名称。

注册claim名称有下面几个部分:

  • iss: token的发行者
  • sub: token的题目
  • aud: token的客户
  • exp: 经常使用的,以数字时间定义失效期,也就是当前时间以后的某个时间本token失效。
  • nbf: 定义在此时间之前,JWT不会接受处理。
  • iat: JWT发布时间,能用于决定JWT年龄
  • jti: JWT唯一标识. 能用于防止 JWT重复使用,一次只用一个token

 

公共claim名称用于定义我们自己创造的信息,比如用户信息和其他重要信息。

私有claim名称用于发布者和消费者都同意以私有的方式使用claim名称。

下面是JWT的一个案例:

{
"iss": "scotch.io",
"exp": 1300819380,
"name": "Chris Sevilleja",
"admin": true
}

签名

JWT第三部分最后是签名,签名由以下组件组成:

  • header
  • payload
  • 密钥

下面是我们如何得到JWT的第三部分:

var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload); HMACSHA256(encodedString, 'secret');

这里的secret是被服务器签名,我们服务器能够验证存在的token并签名新的token。

 

授权应用

我们看看JWT在实际授权中应用,JWT提供下述功能:

  • 某种程度的用户身份验证
  • 使用密钥签名
  • 客户端每个请求都带有JWT
  • 服务器使用密钥分析和检查claim名称

下面我们看看授权流程,以email和password为例,用户注册:

POST /signup{
  "email": "user @gmail.com",
  "password": "secret" 
} 

一旦成功,用户也需要确认emai或者客户端立即登入:

POST /signin{
  "email": "user @gmail.com",
  "password": "secret" 
} 

服务器使用JWT编码claim响应:

200 OK{
  "jwt": "very.long.string" 
} 

在这个用户会话session阶段,客户端会将JWT包含在HTTP头部中,在claim会定义访问资源和指定的服务。

Authorization: Bearer very.long.string

下面以Go语言为案例说明具体如何编程:

package jwt

当你创建新的JWT token时,你能从你的服务发行claim到客户端,你应该指定iss为你的域名:

const iss = "example.com"

敏感配置应该作为环境变量传递:

import "os"    
var secret = []byte(os.Getenv("JWT_SECRET"))

对于消费者是app,你不必总是授权claim,选择一个默认exp有效期

import "time"    
// expire in two weeks  var exp = time.Hour * 24 * 14

使用下面定义claim:

type Claims map[string]interface{}

可以像一个map定义新的claim对象:

claims := Claims{"sub": user.UUID}

增加一个签名方法给这个定制的map类型:

import "github.com/dgrijalva/jwt-go"

func (c Claims) Sign() string {
    token := jwt.New(jwt.SigningMethodHS256)
    token.Claims["iss"] = iss
    token.Claims["iat"] = time.Now().Unix()
    token.Claims["exp"] = time.Now().Add(exp).Unix()
    for k, v := range c {
        token.Claims[k] = v
    }
    s, err := token.SignedString(secret)
    if err != nil {
        panic(err)
    }
    return s
}

使用方法:

Claims{"sub": user.UUID}.Sign()

下一步是解析token装回claim:

import "errors"
var InvalidToken = errors.New("jwt invalid token")
func Verify(input string) (Claims, error) {
      token, err := jwt.Parse(input, getValidationKey)
      if err != nil {
            return nil, InvalidToken
      }
      if jwt.SigningMethodHS256.Alg() != token.Header["alg"] {
            return nil, InvalidToken
      }

      if !token.Valid {
            return nil, InvalidToken
      }

      if token.Claims["iss"] != iss {
            return nil, InvalidToken
      }
      return Claims(token.Claims), nil

}

func getValidationKey(*jwt.Token) (interface{}, error) {
      return secret, nil
}

你绝对需要用合适算法检查alg,否则黑客会使用none算法忽视安全密钥签名,这意味着任何人都能黑掉你的系统。能够检查iss也是好的,保存部署环境分离,你的产品iss可能是服务的域名,但是在dev开发阶段可以是其他值。

下面是关键在URL资源处检查claim,确认JWT并签名claim给每个请求上下文:

// main.go
package main
import (
    "net/http"
    "strings"
    "example.com/project/jwt"
)
var NotAuthorized = errors.New("not authorized")
func verify(r *http.Request) (jwt.Claims, error) {
    auth := r.Headers.Get("Authorization")
    if auth == "" {
        return nil, NotAuthorized
    }

    parts := strings.Split(auth, " ")
    if len(parts) != 2 || parts[0] != "Bearer" {
        return nil, NotAuthorized
    }

    claims, err := jwt.Verify(parts[1])
    if err != nil {
        return nil, NotAuthorized
    }
    return claims, nil
}

然后在路由控制器中,检查这个claim,用户请求中的JWT是否能够访问这个路由URL:
     

func ModifyResource(w http.ResponseWriter, r *http.Request) {
  claims, err := verify(r)
 if err != nil {
      http.Error(w, err.Error(), 401)
      return
  }

  if !CanModifyResource(claims, r) {
      http.Error(w, "not authorized", 403)
      return
  }
 // do stuff

}

使用JSON Web Tokens和Spring实现微服务

使用JWT(JSON Web Tokens)实现React原生应用权限验证

微服务专题

REST专题