1.什么是JWT
JWT(JSON Web Token)是一個非常輕巧的規范,這個規范允許我們使用JWT在用戶和服務器之間傳遞安全可靠的信息,
一個JWT由三部分組成,Header頭部、Claims載荷、Signature簽名,
JWT原理類似我們加蓋公章或手寫簽名的的過程,合同上寫了很多條款,不是隨便一張紙隨便寫啥都可以的,必須要一些證明,比如簽名,比如蓋章,JWT就是通過附加簽名,保證傳輸過來的信息是真的,而不是偽造的,
它將用戶信息加密到token里,服務器不保存任何用戶信息,服務器通過使用保存的密鑰驗證token的正確性,只要正確即通過驗證,
2.JWT構成
一個JWT由三部分組成,Header頭部、Claims載荷、Signature簽名,
- Header頭部:頭部,表明類型和加密算法
- Claims載荷:聲明,即載荷(承載的內容)
- Signature簽名:簽名,這一部分是將header和claims進行base64轉碼后,并用header中聲明的加密算法加鹽(secret)后構成,即:
1
2
3
4
|
let tmpstr = base64(header)+base64(claims) let signature = encrypt(tmpstr,secret) //最后三者用"."連接,即: let token = base64(header)+ "." +base64(claims)+ "." +signature |
3.javascript提取JWT字符串荷載信息
JWT里面payload可以包含很多字段,字段越多你的token字符串就越長.
你的HTTP請求通訊的發送的數據就越多,回到之接口響應時間等待稍稍的變長一點點.
一下代碼就是前端javascript從payload獲取登錄的用戶信息.
當然后端middleware也可以直接解析payload獲取用戶信息,減少到數據庫中查詢user表數據.接口速度會更快,數據庫壓力更小.
后端檢查JWT身份驗證時候當然會校驗payload和Signature簽名是否合法.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
let tokenString = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Njc3Nzc5NjIsImp0aSI6IjUiLCJpYXQiOjE1Njc2OTE1NjIsImlzcyI6ImZlbGl4Lm1vam90di5jbiIsImlkIjo1LCJjcmVhdGVkX2F0IjoiMjAxOS0wOS0wNVQxMTo1Njo1OS41NjI1NDcwODYrMDg6MDAiLCJ1cGRhdGVkX2F0IjoiMjAxOS0wOS0wNVQxNjo1ODoyMC41NTYxNjAwOTIrMDg6MDAiLCJ1c2VybmFtZSI6ImVyaWMiLCJuaWNrX25hbWUiOiIiLCJlbWFpbCI6IjEyMzQ1NkBxcS5jb20iLCJtb2JpbGUiOiIiLCJyb2xlX2lkIjo4LCJzdGF0dXMiOjAsImF2YXRhciI6Ii8vdGVjaC5tb2pvdHYuY24vYXNzZXRzL2ltYWdlL2F2YXRhcl8zLnBuZyIsInJlbWFyayI6IiIsImZyaWVuZF9pZHMiOm51bGwsImthcm1hIjowLCJjb21tZW50X2lkcyI6bnVsbH0.tGjukvuE9JVjzDa42iGfh_5jIembO5YZBZDqLnaG6KQ' function parseTokenGetUser(jwtTokenString) { let base64Url = jwtTokenString.split( '.' )[1]; let base64 = base64Url.replace(/-/g, '+' ).replace(/_/g, '/' ); let jsonPayload = decodeURIComponent(atob(base64).split( '' ).map( function (c) { return '%' + ( '00' + c.charCodeAt(0).toString(16)).slice(-2); }).join( '' )); let user = JSON.parse(jsonPayload); localStorage.setItem( "token" , jwtTokenString); localStorage.setItem( "expire_ts" , user.exp); localStorage.setItem( "user" , jsonPayload); return user; } parseTokenGetUser(tokenString) |
復制上面javascript代碼到瀏覽器console中執行就可以解析出用戶信息了! 當然你要可以使用在線工具來解析jwt token的payload荷載
JWT在線解析工具
4. go語言Gin框架實現JWT用戶認證
接下來我將使用最受歡迎的gin-gonic/gin 和 dgrijalva/jwt-go
這兩個package來演示怎么使用JWT身份認證.
4.1 登錄接口
4.1.1 登錄接口路由(login-route)
https://github.com/libragen/felix/blob/master/ssh2ws/ssh2ws.go
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
|
r := gin.New() r.MaxMultipartMemory = 32 << 20 //sever static file in http's root path binStaticMiddleware, err := felixbin.NewGinStaticBinMiddleware("/") if err != nil { return err } //支持跨域 mwCORS := cors.New(cors.Config{ AllowOrigins: []string{"*"}, AllowMethods: []string{"PUT", "PATCH", "POST", "GET", "DELETE"}, AllowHeaders: []string{"Origin", "Authorization", "Content-Type"}, ExposeHeaders: []string{"Content-Type"}, AllowCredentials: true, AllowOriginFunc: func(origin string) bool { return true }, MaxAge: 2400 * time.Hour, }) r.Use(binStaticMiddleware, mwCORS) { r.POST("comment-login", internal.LoginCommenter) //評論用戶登陸 r.POST("comment-register", internal.RegisterCommenter) //評論用戶注冊 } api := r.Group("api") api.POST("admin-login", internal.LoginAdmin) //管理后臺登陸 |
internal.LoginCommenter
和 internal.LoginAdmin
這兩個方法是一樣的,
只需要關注其中一個就可以了,我們就關注internal.LoginCommenter
4.1.2 登錄login handler
編寫登錄的handler
https://github.com/libragen/felix/blob/master/ssh2ws/internal/h_login.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func LoginCommenter(c *gin.Context) { var mdl model.User err := c.ShouldBind(&mdl) if handleError(c, err) { return } //獲取ip ip := c.ClientIP() //roleId 8 是評論系統的用戶 data, err := mdl.Login(ip, 8) if handleError(c, err) { return } jsonData(c, data) } |
其中最關鍵的是mdl.Login(ip, 8)
這個函數
https://github.com/libragen/felix/blob/master/model/m_users.go
- 1.數據庫查詢用戶
- 2.校驗用戶role_id
- 3.比對密碼
- 4.防止密碼泄露(清空struct的屬性)
- 5.生成JWT-string
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
|
//Login func (m *User) Login(ip string, roleId uint) (string, error) { m.Id = 0 if m.Password == "" { return "" , errors.New( "password is required" ) } inputPassword := m.Password //獲取登錄的用戶 err := db.Where( "username = ? or email = ?" , m.Username, m.Username).First(&m).Error if err != nil { return "" , err } //校驗用戶角色 if (m.RoleId & roleId) != roleId { return "" , fmt.Errorf( "not role of %d" , roleId) } //驗證密碼 //password is set to bcrypt check if err := bcrypt.CompareHashAndPassword([]byte(m.HashedPassword), []byte(inputPassword)); err != nil { return "" , err } //防止密碼泄露 m.Password = "" //生成jwt-string return jwtGenerateToken(m, time.Hour*24*365) } |
4.1.2 生成JWT-string(核心代碼)
1.自定義payload結構體,不建議直接使用 dgrijalva/jwt-go jwt.StandardClaims
結構體.因為他的payload包含的用戶信息太少.
2.實現 type Claims interface
的 Valid() error
方法,自定義校驗內容
3.生成JWT-string jwtGenerateToken(m *User,d time.Duration) (string, error)
https://github.com/libragen/felix/blob/master/model/m_jwt.go
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
|
package model import ( "errors" "fmt" "time" "github.com/dgrijalva/jwt-go" "github.com/sirupsen/logrus" ) var AppSecret = ""//viper.GetString會設置這個值(32byte長度) var AppIss = "github.com/libragen/felix"//這個值會被viper.GetString重寫 //自定義payload結構體,不建議直接使用 dgrijalva/jwt-go `jwt.StandardClaims`結構體.因為他的payload包含的用戶信息太少. type userStdClaims struct { jwt.StandardClaims *User } //實現 `type Claims interface` 的 `Valid() error` 方法,自定義校驗內容 func (c userStdClaims) Valid() (err error) { if c.VerifyExpiresAt(time.Now().Unix(), true) == false { return errors.New("token is expired") } if !c.VerifyIssuer(AppIss, true) { return errors.New("token's issuer is wrong") } if c.User.Id < 1 { return errors.New("invalid user in jwt") } return } func jwtGenerateToken(m *User,d time.Duration) (string, error) { m.Password = "" expireTime := time.Now().Add(d) stdClaims := jwt.StandardClaims{ ExpiresAt: expireTime.Unix(), IssuedAt: time.Now().Unix(), Id: fmt.Sprintf("%d", m.Id), Issuer: AppIss, } uClaims := userStdClaims{ StandardClaims: stdClaims, User: m, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, uClaims) // Sign and get the complete encoded token as a string using the secret tokenString, err := token.SignedString([]byte(AppSecret)) if err != nil { logrus.WithError(err).Fatal("config is wrong, can not generate jwt") } return tokenString, err } //JwtParseUser 解析payload的內容,得到用戶信息 //gin-middleware 會使用這個方法 func JwtParseUser(tokenString string) (*User, error) { if tokenString == "" { return nil, errors.New("no token is found in Authorization Bearer") } claims := userStdClaims{} _, err := jwt.ParseWithClaims(tokenString, &claims, 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(AppSecret), nil }) if err != nil { return nil, err } return claims.User, err } |
4.2 JWT中間件(middleware)
1.從url-query的_t
獲取JWT-string或者從請求頭 Authorization中獲取JWT-string
2.model.JwtParseUser(token)
解析JWT-string獲取User結構體(減少中間件查詢數據庫的操作和時間)
3.設置用戶信息到gin.Context
其他的handler通過gin.Context.Get(contextKeyUserObj),在進行用戶Type Assert得到model.User 結構體.
4.使用了jwt-middle之后的handle從gin.Context中獲取用戶信息
https://github.com/libragen/felix/blob/master/ssh2ws/internal/mw_jwt.go
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
|
package internal import ( "net/http" "strings" "github.com/libragen/felix/model" "github.com/gin-gonic/gin" ) const contextKeyUserObj = "authedUserObj" const bearerLength = len("Bearer ") func ctxTokenToUser(c *gin.Context, roleId uint) { token, ok := c.GetQuery("_t") if !ok { hToken := c.GetHeader("Authorization") if len(hToken) < bearerLength { c.AbortWithStatusJSON(http.StatusPreconditionFailed, gin.H{"msg": "header Authorization has not Bearer token"}) return } token = strings.TrimSpace(hToken[bearerLength:]) } usr, err := model.JwtParseUser(token) if err != nil { c.AbortWithStatusJSON(http.StatusPreconditionFailed, gin.H{"msg": err.Error()}) return } if (usr.RoleId & roleId) != roleId { c.AbortWithStatusJSON(http.StatusPreconditionFailed, gin.H{"msg": "roleId 沒有權限"}) return } //store the user Model in the context c.Set(contextKeyUserObj, *usr) c.Next() // after request } func MwUserAdmin(c *gin.Context) { ctxTokenToUser(c, 2) } func MwUserComment(c *gin.Context) { ctxTokenToUser(c, 8) } |
使用了jwt-middle之后的handle從gin.Context中獲取用戶信息,
https://github.com/libragen/felix/blob/master/ssh2ws/internal/helper.go
1
2
3
4
5
6
7
8
9
10
11
|
func mWuserId(c *gin.Context) (uint, error) { v,exist := c.Get(contextKeyUserObj) if !exist { return 0,errors.New(contextKeyUserObj + " not exist") } user, ok := v.(model.User) if ok { return user.Id, nil } return 0,errors.New("can't convert to user struct") } |
4.2 使用JWT中間件
一下代碼有兩個JWT中間件的用法
-
internal.MwUserAdmin
管理后臺用戶中間件 -
internal.MwUserCommenter
評論用戶中間件
https://github.com/libragen/felix/blob/master/ssh2ws/ssh2ws.go
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
package ssh2ws import ( "time" "github.com/libragen/felix/felixbin" "github.com/libragen/felix/model" "github.com/libragen/felix/ssh2ws/internal" "github.com/libragen/felix/wslog" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func RunSsh2ws(bindAddress, user, password, secret string, expire time.Duration, verbose bool) error { err := model.CreateGodUser(user, password) if err != nil { return err } //config jwt variables model.AppSecret = secret model.ExpireTime = expire model.AppIss = "felix.mojotv.cn" if !verbose { gin.SetMode(gin.ReleaseMode) } r := gin.New() r.MaxMultipartMemory = 32 << 20 //sever static file in http's root path binStaticMiddleware, err := felixbin.NewGinStaticBinMiddleware("/") if err != nil { return err } mwCORS := cors.New(cors.Config{ AllowOrigins: []string{"*"}, AllowMethods: []string{"PUT", "PATCH", "POST", "GET", "DELETE"}, AllowHeaders: []string{"Origin", "Authorization", "Content-Type"}, ExposeHeaders: []string{"Content-Type"}, AllowCredentials: true, AllowOriginFunc: func(origin string) bool { return true }, MaxAge: 2400 * time.Hour, }) r.Use(binStaticMiddleware, mwCORS) { r.POST("comment-login", internal.LoginCommenter) //評論用戶登陸 r.POST("comment-register", internal.RegisterCommenter) //評論用戶注冊 } api := r.Group("api") api.POST("admin-login", internal.LoginAdmin) //管理后臺登陸 api.GET("meta", internal.Meta) //terminal log hub := wslog.NewHub() go hub.Run() { //websocket r.GET("ws/hook", internal.MwUserAdmin, internal.Wslog(hub)) r.GET("ws/ssh/:id", internal.MwUserAdmin, internal.WsSsh) } //給外部調用 { api.POST("wslog/hook-api", internal.JwtMiddlewareWslog, internal.WsLogHookApi(hub)) api.GET("wslog/hook", internal.MwUserAdmin, internal.WslogHookAll) api.POST("wslog/hook", internal.MwUserAdmin, internal.WslogHookCreate) api.PATCH("wslog/hook", internal.MwUserAdmin, internal.WslogHookUpdate) api.DELETE("wslog/hook/:id", internal.MwUserAdmin, internal.WslogHookDelete) api.GET("wslog/msg", internal.MwUserAdmin, internal.WslogMsgAll) api.POST("wslog/msg-rm", internal.MwUserAdmin, internal.WslogMsgDelete) } //評論 { api.GET("comment", internal.CommentAll) api.GET("comment/:id/:action", internal.MwUserComment, internal.CommentAction) api.POST("comment", internal.MwUserComment, internal.CommentCreate) api.DELETE("comment/:id", internal.MwUserAdmin, internal.CommentDelete) } { api.GET("hacknews",internal.MwUserAdmin, internal.HackNewAll) api.PATCH("hacknews", internal.HackNewUpdate) api.POST("hacknews-rm", internal.HackNewRm) } authG := api.Use(internal.MwUserAdmin) { //create wslog hook authG.GET("ssh", internal.SshAll) authG.POST("ssh", internal.SshCreate) authG.GET("ssh/:id", internal.SshOne) authG.PATCH("ssh", internal.SshUpdate) authG.DELETE("ssh/:id", internal.SshDelete) authG.GET("sftp/:id", internal.SftpLs) authG.GET("sftp/:id/dl", internal.SftpDl) authG.GET("sftp/:id/cat", internal.SftpCat) authG.GET("sftp/:id/rm", internal.SftpRm) authG.GET("sftp/:id/rename", internal.SftpRename) authG.GET("sftp/:id/mkdir", internal.SftpMkdir) authG.POST("sftp/:id/up", internal.SftpUp) authG.POST("ginbro/gen", internal.GinbroGen) authG.POST("ginbro/db", internal.GinbroDb) authG.GET("ginbro/dl", internal.GinbroDownload) authG.GET("ssh-log", internal.SshLogAll) authG.DELETE("ssh-log/:id", internal.SshLogDelete) authG.PATCH("ssh-log", internal.SshLogUpdate) authG.GET("user", internal.UserAll) authG.POST("user", internal.RegisterCommenter) //api.GET("user/:id", internal.SshAll) authG.DELETE("user/:id", internal.UserDelete) authG.PATCH("user", internal.UserUpdate) } if err := r.Run(bindAddress); err != nil { return err } return nil } |
5. Cookie-Session VS JWT
JWT和session有所不同,session需要在服務器端生成,服務器保存session,只返回給客戶端sessionid,客戶端下次請求時帶上sessionid即可,因為session是儲存在服務器中,有多臺服務器時會出現一些麻煩,需要同步多臺主機的信息,不然會出現在請求A服務器時能獲取信息,但是請求B服務器身份信息無法通過,JWT能很好的解決這個問題,服務器端不用保存jwt,只需要保存加密用的secret,在用戶登錄時將jwt加密生成并發送給客戶端,由客戶端存儲,以后客戶端的請求帶上,由服務器解析jwt并驗證,這樣服務器不用浪費空間去存儲登錄信息,不用浪費時間去做同步,
5.1 什么是cookie
基于cookie的身份驗證是有狀態的,這意味著驗證的記錄或者會話(session)必須同時保存在服務器端和客戶端,服務器端需要跟蹤記錄session并存至數據庫,
同時前端需要在cookie中保存一個sessionID,作為session的唯一標識符,可看做是session的“身份證”,
cookie,簡而言之就是在客戶端(瀏覽器等)保存一些用戶操作的歷史信息(當然包括登錄信息),并在用戶再次訪問該站點時瀏覽器通過HTTP協議將本地cookie內容發送給服務器,從而完成驗證,或繼續上一步操作,
5.2 什么是session
session,會話,簡而言之就是在服務器上保存用戶操作的歷史信息,在用戶登錄后,服務器存儲用戶會話的相關信息,并為客戶端指定一個訪問憑證,如果有客戶端憑此憑證發出請求,則在服務端存儲的信息中,取出用戶相關登錄信息,
并且使用服務端返回的憑證常存儲于Cookie中,也可以改寫URL,將id放在url中,這個訪問憑證一般來說就是SessionID,
5.3 cookie-session身份驗證機制的流程
session和cookie的目的相同,都是為了克服http協議無狀態的缺陷,但完成的方法不同,
session可以通過cookie來完成,在客戶端保存session id,而將用戶的其他會話消息保存在服務端的session對象中,與此相對的,cookie需要將所有信息都保存在客戶端,
因此cookie存在著一定的安全隱患,例如本地cookie中保存的用戶名密碼被破譯,或cookie被其他網站收集(例如:1. appA主動設置域B cookie,讓域B cookie獲取;2. XSS,在appA上通過javascript獲取document.cookie,并傳遞給自己的appB),
- 用戶輸入登錄信息
- 服務器驗證登錄信息是否正確,如果正確就創建一個session,并把session存入數據庫
- 服務器端會向客戶端返回帶有sessionID的cookie
- 在接下來的請求中,服務器將把sessionID與數據庫中的相匹配,如果有效則處理該請求
- 如果用戶登出app,session會在客戶端和服務器端都被銷毀
5.4 Cookie-session 和 JWT 使用場景
后端渲染HTML頁面建議使用Cookie-session認證
后按渲染頁面可以很方便的寫入/清除cookie到瀏覽器,權限控制非常方便.很少需要要考慮跨域AJAX認證的問題.
App,web單頁面應用,APIs建議使用JWT認證
App、web APIs等的興起,基于token的身份驗證開始流行,
當我們談到利用token進行認證,我們一般說的就是利用JSON Web Tokens(JWTs)進行認證,雖然有不同的方式來實現token,
事實上,JWTs 已成為標準,因此在本文中將互換token與JWTs,
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流, 謝謝大家對mojotv.cn的支持.喜歡這個網站麻煩幫忙添加到收藏夾,添加我的微信好友: felixarebest 微博賬號: MojoTech 向我提問.
原文地址:Go進階24:Go-jwt RESTful身份認證教程
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://segmentfault.com/a/1190000020329813