使用JWT進(jìn)行身份校驗(yàn)
但是,還存在一些非常嚴(yán)重的問題月洛,例如婉徘,我們現(xiàn)在的API是可以隨意調(diào)用的,這顯然還不夠完美敬矩,是有問題的
那么我們采用 jwt-go (GoDoc)的方式來簡(jiǎn)單解決這個(gè)問題
下載依賴包
go get -u github.com/dgrijalva/jwt-go
編寫jwt工具包
我們需要編寫一個(gè)jwt的工具包概行,我們?cè)趐kg下的util目錄新建jwt.go,寫入文件內(nèi)容:
package util
import (
"time"
jwt "github.com/dgrijalva/jwt-go"
"gin-blog/pkg/setting"
)
var jwtSecret = []byte(setting.JwtSecret)
type Claims struct {
Username string `json:"username"`
Password string `json:"password"`
jwt.StandardClaims
}
func GenerateToken(username, password string) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(3 * time.Hour)
claims := Claims{
username,
password,
jwt.StandardClaims {
ExpiresAt : expireTime.Unix(),
Issuer : "gin-blog",
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString(jwtSecret)
return token, err
}
func ParseToken(token string) (*Claims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
在這個(gè)工具包弧岳,我們涉及到
-
NewWithClaims(method SigningMethod, claims Claims)
凳忙,method
對(duì)應(yīng)著SigningMethodHMAC struct{}
,其包含SigningMethodHS256
缩筛、SigningMethodHS384
消略、SigningMethodHS512
三種crypto.Hash
方案 -
func (t *Token) SignedString(key interface{})
該方法內(nèi)部生成簽名字符串,再用于獲取完整瞎抛、已簽名的token
-
func (p *Parser) ParseWithClaims
用于解析鑒權(quán)的聲明艺演,方法內(nèi)部主要是具體的解碼和校驗(yàn)的過程,最終返回*Token
-
func (m MapClaims) Valid()
驗(yàn)證基于時(shí)間的聲明exp, iat, nbf
桐臊,注意如果沒有任何聲明在令牌中胎撤,仍然會(huì)被認(rèn)為是有效的。并且對(duì)于時(shí)區(qū)偏差沒有計(jì)算方法
有了jwt工具包断凶,接下來我們要編寫要用于Gin的中間件伤提,我們?cè)趍iddleware下新建jwt目錄,新建jwt.go文件认烁,寫入內(nèi)容:
package jwt
import (
"time"
"net/http"
"github.com/gin-gonic/gin"
"gin-blog/pkg/util"
"gin-blog/pkg/e"
)
func JWT() gin.HandlerFunc {
return func(c *gin.Context) {
var code int
var data interface{}
code = e.SUCCESS
token := c.Query("token")
if token == "" {
code = e.INVALID_PARAMS
} else {
claims, err := util.ParseToken(token)
if err != nil {
code = e.ERROR_AUTH_CHECK_TOKEN_FAIL
} else if time.Now().Unix() > claims.ExpiresAt {
code = e.ERROR_AUTH_CHECK_TOKEN_TIMEOUT
}
}
if code != e.SUCCESS {
c.JSON(http.StatusUnauthorized, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : data,
})
c.Abort()
return
}
c.Next()
}
}
這個(gè)中間件涉及到:
gin使用中間件一般是在引擎Engine初始化的時(shí)候就綁定肿男,也就是說在需要使用中間件才能訪問資源的接口之前干這些事情介汹。比如對(duì)登錄操作放行,對(duì)其他操作進(jìn)行鑒權(quán)舶沛。
中間件里面有錯(cuò)誤如果不想繼續(xù)后續(xù)接口的調(diào)用不能直接return嘹承,而是應(yīng)該調(diào)用c.Abort()方法。
如何獲取Token
那么我們?nèi)绾握{(diào)用它呢如庭,我們還要獲取Token呢叹卷?
1、 我們要新增一個(gè)獲取Token的API
在models下新建auth.go文件坪它,寫入內(nèi)容:
package models
type Auth struct {
ID int `gorm:"primary_key" json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
func CheckAuth(username, password string) bool {
var auth Auth
db.Select("id").Where(Auth{Username : username, Password : password}).First(&auth)
if auth.ID > 0 {
return true
}
return false
}
在routers下的api目錄新建auth.go文件骤竹,寫入內(nèi)容:
package api
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/astaxie/beego/validation"
"gin-blog/pkg/e"
"gin-blog/pkg/util"
"gin-blog/models"
)
type auth struct {
Username string `valid:"Required; MaxSize(50)"`
Password string `valid:"Required; MaxSize(50)"`
}
func GetAuth(c *gin.Context) {
username := c.Query("username")
password := c.Query("password")
valid := validation.Validation{}
a := auth{Username: username, Password: password}
ok, _ := valid.Valid(&a)
data := make(map[string]interface{})
code := e.INVALID_PARAMS
if ok {
isExist := models.CheckAuth(username, password)
if isExist {
token, err := util.GenerateToken(username, password)
if err != nil {
code = e.ERROR_AUTH_TOKEN
} else {
data["token"] = token
code = e.SUCCESS
}
} else {
code = e.ERROR_AUTH
}
} else {
for _, err := range valid.Errors {
log.Println(err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : data,
})
}
我們打開routers目錄下的router.go文件,修改文件內(nèi)容(新增獲取token的方法):
package routers
import (
"github.com/gin-gonic/gin"
"gin-blog/routers/api"
"gin-blog/routers/api/v1"
"gin-blog/pkg/setting"
)
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
gin.SetMode(setting.RunMode)
r.GET("/auth", api.GetAuth)
apiv1 := r.Group("/api/v1")
{
...
}
return r
}
驗(yàn)證Token
訪問 GET http://localhost:8000/auth?username=test&password=test123456
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6InRlc3QxMjM0NTYiLCJleHAiOjE1NTExNzk4MzEsImlzcyI6Imdpbi1ibG9nIn0.bE0agND6A_fyyZz_UeR_YtdBvOrC2pjTPtL47SuAYWI"
},
"msg": "ok"
}
將中間件接入Gin
2往毡、 接下來我們將中間件接入到Gin的訪問流程中
我們打開routers目錄下的router.go文件蒙揣,修改文件內(nèi)容(新增引用包和中間件引用)
package routers
import (
"github.com/gin-gonic/gin"
"gin-blog/routers/api"
"gin-blog/routers/api/v1"
"gin-blog/pkg/setting"
"gin-blog/middleware/jwt"
)
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
gin.SetMode(setting.RunMode)
r.GET("/auth", api.GetAuth)
apiv1 := r.Group("/api/v1")
apiv1.Use(jwt.JWT())
{
...
}
return r
}
當(dāng)前結(jié)構(gòu)目錄
gin-blog/
├── conf
│ └── app.ini
├── main.go
├── middleware
│ └── jwt
│ └── jwt.go
├── models
│ ├── article.go
│ ├── auth.go
│ ├── models.go
│ └── tag.go
├── pkg
│ ├── e
│ │ ├── code.go
│ │ └── msg.go
│ ├── setting
│ │ └── setting.go
│ └── util
│ ├── jwt.go
│ └── pagination.go
├── routers
│ ├── api
│ │ ├── auth.go
│ │ └── v1
│ │ ├── article.go
│ │ └── tag.go
│ └── router.go
├── runtime
驗(yàn)證功能
- 訪問
http://127.0.0.1:8000/api/v1/articles
{
"code": 400,
"data": null,
"msg": "請(qǐng)求參數(shù)錯(cuò)誤"
}
- 訪問
http://127.0.0.1:8000/api/v1/articles?token=23131
{
"code": 20001,
"data": null,
"msg": "Token鑒權(quán)失敗"
}
我們需要訪問http://127.0.0.1:8000/auth?username=test&password=test123456
,得到token
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6InRlc3QxMjM0NTYiLCJleHAiOjE1NTExODA1MjgsImlzcyI6Imdpbi1ibG9nIn0.dcbLkez5QDrgierHmWWYMRBoyLPawSsLew32C_IARz0"
},
"msg": "ok"
}
再用包含token的URL參數(shù)去訪問我們的應(yīng)用API卖擅,
訪問http://127.0.0.1:8000/api/v1/articles?token=eyJhbGci...
鸣奔,檢查接口返回值
{
"code": 200,
"data": {
"lists": [
{
"id": 1,
"created_on": 0,
"modified_on": 0,
"title": "夸父逐日",
"desc": "\b夸父鍥而不舍追求太陽最終失敗的\b\b故事",
"content": "\baaabbbcccdddeee夸父",
"created_by": "test",
"modified_by": "",
"state": 1,
"tag_id gorm:": 4,
"tag": {
"id": 4,
"created_on": 1551077861,
"modified_on": 0,
"name": "百科全書",
"created_by": "test",
"modified_by": "",
"state": 1
}
}
],
"total": 1
},
"msg": "ok"
}
jwt與session方案對(duì)比等參考
九幅圖理解使用JSON Web Token(JWT)設(shè)計(jì)單點(diǎn)登錄系統(tǒng)
https://blog.csdn.net/uniquewonderq/article/details/79720897
使用JWT做微服務(wù)的登錄方案 https://blog.csdn.net/w57685321/article/details/79463837