前言
哈嘍,你們好,我是asong,這是個人第八篇原創文章。據說大家還不會jwt、swagger,因此我帶來一個入門級別的小項目。實現用戶登錄、修改密碼的操做。使用GIN(後臺回覆Golang夢工廠
:gin,可獲取2020GIN中文文檔)做爲web框架,使用jwt進行身份校驗,使用swagger生成接口文檔。代碼已上傳我的github:https://github.com/asong2020/Golang_Dream/tree/master/Gin/gin_jwt_swagger。有須要的自行下載,配有詳細使用文檔。html
1. jwt
1.1 簡介
jwt全稱 Json web token,是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準。JWT的聲明通常被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也能夠增長一些額外的其餘業務邏輯所必須的聲明信息,該token也可直接被用於認證,也能夠被加密。學習jwt,咱們能夠從官網文檔入手,jwt官網傳送門。前端
1.2 json web 令牌結構
JSON Web令牌由三部分組成,這些部分由.
分隔,分別是git
Header程序員
Payloadgithub
Signatureweb
一個JWT表示如示例:xxxxx.yyyyy.zzzzz
redis
1.2.1 Header
Header一般由兩部分組成:令牌的類型和所使用的簽名算法,例如HMAC、SHA256或者RSA。算法
1{
2 "alg": "HS256",
3 "typ": "JWT"
4}
此json是由Base64Url編碼造成JWT的第一部分。數據庫
1.2.2 Payload
令牌的第二部分是有效載荷。用於聲明,一般存儲一些用戶ID之類的索引數據,也能夠放一些其餘有用的信息,注意:不要存儲機密數據。JWT標準定義了一些基本字段:json
iss
:該JWT的簽發者sub
:該JWT所面向的用戶aud
:接收該JWT的一方exp
(expires):過時時間iat
:簽發時間
除了定義這幾個標準字段外,咱們能夠定義一些咱們在業務處理中須要用到的字段,能夠有用戶的id、名字等。來個例子看一看吧:
1{
2 "iss": "asong",
3 "iat": 6666666666,
4 "exp": 6666666666,
5 "aud": "user",
6 "sub": "all",
7 "user_id": "6666666666666666666",
8 "username": "asong"
9}
上面的user_id、username都是咱們定義的字段。對此負載進行Base64Url編碼,造成JSON Web令牌的第二部分。
1.2.3 Signature
簽名實際上是對JWT的Header和Payload整合的一個簽名驗證。咱們須要將Header和Payload連接起來,而後使用一個key用HMAC SHA256進行加密,建立一個簽名,像下面這樣。
1HMACSHA256(
2 base64UrlEncode(header) + "." +
3 base64UrlEncode(payload),
4 secret)
簽名的做用用於驗證消息在此過程當中沒有更改,而且對於使用私鑰進行簽名的令牌,它還能夠驗證JWT的發件人是誰。
最終將這三部分放在一塊兒,由.
進行分隔,示例以下:
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中信息,示例定義以下:
1type UserClaims struct {
2 Username string
3 jwt.StandardClaims
4}
定義secret
1jwt:
2 signkey: 'asong'
定義過時時間
1redis:
2 addr: 127.0.0.1:6379
3 db: 1
4 password: ''
5 poolsize: 100
6 cache:
7 tokenexpired: 7200 # expired time 2*60*60
建立一個JWT對象
1type JWT struct {
2 SigningKey []byte
3}
4
5func NewJWT() *JWT {
6 return &JWT{
7 []byte(global.AsongServer.Jwt.Signkey),
8 }
9}
1.3.2 生成JWT
1func (j *JWT)GenerateToken(claims request.UserClaims) (string,error){
2 token := jwt.NewWithClaims(jwt.SigningMethodHS256,claims)
3 return token.SignedString(j.SigningKey)
4}
1.3.3 解析Token
1func (j *JWT)ParseToken(t string) (*request.UserClaims,error) {
2 token, err := jwt.ParseWithClaims(t,&request.UserClaims{}, func(token *jwt.Token) (interface{}, error) {
3 return j.SigningKey,nil
4 })
5 if err != nil{
6 if v, ok := err.(*jwt.ValidationError); ok {
7 if v.Errors&jwt.ValidationErrorMalformed != 0 {
8 return nil, errors.New("That's not even a token")
9 } else if v.Errors&jwt.ValidationErrorExpired != 0 {
10 // Token is expired
11 return nil, errors.New("Token is expired")
12 } else if v.Errors&jwt.ValidationErrorNotValidYet != 0 {
13 return nil, errors.New("Token not active yet")
14 } else {
15 return nil, errors.New("Couldn't handle this token:")
16 }
17 }
18 }
19 if token != nil {
20 if claims, ok := token.Claims.(*request.UserClaims); ok && token.Valid {
21 return claims, nil
22 }
23 return nil, errors.New("Couldn't handle this token:")
24
25 } else {
26 return nil, errors.New("Couldn't handle this token:")
27
28 }
29}
1.3.4 更新Token
1func (j *JWT) RefreshToken(t string) (string, error) {
2 jwt.TimeFunc = func() time.Time {
3 return time.Unix(0, 0)
4 }
5 token, err := jwt.ParseWithClaims(t, &request.UserClaims{}, func(token *jwt.Token) (interface{}, error) {
6 return j.SigningKey, nil
7 })
8 if err != nil {
9 return "", err
10 }
11 if claims, ok := token.Claims.(*request.UserClaims); ok && token.Valid {
12 jwt.TimeFunc = time.Now
13 claims.StandardClaims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix()
14 return j.GenerateToken(*claims)
15 }
16 return "", errors.New("Couldn't handle this token:")
17}
1.3.5 編寫中間件
1func Auth() gin.HandlerFunc{
2 return func(c *gin.Context) {
3 token := c.Request.Header.Get("token")
4 if token == ""{
5 response.Result(response.ERROR,gin.H{
6 "reload": true,
7 },"非法訪問",c)
8 c.Abort()
9 return
10 }
11 j := NewJWT()
12 claims, err := j.ParseToken(token)
13 if err != nil{
14 response.Result(response.ERROR, gin.H{
15 "reload": true,
16 }, err.Error(), c)
17 c.Abort()
18 return
19 }
20 c.Set("claims",claims)
21 c.Next()
22 }
23}
1.3.6 在gin框架中使用
路由註冊時,使用中間,做爲用戶登陸驗證
1func RouteUserInit(Router *gin.RouterGroup) {
2 UserRouter := Router.Group("user").Use(middleware.Auth())
3 {
4 UserRouter.PUT("setPassword",setPassword)
5 }
6}
用戶經過接口獲取Token以後,後續就會攜帶着Token再來請求咱們的其餘接口,這個時候就須要對這些請求的Token進行校驗操做了
1func login(c *gin.Context) {
2 var req request.LoginRequest
3 _ = c.ShouldBindJSON(&req)
4 if req.Username == "" || req.Password == ""{
5 response.FailWithMessage("參數錯誤",c)
6 return
7 }
8 user := &model.User{
9 Username: req.Username,
10 Password: req.Password,
11 }
12 u ,err := service.Login(user)
13 if err != nil{
14 response.FailWithMessage(err.Error(),c)
15 return
16 }
17 service.GenerateTokenForUser(c,u)
18}
19//生成token
20func GenerateTokenForUser(c *gin.Context,u *model.User) {
21 j := &middleware.JWT{
22 SigningKey: []byte(global.AsongServer.Jwt.Signkey), // 惟一簽名
23 }
24 claims := request.UserClaims{
25 Username: u.Username,
26 StandardClaims: jwt.StandardClaims{
27 NotBefore: time.Now().Unix() - 1000, // 簽名生效時間
28 ExpiresAt: time.Now().Unix() + 60*60*2, // 過時時間 2h
29 Issuer: "asong", // 簽名的發行者
30 },
31 }
32 token ,err := j.GenerateToken(claims)
33 if err != nil {
34 response.FailWithMessage("獲取token失敗", c)
35 return
36 }
37 res := resp.ResponseUser{Username: u.Username,Nickname: u.Nickname,Avatar: u.Avatar}
38 response.OkWithData(resp.LoginResponse{User: res,Token: token,ExpiresAt: claims.ExpiresAt *1000},c)
39 return
40}
好啦,代碼部分完成了,接下來就能夠測試了,在測試以前,我再來學一下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
1$ go get -u github.com/swaggo/swag/cmd/swag
這裏有一點須要注意,生成的可執行文件會放到 $GOPATH/bin目錄下,須要將這個路徑放到環境變量中,驗證是否安裝成功:
1$ swag -v
2$ swag version v1.6.7
安裝gin-swagger
1$ go get -u github.com/swaggo/gin-swagger
2$ go get -u github.com/swaggo/gin-swagger/swaggerFiles
安裝好第三方庫後,咱們進入到API接口中,在相應方法上編寫swagger註釋,註釋參考go-gin-example
1// @Tags Base
2// @Summary 用戶登陸
3// @Produce application/json
4// @Param data body request.LoginRequest true "用戶登陸接口"
5// @Success 200 {string} string "{"success":true,"data": { "user": { "username": "asong", "nickname": "", "avatar": "" }, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VybmFtZSI6ImFzb25nIiwiZXhwIjoxNTk2OTAyMzEyLCJpc3MiOiJhc29uZyIsIm5iZiI6MTU5Njg5NDExMn0.uUS1TreZusX-hL3nKOSNYZIeZ_0BGrxWjKI6xdpdO40", "expiresAt": 1596902312000 },,"msg":"操做成功"}"
6// @Router /base/login [post]
7func login(c *gin.Context) {}
8
9// @Tags User
10// @Summary 用戶修改密碼
11// @Security ApiKeyAuth
12// @Produce application/json
13// @Param data body request.ChangePassword true "用戶修改密碼"
14// @Success 200 {string} string "{"success":true,"data":{},"msg":"修改爲功"}"
15// @Router /user/setPassword [PUT]
16func setPassword(c *gin.Context) {}
接下來生成swagger文檔,進入項目根目錄,執行如下命令
1$ swag init
完畢後會在你的項目根目錄下生成docs目錄:
1docs/
2├── docs.go
3├── swagger.json
4└── 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,便可獲取。由筆者進行翻譯,會按期進行維護。
到這裏結束嘍,咱們下期見。
本文分享自微信公衆號 - Golang夢工廠(AsongDream)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。