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
}
下面是关键在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
}
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
}
使用JWT(JSON Web Tokens)实现React原生应用权限验证