前言
哈嘍,大家好,我是asong,這是我的第八篇原創文章。聽說你們還不會jwt、swagger,所以我帶來一個入門級別的小項目。實現用戶登陸、修改密碼的操作。使用GIN(后臺回復Golang夢工廠
:gin,可獲取2020GIN中文文檔)作為web框架,使用jwt進行身份校驗,使用swagger生成接口文檔。代碼已上傳個人github:https://github.com/asong2020/Golang_Dream/tree/master/Gin/gin_jwt_swagger。有需要的自行下載,配有詳細使用文檔。
原文鏈接:原文傳送門
1. jwt
1.1 簡介
jwt全稱 Json web token,是為了在網絡應用環境間傳遞聲明而執行的一種基于JSON的開放標準。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便于從資源服務器獲取資源,也可以增加一些額外的其他業務邏輯所必須的聲明信息,該token也可直接被用于認證,也可以被加密。學習jwt,我們可以從官網文檔入手,jwt官網傳送門。
1.2 json web 令牌結構
JSON Web令牌由三部分組成,這些部分由.
分隔,分別是
- Header
- Payload
- Signature
一個JWT表示如示例:xxxxx.yyyyy.zzzzz
1.2.1 Header
Header通常由兩部分組成:令牌的類型和所使用的簽名算法,例如HMAC、SHA256或者RSA。
{
"alg": "HS256",
"typ": "JWT"
}
此json是由Base64Url編碼形成JWT的第一部分。
1.2.2 Payload
令牌的第二部分是有效載荷。用于聲明,通常存儲一些用戶ID之類的索引數據,也可以放一些其他有用的信息,注意:不要存儲機密數據。JWT標準定義了一些基本字段:
-
iss
:該JWT的簽發者 -
sub
:該JWT所面向的用戶 -
aud
:接收該JWT的一方 -
exp
(expires):過期時間 -
iat
:簽發時間
除了定義這幾個標準字段外,我們可以定義一些我們在業務處理中需要用到的字段,可以有用戶的id、名字等。來個例子看一看吧:
{
"iss": "asong",
"iat": 6666666666,
"exp": 6666666666,
"aud": "user",
"sub": "all",
"user_id": "6666666666666666666",
"username": "asong"
}
上面的user_id、username都是我們定義的字段。對此負載進行Base64Url編碼,形成JSON Web令牌的第二部分。
1.2.3 Signature
簽名其實是對JWT的Header和Payload整合的一個簽名驗證。我們需要將Header和Payload鏈接起來,然后使用一個key用HMAC SHA256進行加密,創建一個簽名,像下面這樣。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
簽名的作用用于驗證消息在此過程中沒有更改,并且對于使用私鑰進行簽名的令牌,它還可以驗證JWT的發件人是誰。
最終將這三部分放在一起,由.
進行分隔,示例如下:
[圖片上傳失敗...(image-b781ec-1596983242695)]
1.3 什么時候使用JWT
- Authorization(授權):用戶請求的token中包含了該令牌允許的路由,服務和資源。單點登錄就是其中廣泛使用JWT的一個特性。
- Information Exchange(信息交換):對于安全的在各方之間傳輸信息而言,JSON Web Tokens無疑是一種很好的方式。因為JWTs可以被簽名,例如,用公鑰/私鑰對,可以確定發送人身份。并且·簽名是使用頭和有效負載計算的,還可以驗證內容有沒有被篡改。
JWT工作可以用如下圖表示:
根據上圖所示,我們可以看到整個過程分為兩個階段,第一個階段,客戶端向服務器獲取token,第二階段,客戶端帶著該token去請求相關的資源。服務端通常根據指定的規則進行token的生成。在認證的時候,當用戶用他們的憑證成功登錄以后,一個JSON WebToken將會被返回。這是這個token就是用戶憑證了,我們必須小心防止出現安全問題。一般我們保存令牌的時候不應該超過你所需要他的時間。無論何時用戶想要訪問受保護的路由或者資源的時候,用戶代理(通常是瀏覽器)都應該帶上JWT,典型的,通常放在Authorization header中,用Bearer schema: Authorization: Bearer <token>
。服務器上的受保護的路由將會檢查Authorization header中的JWT是否有效,如果有效,則用戶可以訪問受保護的資源。如果JWT包含足夠多的必需的數據,那么就可以減少對某些操作的數據庫查詢的需要,盡管可能并不總是如此。如果token是在授權頭(Authorization header)中發送的,那么跨源資源共享(CORS)將不會成為問題,因為它不使用cookie.
- 客戶端向授權接口請求授權
- 服務端授權后返回一個access token給客戶端
- 客戶端使用access token訪問受保護的資源
好啦,JWT的基本原理介紹完畢,你學了嘛?沒學會不要緊,我們還有代碼呢。
1.3 代碼實踐
- Web框架:Gin
- 第三方庫:github.com/dgrijalva/jwt-go
代碼地址:https://github.com/asong2020/Golang_Dream/tree/master/Gin/gin_jwt_swagger
在這再推薦一個別人寫好的JWT包,直接使用也可以:https://github.com/appleboy/gin-jwt
1.3.1 定義相關參數
- 定義claims中信息,示例定義如下:
type UserClaims struct {
Username string
jwt.StandardClaims
}
- 定義secret
jwt:
signkey: 'asong'
- 定義過期時間
redis:
addr: 127.0.0.1:6379
db: 1
password: ''
poolsize: 100
cache:
tokenexpired: 7200 # expired time 2*60*60
- 創建一個JWT對象
type JWT struct {
SigningKey []byte
}
func NewJWT() *JWT {
return &JWT{
[]byte(global.AsongServer.Jwt.Signkey),
}
}
1.3.2 生成JWT
func (j *JWT)GenerateToken(claims request.UserClaims) (string,error){
token := jwt.NewWithClaims(jwt.SigningMethodHS256,claims)
return token.SignedString(j.SigningKey)
}
1.3.3 解析Token
func (j *JWT)ParseToken(t string) (*request.UserClaims,error) {
token, err := jwt.ParseWithClaims(t,&request.UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey,nil
})
if err != nil{
if v, ok := err.(*jwt.ValidationError); ok {
if v.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, errors.New("That's not even a token")
} else if v.Errors&jwt.ValidationErrorExpired != 0 {
// Token is expired
return nil, errors.New("Token is expired")
} else if v.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, errors.New("Token not active yet")
} else {
return nil, errors.New("Couldn't handle this token:")
}
}
}
if token != nil {
if claims, ok := token.Claims.(*request.UserClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("Couldn't handle this token:")
} else {
return nil, errors.New("Couldn't handle this token:")
}
}
1.3.4 更新Token
func (j *JWT) RefreshToken(t string) (string, error) {
jwt.TimeFunc = func() time.Time {
return time.Unix(0, 0)
}
token, err := jwt.ParseWithClaims(t, &request.UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(*request.UserClaims); ok && token.Valid {
jwt.TimeFunc = time.Now
claims.StandardClaims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix()
return j.GenerateToken(*claims)
}
return "", errors.New("Couldn't handle this token:")
}
1.3.5 編寫中間件
func Auth() gin.HandlerFunc{
return func(c *gin.Context) {
token := c.Request.Header.Get("token")
if token == ""{
response.Result(response.ERROR,gin.H{
"reload": true,
},"非法訪問",c)
c.Abort()
return
}
j := NewJWT()
claims, err := j.ParseToken(token)
if err != nil{
response.Result(response.ERROR, gin.H{
"reload": true,
}, err.Error(), c)
c.Abort()
return
}
c.Set("claims",claims)
c.Next()
}
}
1.3.6 在gin框架中使用
- 路由注冊時,使用中間,作為用戶登錄驗證
func RouteUserInit(Router *gin.RouterGroup) {
UserRouter := Router.Group("user").Use(middleware.Auth())
{
UserRouter.PUT("setPassword",setPassword)
}
}
- 用戶通過接口獲取Token之后,后續就會攜帶著Token再來請求我們的其他接口,這個時候就需要對這些請求的Token進行校驗操作了
func login(c *gin.Context) {
var req request.LoginRequest
_ = c.ShouldBindJSON(&req)
if req.Username == "" || req.Password == ""{
response.FailWithMessage("參數錯誤",c)
return
}
user := &model.User{
Username: req.Username,
Password: req.Password,
}
u ,err := service.Login(user)
if err != nil{
response.FailWithMessage(err.Error(),c)
return
}
service.GenerateTokenForUser(c,u)
}
//生成token
func GenerateTokenForUser(c *gin.Context,u *model.User) {
j := &middleware.JWT{
SigningKey: []byte(global.AsongServer.Jwt.Signkey), // 唯一簽名
}
claims := request.UserClaims{
Username: u.Username,
StandardClaims: jwt.StandardClaims{
NotBefore: time.Now().Unix() - 1000, // 簽名生效時間
ExpiresAt: time.Now().Unix() + 60*60*2, // 過期時間 2h
Issuer: "asong", // 簽名的發行者
},
}
token ,err := j.GenerateToken(claims)
if err != nil {
response.FailWithMessage("獲取token失敗", c)
return
}
res := resp.ResponseUser{Username: u.Username,Nickname: u.Nickname,Avatar: u.Avatar}
response.OkWithData(resp.LoginResponse{User: res,Token: token,ExpiresAt: claims.ExpiresAt *1000},c)
return
}
好啦,代碼部分完成了,接下來就可以測試了,在測試之前,我再來學一下swagger,生成接口文檔。
2. swagger
隨著互聯網的發展,現在的網站都是前后端分離了,前端渲染頁面,后端提供API。前后端的唯一聯系,變成了API接口;API文檔變成了前后端開發人員聯系的紐帶,變得越來越重要,swagger就是一款讓你更好的書寫API文檔的框架。
swagger可以減少我們的工作量,直接生成API文檔,減少了文檔編寫的工作。我們先來看一看swagger的生態使用圖:
紅色字是官方推薦的。
- swagger-ui:一個渲染頁面,可以用來顯示API文檔。不可以編輯。
- swagger-editor:就是一個在線編輯文檔說明文件(swagger.json或swagger.yaml文件)的工具,以方便生態中的其他小工具(swagger-ui)等使用
- swagger-codegen:代碼生成器,腳手架。可以根據swagger.json或者swagger.yml文件生成指定的計算機語言指定框架的代碼。
- Swagger-validator:這個小工具是用來校驗生成的文檔說明文件是否符合語法規定的。用法非常簡單,只需url地址欄,根路徑下加上一個參數url,參數內容是放swagger說明文件的地址。即可校驗。
目前最流行的做法,就是在代碼注釋中寫上swagger相關的注釋,然后,利用小工具生成swagger.json或者swagger.yaml文件。
這里我們采用代碼注釋的方式實現。注釋規范參考官網即可:https://goswagger.io/use/spec/params.html。這里只是一個簡單實用,更多實用閱讀官方文檔學習。或者參考這篇文章:https://razeencheng.com/post/go-swagger
2.1 項目中使用swagger
- 安裝swag
$ go get -u github.com/swaggo/swag/cmd/swag
這里有一點需要注意,生成的可執行文件會放到 $GOPATH/bin目錄下,需要將這個路徑放到環境變量中,驗證是否安裝成功:
$ swag -v
$ swag version v1.6.7
- 安裝gin-swagger
$ go get -u github.com/swaggo/gin-swagger
$ go get -u github.com/swaggo/gin-swagger/swaggerFiles
- 安裝好第三方庫后,我們進入到API接口中,在相應方法上編寫swagger注釋,注釋參考go-gin-example
// @Tags Base
// @Summary 用戶登錄
// @Produce application/json
// @Param data body request.LoginRequest true "用戶登錄接口"
// @Success 200 {string} string "{"success":true,"data": { "user": { "username": "asong", "nickname": "", "avatar": "" }, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VybmFtZSI6ImFzb25nIiwiZXhwIjoxNTk2OTAyMzEyLCJpc3MiOiJhc29uZyIsIm5iZiI6MTU5Njg5NDExMn0.uUS1TreZusX-hL3nKOSNYZIeZ_0BGrxWjKI6xdpdO40", "expiresAt": 1596902312000 },,"msg":"操作成功"}"
// @Router /base/login [post]
func login(c *gin.Context) {}
// @Tags User
// @Summary 用戶修改密碼
// @Security ApiKeyAuth
// @Produce application/json
// @Param data body request.ChangePassword true "用戶修改密碼"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}"
// @Router /user/setPassword [PUT]
func setPassword(c *gin.Context) {}
- 接下來生成swagger文檔,進入項目根目錄,執行以下命令
$ swag init
完畢后會在你的項目根目錄下生成docs目錄:
docs/
├── docs.go
├── swagger.json
└── swagger.yaml
-
還差最后一步,對swagger-ui進行路由注冊,引入swagger生成的docs文件夾,圖片中紅色線是需要添加的
在這里插入圖片描述
至此,swagger也配置完成了。
3. 運行示例
運行代碼,瀏覽器進入http://localhost:8888/swagger/index.html,可以看到頁面如下:
在這里就可以進行接口測試了。
4. 總結
這篇文章,到此就結束了,第一次寫這么長的文章,對自己也是一種提升。有些模糊的概念,又學習了一遍,此篇文章,全是自己總結的,有錯誤歡迎指出。如果歡迎三連:點贊、看一看、轉發;感謝各位。
不會的小伙伴,趕快學起來吧,項目代碼地址:https://github.com/asong2020/Golang_Dream/tree/master/Gin/gin_jwt_swagger
我是asong,一名普普通通的程序員,永遠保持一顆熱愛技術的心。歡迎關注個人公眾號【Golang夢工廠】,第一時間觀看優質文章,你想學的這里都有。
獲取2020GIN官方中文文檔,后臺回復:gin,即可獲取。由筆者進行翻譯,會定期進行維護。
到這里結束嘍,我們下期見。