1.什么是JWT
JWT(JSON Web Token)是一個(gè)非常輕巧的規(guī)范,這個(gè)規(guī)范允許我們使用JWT在用戶和服務(wù)器之間傳遞安全可靠的信息,
一個(gè)JWT由三部分組成,Header頭部、Claims載荷、Signature簽名,
JWT原理類似我們加蓋公章或手寫簽名的的過程,合同上寫了很多條款,不是隨便一張紙隨便寫啥都可以的,必須要一些證明,比如簽名,比如蓋章,JWT就是通過附加簽名,保證傳輸過來的信息是真的,而不是偽造的,
它將用戶信息加密到token里,服務(wù)器不保存任何用戶信息,服務(wù)器通過使用保存的密鑰驗(yàn)證token的正確性,只要正確即通過驗(yàn)證,
2.JWT構(gòu)成
一個(gè)JWT由三部分組成,Header頭部、Claims載荷、Signature簽名,
- Header頭部:頭部,表明類型和加密算法
- Claims載荷:聲明,即載荷(承載的內(nèi)容)
- Signature簽名:簽名,這一部分是將header和claims進(jìn)行base64轉(zhuǎn)碼后,并用header中聲明的加密算法加鹽(secret)后構(gòu)成,即:
let tmpstr = base64(header)+base64(claims)
let signature = encrypt(tmpstr,secret)
//最后三者用"."連接,即:
let token = base64(header)+"."+base64(claims)+"."+signature
3.javascript提取JWT字符串荷載信息
JWT里面payload可以包含很多字段,字段越多你的token字符串就越長(zhǎng).
你的HTTP請(qǐng)求通訊的發(fā)送的數(shù)據(jù)就越多,回到之接口響應(yīng)時(shí)間等待稍稍的變長(zhǎng)一點(diǎn)點(diǎn).
一下代碼就是前端javascript從payload獲取登錄的用戶信息.
當(dāng)然后端middleware也可以直接解析payload獲取用戶信息,減少到數(shù)據(jù)庫(kù)中查詢user表數(shù)據(jù).接口速度會(huì)更快,數(shù)據(jù)庫(kù)壓力更小.
后端檢查JWT身份驗(yàn)證時(shí)候當(dāng)然會(huì)校驗(yàn)payload和Signature簽名是否合法.
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)
復(fù)制上面javascript代碼到瀏覽器console中執(zhí)行就可以解析出用戶信息了! 當(dāng)然你要可以使用在線工具來解析jwt token的payload荷載
JWT在線解析工具
4. go語(yǔ)言Gin框架實(shí)現(xiàn)JWT用戶認(rèn)證
接下來我將使用最受歡迎的gin-gonic/gin 和 dgrijalva/jwt-go
這兩個(gè)package來演示怎么使用JWT身份認(rèn)證.
4.1 登錄接口
4.1.1 登錄接口路由(login-route)
https://github.com/libragen/felix/blob/master/ssh2ws/ssh2ws.go
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) //評(píng)論用戶登陸
r.POST("comment-register", internal.RegisterCommenter) //評(píng)論用戶注冊(cè)
}
api := r.Group("api")
api.POST("admin-login", internal.LoginAdmin) //管理后臺(tái)登陸
internal.LoginCommenter
和 internal.LoginAdmin
這兩個(gè)方法是一樣的,
只需要關(guān)注其中一個(gè)就可以了,我們就關(guān)注internal.LoginCommenter
4.1.2 登錄login handler
編寫登錄的handler
https://github.com/libragen/felix/blob/master/ssh2ws/internal/h_login.go
func LoginCommenter(c *gin.Context) {
var mdl model.User
err := c.ShouldBind(mdl)
if handleError(c, err) {
return
}
//獲取ip
ip := c.ClientIP()
//roleId 8 是評(píng)論系統(tǒng)的用戶
data, err := mdl.Login(ip, 8)
if handleError(c, err) {
return
}
jsonData(c, data)
}
其中最關(guān)鍵的是mdl.Login(ip, 8)
這個(gè)函數(shù)
https://github.com/libragen/felix/blob/master/model/m_users.go
- 1.數(shù)據(jù)庫(kù)查詢用戶
- 2.校驗(yàn)用戶role_id
- 3.比對(duì)密碼
- 4.防止密碼泄露(清空struct的屬性)
- 5.生成JWT-string
//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
}
//校驗(yàn)用戶角色
if (m.RoleId roleId) != roleId {
return "", fmt.Errorf("not role of %d", roleId)
}
//驗(yàn)證密碼
//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結(jié)構(gòu)體,不建議直接使用 dgrijalva/jwt-go jwt.StandardClaims
結(jié)構(gòu)體.因?yàn)樗膒ayload包含的用戶信息太少.
2.實(shí)現(xiàn) type Claims interface
的 Valid() error
方法,自定義校驗(yàn)內(nèi)容
3.生成JWT-string jwtGenerateToken(m *User,d time.Duration) (string, error)
https://github.com/libragen/felix/blob/master/model/m_jwt.go
package model
import (
"errors"
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/sirupsen/logrus"
)
var AppSecret = ""http://viper.GetString會(huì)設(shè)置這個(gè)值(32byte長(zhǎng)度)
var AppIss = "github.com/libragen/felix"http://這個(gè)值會(huì)被viper.GetString重寫
//自定義payload結(jié)構(gòu)體,不建議直接使用 dgrijalva/jwt-go `jwt.StandardClaims`結(jié)構(gòu)體.因?yàn)樗膒ayload包含的用戶信息太少.
type userStdClaims struct {
jwt.StandardClaims
*User
}
//實(shí)現(xiàn) `type Claims interface` 的 `Valid() error` 方法,自定義校驗(yàn)內(nèi)容
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的內(nèi)容,得到用戶信息
//gin-middleware 會(huì)使用這個(gè)方法
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或者從請(qǐng)求頭 Authorization中獲取JWT-string
2.model.JwtParseUser(token)
解析JWT-string獲取User結(jié)構(gòu)體(減少中間件查詢數(shù)據(jù)庫(kù)的操作和時(shí)間)
3.設(shè)置用戶信息到gin.Context
其他的handler通過gin.Context.Get(contextKeyUserObj),在進(jìn)行用戶Type Assert得到model.User 結(jié)構(gòu)體.
4.使用了jwt-middle之后的handle從gin.Context中獲取用戶信息
https://github.com/libragen/felix/blob/master/ssh2ws/internal/mw_jwt.go
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 沒有權(quán)限"})
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
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中間件
一下代碼有兩個(gè)JWT中間件的用法
internal.MwUserAdmin
管理后臺(tái)用戶中間件
internal.MwUserCommenter
評(píng)論用戶中間件
https://github.com/libragen/felix/blob/master/ssh2ws/ssh2ws.go
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) //評(píng)論用戶登陸
r.POST("comment-register", internal.RegisterCommenter) //評(píng)論用戶注冊(cè)
}
api := r.Group("api")
api.POST("admin-login", internal.LoginAdmin) //管理后臺(tái)登陸
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)
}
//給外部調(diào)用
{
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)
}
//評(píng)論
{
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需要在服務(wù)器端生成,服務(wù)器保存session,只返回給客戶端sessionid,客戶端下次請(qǐng)求時(shí)帶上sessionid即可,因?yàn)閟ession是儲(chǔ)存在服務(wù)器中,有多臺(tái)服務(wù)器時(shí)會(huì)出現(xiàn)一些麻煩,需要同步多臺(tái)主機(jī)的信息,不然會(huì)出現(xiàn)在請(qǐng)求A服務(wù)器時(shí)能獲取信息,但是請(qǐng)求B服務(wù)器身份信息無(wú)法通過,JWT能很好的解決這個(gè)問題,服務(wù)器端不用保存jwt,只需要保存加密用的secret,在用戶登錄時(shí)將jwt加密生成并發(fā)送給客戶端,由客戶端存儲(chǔ),以后客戶端的請(qǐng)求帶上,由服務(wù)器解析jwt并驗(yàn)證,這樣服務(wù)器不用浪費(fèi)空間去存儲(chǔ)登錄信息,不用浪費(fèi)時(shí)間去做同步,
5.1 什么是cookie
基于cookie的身份驗(yàn)證是有狀態(tài)的,這意味著驗(yàn)證的記錄或者會(huì)話(session)必須同時(shí)保存在服務(wù)器端和客戶端,服務(wù)器端需要跟蹤記錄session并存至數(shù)據(jù)庫(kù),
同時(shí)前端需要在cookie中保存一個(gè)sessionID,作為session的唯一標(biāo)識(shí)符,可看做是session的“身份證”,
cookie,簡(jiǎn)而言之就是在客戶端(瀏覽器等)保存一些用戶操作的歷史信息(當(dāng)然包括登錄信息),并在用戶再次訪問該站點(diǎn)時(shí)瀏覽器通過HTTP協(xié)議將本地cookie內(nèi)容發(fā)送給服務(wù)器,從而完成驗(yàn)證,或繼續(xù)上一步操作,
5.2 什么是session
session,會(huì)話,簡(jiǎn)而言之就是在服務(wù)器上保存用戶操作的歷史信息,在用戶登錄后,服務(wù)器存儲(chǔ)用戶會(huì)話的相關(guān)信息,并為客戶端指定一個(gè)訪問憑證,如果有客戶端憑此憑證發(fā)出請(qǐng)求,則在服務(wù)端存儲(chǔ)的信息中,取出用戶相關(guān)登錄信息,
并且使用服務(wù)端返回的憑證常存儲(chǔ)于Cookie中,也可以改寫URL,將id放在url中,這個(gè)訪問憑證一般來說就是SessionID,
5.3 cookie-session身份驗(yàn)證機(jī)制的流程
session和cookie的目的相同,都是為了克服http協(xié)議無(wú)狀態(tài)的缺陷,但完成的方法不同,
session可以通過cookie來完成,在客戶端保存session id,而將用戶的其他會(huì)話消息保存在服務(wù)端的session對(duì)象中,與此相對(duì)的,cookie需要將所有信息都保存在客戶端,
因此cookie存在著一定的安全隱患,例如本地cookie中保存的用戶名密碼被破譯,或cookie被其他網(wǎng)站收集(例如:1. appA主動(dòng)設(shè)置域B cookie,讓域B cookie獲?。?. XSS,在appA上通過javascript獲取document.cookie,并傳遞給自己的appB),
- 用戶輸入登錄信息
- 服務(wù)器驗(yàn)證登錄信息是否正確,如果正確就創(chuàng)建一個(gè)session,并把session存入數(shù)據(jù)庫(kù)
- 服務(wù)器端會(huì)向客戶端返回帶有sessionID的cookie
- 在接下來的請(qǐng)求中,服務(wù)器將把sessionID與數(shù)據(jù)庫(kù)中的相匹配,如果有效則處理該請(qǐng)求
- 如果用戶登出app,session會(huì)在客戶端和服務(wù)器端都被銷毀
5.4 Cookie-session 和 JWT 使用場(chǎng)景
后端渲染HTML頁(yè)面建議使用Cookie-session認(rèn)證
后按渲染頁(yè)面可以很方便的寫入/清除cookie到瀏覽器,權(quán)限控制非常方便.很少需要要考慮跨域AJAX認(rèn)證的問題.
App,web單頁(yè)面應(yīng)用,APIs建議使用JWT認(rèn)證
App、web APIs等的興起,基于token的身份驗(yàn)證開始流行,
當(dāng)我們談到利用token進(jìn)行認(rèn)證,我們一般說的就是利用JSON Web Tokens(JWTs)進(jìn)行認(rèn)證,雖然有不同的方式來實(shí)現(xiàn)token,
事實(shí)上,JWTs 已成為標(biāo)準(zhǔn),因此在本文中將互換token與JWTs,
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流, 謝謝大家對(duì)mojotv.cn的支持.喜歡這個(gè)網(wǎng)站麻煩幫忙添加到收藏夾,添加我的微信好友: felixarebest 微博賬號(hào): MojoTech 向我提問.
原文地址:Go進(jìn)階24:Go-jwt RESTful身份認(rèn)證教程
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:- go-zero 應(yīng)對(duì)海量定時(shí)/延遲任務(wù)的技巧
- 詳解Django配置JWT認(rèn)證方式
- Django JWT Token RestfulAPI用戶認(rèn)證詳解
- 利用go-zero在Go中快速實(shí)現(xiàn)JWT認(rèn)證的步驟詳解