JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准( RFC 7519 ),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
JWT包含了使用 .
分隔的三部分:
其结构看起来是这样的
xxxxx.yyyyy.zzzzz
在header中通常包含了两部分:token类型和采用的加密算法。
{ "alg": "HS256", "typ": "JWT" }
接下来对这部分内容使用 Base64Url 编码组成了JWT结构的第一部分。
Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据,有三种类型的claim: reserved , public 和 private .
iss(签发者)
, exp(过期时间戳)
, sub(面向的用户)
, aud(接收方)
, iat(签发时间)
。 负载使用的例子:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
上述的负载需要经过 Base64Url 编码后作为JWT结构的第二部分。
创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。例如如果希望使用HMAC SHA256算法,那么签名应该使用下列方式创建:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名用于验证消息的发送者以及消息是没有经过篡改的。
JWT格式的输出是以 .
分隔的三段Base64编码,与SAML等基于XML的标准相比,JWT在HTTP和HTML环境中更容易传递。
下列的JWT展示了一个完整的JWT格式,它拼接了之前的Header, Payload以及秘钥签名:
在身份鉴定的实现中,传统方法是在服务端存储一个session,给客户端返回一个cookie,而使用JWT之后,当用户使用它的认证信息登陆系统之后,会返回给用户一个JWT,用户只需要本地保存该token(通常使用local storage,也可以使用cookie)即可。
当用户希望访问一个受保护的路由或者资源的时候,通常应该在 Authorization
头部使用 Bearer
模式添加JWT,其内容看起来是下面这样:
Authorization: Bearer <token>
因为用户的状态在服务端的内存中是不存储的,所以这是一种 无状态 的认证机制。服务端的保护路由将会检查请求头 Authorization
中的JWT信息,如果合法,则允许用户的行为。由于JWT是自包含的,因此减少了需要查询数据库的需要。
JWT的这些特性使得我们可以完全依赖其无状态的特性提供数据API服务,甚至是创建一个下载流服务。因为JWT并不使用Cookie的,所以你可以使用任何域名提供你的API服务而不需要担心跨域资源共享问题(CORS)。
下面的序列图展示了该过程:
相比XML格式,JSON更加简洁,编码之后更小,这使得JWT比SAML更加简洁,更加适合在HTML和HTTP环境中传递。
在安全性方面,SWT只能够使用HMAC算法和共享的对称秘钥进行签名,而JWT和SAML token则可以使用X.509认证的公私秘钥对进行签名。与简单的JSON相比,XML和XML数字签名会引入复杂的安全漏洞。
因为JSON可以直接映射为对象,在大多数编程语言中都提供了JSON解析器,而XML则没有这么自然的文档-对象映射关系,这就使得使用JWT比SAML更方便。
//auth.go package main import "net/http" funchomePage(reshttp.ResponseWriter, req *http.Request){ res.Write([]byte("Home Page")) } funcmain(){ http.HandleFunc("/", homePage) http.ListenAndServe(":8080", nil) }
gogetgithub.com/dgrijalva/jwt-go
创建请求
type MyCustomClaims struct { // This will hold a users username after authenticating. // Ignore `json:"username"` it's required by JSON Usernamestring `json:"username"` // This will hold claims that are recommended having (Expiration, issuer) jwt.StandardClaims }
创建Handle设置客户端cookie
http.HandleFunc("/setToken", setToken)
funcsetToken(reshttp.ResponseWriter, req *http.Request) { // Expires the token and cookie in 24 hours expireToken := time.Now().Add(time.Hour * 24).Unix() expireCookie := time.Now().Add(time.Hour * 24) // We'll manually assign the claims but in production you'd insert values from a database claims := MyCustomClaims { "myusername", jwt.StandardClaims { ExpiresAt: expireToken, Issuer: "example.com", }, } // Create the token using your claims token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Signs the token with a secret. signedToken, _ := token.SignedString([]byte("secret")) // This cookie will store the token on the client side cookie := http.Cookie{Name: "Auth", Value: signedToken, Expires: expireCookie, HttpOnly: true} http.SetCookie(res, &cookie) // Redirect the user to his profile http.Redirect(res, req, "/profile", 301) }
中间件可以在http请求前执行
// Middleware to protect private pages funcvalidate(protectedPagehttp.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(reshttp.ResponseWriter, req *http.Request){ //Validate the token and if it passes call the protected handler below. protectedPage(res, req) }) }
首先,让我们确定一个cookie
// Middleware to protect private pages funcvalidate(protectedPagehttp.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(reshttp.ResponseWriter, req *http.Request){ // If no Auth cookie is set then return a 404 not found cookie, err := req.Cookie("Auth") if err != nil { http.NotFound(res, req) return } //Validate the token and if it passes call the protected handler below. protectedPage(res, req) }) }
从 cookie 中提取token
// Middleware to protect private pages funcvalidate(protectedPagehttp.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(reshttp.ResponseWriter, req *http.Request){ // If no Auth cookie is set then return a 404 not found cookie, err := req.Cookie("Auth") if err != nil { http.NotFound(res, req) return } // Cookies concatenate the key/value. Remove the Auth= part splitCookie := strings.Split(cookie.String(), "Auth=") //Validate the token and if it passes call the protected handler below. protectedPage(res, req) }) }
验证token
// Middleware to protect private pages funcvalidate(protectedPagehttp.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(reshttp.ResponseWriter, req *http.Request){ // If no Auth cookie is set then return a 404 not found cookie, err := req.Cookie("Auth") if err != nil { http.NotFound(res, req) return } // The token is concatenated with its key Auth=token // We remove the Auth= part by splitting the cookie in two splitCookie := strings.Split(cookie.String(), "Auth=") // Parse, validate and return a token. token, err := jwt.ParseWithClaims(splitCookie[1], &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error){ // Prevents a known exploit if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok{ return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"]) } return []byte("secret"), nil }) protectedPage(res, req) }) }
我们使用 gorilla/context 作为项目的context
gogetgithub.com/gorilla/context
// Middleware to protect private pages funcvalidate(protectedPagehttp.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(reshttp.ResponseWriter, req *http.Request){ // If no Auth cookie is set then return a 404 not found cookie, err := req.Cookie("Auth") if err != nil { http.NotFound(res, req) return } // The token is concatenated with its key Auth=token // We remove the Auth= part by splitting the cookie in two splitCookie := strings.Split(cookie.String(), "Auth=") // Parse, validate and return a token. token, err := jwt.ParseWithClaims(splitCookie[1], &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error){ // Prevents a known exploit if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok{ return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"]) } return []byte("secret"), nil }) // Validate the token and save the token's claims to a context if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid { context.Set(req, "Claims", claims) } else { http.NotFound(res, req) return } // If everything is valid then call the original protected handler protectedPage(res, req) }) }
这个页面只通过token验证的才能访问
funcprofile(reshttp.ResponseWriter, req *http.Request){ claims := context.Get(req, "Claims").(*MyCustomClaims) res.Write([]byte(claims.Username)) context.Clear(req) }
Demo 完成 !!!
下面是完整的代码
package main import "github.com/dgrijalva/jwt-go" import "github.com/gorilla/context" import "net/http" import "fmt" import "strings" import "time" type MyCustomClaims struct { Usernamestring `json:"username"` jwt.StandardClaims } funcsetToken(reshttp.ResponseWriter, req *http.Request) { expireToken := time.Now().Add(time.Hour * 24).Unix() expireCookie := time.Now().Add(time.Hour * 24) claims := MyCustomClaims { "myusername", jwt.StandardClaims { ExpiresAt: expireToken, Issuer: "example.com", }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signedToken, _ := token.SignedString([]byte("secret")) cookie := http.Cookie{Name: "Auth", Value: signedToken, Expires: expireCookie, HttpOnly: true} http.SetCookie(res, &cookie) http.Redirect(res, req, "/profile", 301) } funcvalidate(protectedPagehttp.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(reshttp.ResponseWriter, req *http.Request){ cookie, err := req.Cookie("Auth") if err != nil { http.NotFound(res, req) return } splitCookie := strings.Split(cookie.String(), "Auth=") token, err := jwt.ParseWithClaims(splitCookie[1], &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error){ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok{ return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"]) } return []byte("secret"), nil }) if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid { context.Set(req, "Claims", claims) } else { http.NotFound(res, req) return } protectedPage(res, req) }) } funcprofile(reshttp.ResponseWriter, req *http.Request){ claims := context.Get(req, "Claims").(*MyCustomClaims) res.Write([]byte(claims.Username)) context.Clear(req) } funchomePage(reshttp.ResponseWriter, req *http.Request){ res.Write([]byte("Home Page")) } funcmain(){ http.HandleFunc("/profile", validate(profile)) http.HandleFunc("/setToken", setToken) http.HandleFunc("/", homePage) http.ListenAndServe(":8080", nil) }
参考文献:
https://jwt.io/introduction/
https://dinosaurscode.xyz/go/2016/06/17/golang-jwt-authentication/
Go与Json-Web-Token