JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
- 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
JWT的主要应用场景
- 身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。
- 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
- 前后端分离是一个很有趣的议题,它不仅仅是指前后端工程师之间的相互独立的合作分工方式,更是前后端之间开发模式与交互模式的模块化、解耦化。计算机世界的经验告诉我们,对于复杂的事物,模块化总是好的,无论是后端API开发中越来越成为规范的RESTful API风格,还是Web前端越来越多的模板、框架(参见MVC,MVP 和 MVVM 的图示),包括移动应用中前后端天然分离的特质,都证实了前后端分离的重要性与必要性(更生动的细节与实例说明可以参看赫门分享的主题淘宝前后端分离实践)。实现前后端分离,对于后端开发人员来说是一件很幸福的事情,因为不需要再考虑怎样在HTML中套入数据,只关心数据逻辑的处理;而前端则需要承担接收数据之后界面呈现、用户交互、数据传递等所有任务。虽然这看起来加重了前端的工作量,但实际上有越来越多丰富多样的前端框架可供选择,这让前端开发变得越来越结构化、系统化,前端工程师也不再只是“套版的”。
JWT的结构
JWT包含了使用.
分隔的三部分:
- Header 头部
- Payload 负载
- Signature 签名
其结构看起来是这样的
1 2 |
xxxxx.yyyyy.zzzzz |
Header
在header中通常包含了两部分:token类型和采用的加密算法。
1 2 3 4 5 |
{ "alg": "HS256", "typ": "JWT" } |
接下来对这部分内容使用 Base64Url 编码组成了JWT结构的第一部分。
Payload
Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据,有三种类型的claim: reserved, public 和 private.
- Reserved claims: 这些claim是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用,常用的有
iss(签发者)
,exp(过期时间戳)
,sub(面向的用户)
,aud(接收方)
,iat(签发时间)
。 - Public claims:根据需要定义自己的字段,注意应该避免冲突
- Private claims:这些是自定义的字段,可以用来在双方之间交换信息
负载使用的例子:
1 2 3 4 5 6 |
{ "sub": "1234567890", "name": "John Doe", "admin": true } |
上述的负载需要经过Base64Url编码后作为JWT结构的第二部分。
Signature
创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。例如如果希望使用HMAC SHA256算法,那么签名应该使用下列方式创建:
1 2 3 4 5 |
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) |
签名用于验证消息的发送者以及消息是没有经过篡改的。
完整的JWT
JWT格式的输出是以.
分隔的三段Base64编码,与SAML等基于XML的标准相比,JWT在HTTP和HTML环境中更容易传递。
下列的JWT展示了一个完整的JWT格式,它拼接了之前的Header, Payload以及秘钥签名:
如何使用JWT?
在身份鉴定的实现中,传统方法是在服务端存储一个session,给客户端返回一个cookie,而使用JWT之后,当用户使用它的认证信息登陆系统之后,会返回给用户一个JWT,用户只需要本地保存该token(通常使用local storage,也可以使用cookie)即可。
当用户希望访问一个受保护的路由或者资源的时候,通常应该在Authorization
头部使用Bearer
模式添加JWT,其内容看起来是下面这样:
1 2 |
Authorization: Bearer <token> |
因为用户的状态在服务端的内存中是不存储的,所以这是一种无状态的认证机制。服务端的保护路由将会检查请求头Authorization
中的JWT信息,如果合法,则允许用户的行为。由于JWT是自包含的,因此减少了需要查询数据库的需要。
JWT的这些特性使得我们可以完全依赖其无状态的特性提供数据API服务,甚至是创建一个下载流服务。因为JWT并不使用Cookie的,所以你可以使用任何域名提供你的API服务而不需要担心跨域资源共享问题(CORS)。
下面的序列图展示了该过程:
为什么要使用JWT?
相比XML格式,JSON更加简洁,编码之后更小,这使得JWT比SAML更加简洁,更加适合在HTML和HTTP环境中传递。
在安全性方面,SWT只能够使用HMAC算法和共享的对称秘钥进行签名,而JWT和SAML token则可以使用X.509认证的公私秘钥对进行签名。与简单的JSON相比,XML和XML数字签名会引入复杂的安全漏洞。
因为JSON可以直接映射为对象,在大多数编程语言中都提供了JSON解析器,而XML则没有这么自然的文档-对象映射关系,这就使得使用JWT比SAML更方便。
创建一个Go web项目:
首先新建一个auth.go
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//auth.go package main import "net/http" func homePage(res http.ResponseWriter, req *http.Request){ res.Write([]byte("Home Page")) } func main(){ http.HandleFunc("/", homePage) http.ListenAndServe(":8080", nil) } |
设置 TOKEN
1 |
go get github.com/dgrijalva/jwt-go |
创建请求
1 2 3 4 5 6 7 8 |
type MyCustomClaims struct { // This will hold a users username after authenticating. // Ignore `json:"username"` it's required by JSON Username string `json:"username"` // This will hold claims that are recommended having (Expiration, issuer) jwt.StandardClaims } |
创建Handle设置客户端cookie
1 |
http.HandleFunc("/setToken", setToken) |
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 setToken(res http.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请求前执行
1 2 3 4 5 6 7 |
// Middleware to protect private pages func validate(protectedPage http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request){ //Validate the token and if it passes call the protected handler below. protectedPage(res, req) }) } |
首先,让我们确定一个cookie
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Middleware to protect private pages func validate(protectedPage http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(res http.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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Middleware to protect private pages func validate(protectedPage http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(res http.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
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 |
// Middleware to protect private pages func validate(protectedPage http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(res http.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
1 |
go get github.com/gorilla/context |
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 |
// Middleware to protect private pages func validate(protectedPage http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(res http.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验证的才能访问
1 2 3 4 5 |
func profile(res http.ResponseWriter, req *http.Request){ claims := context.Get(req, "Claims").(*MyCustomClaims) res.Write([]byte(claims.Username)) context.Clear(req) } |
Demo 完成 !!!
下面是完整的代码
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
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 { Username string `json:"username"` jwt.StandardClaims } func setToken(res http.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) } func validate(protectedPage http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(res http.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) }) } func profile(res http.ResponseWriter, req *http.Request){ claims := context.Get(req, "Claims").(*MyCustomClaims) res.Write([]byte(claims.Username)) context.Clear(req) } func homePage(res http.ResponseWriter, req *http.Request){ res.Write([]byte("Home Page")) } func main(){ http.HandleFunc("/profile", validate(profile)) http.HandleFunc("/setToken", setToken) http.HandleFunc("/", homePage) http.ListenAndServe(":8080", nil) } |
参考文献:
https://dinosaurscode.xyz/go/2016/06/17/golang-jwt-authentication/
http://b.aicode.cc/%E5%85%B6%E5%AE%83/2016/03/03/JWT-Json-Web-Token%E7%AE%80%E4%BB%8B(%E7%BF%BB%E8%AF%91).html
有疑问加站长微信联系(非本文作者)