利用 web 客戶端調用遠端服務是服務開發本實驗的重要內容。其中,要點創建 API First 的開發理念,實現先後端分離,使得團隊協做變得更有效率。html
項目參照星球大戰API SWAPI編寫vue
服務器實現其實並不難,可是理解GraphQL和GQLGEN這兩個預備工做比較困難,須要大量閱讀。git
GraphQL 是一個用於 API 的查詢語言,是一個使用基於類型系統來執行查詢的服務端運行時(類型系統由你的數據定義)。GraphQL 並無和任何特定數據庫或者存儲引擎綁定,而是依靠你現有的代碼和數據支撐。
與Restful相比,GraphQL不會由複雜的URL,請求的Json按照規範被放在數據中。因爲有完備的規範,使用GraphQL構建服務器時不須要自行對每一個請求進行解析,可使用現成的框架,如GQLGen,按規範編寫Schema後便可生成相應的解析函數,最終只須要本身編寫resolve中的查詢函數便可。無需對每一個數據規定複雜的URL,大大簡化了開發流程。github
GraphQL官網
GraphQL核心概念
GQLGEN樣例
GraphQL只是一個規範,具體使用時必須自行實現解析。這裏能夠用各類開源庫來簡化開發流程。web
使用GQLGEN首先應該編寫schema.graphql
文件,其中按照GraphQL規範,定義了全部結構的內容,以及查詢的方法,在這個項目中沒有用到客戶端更新數據,因此沒有使用Mutation。GQLGEN這個組件會根據schema生成對應的請求路徑解析和請求中GraphQL規則的查詢的解析,而且使用者只須要實現每一個請求的處理函數便可,簡化了開發流程。數據庫
type Query
中定義了全部的查詢查詢方法,在這個類型中的查詢函數會被GQLGEN自動實現解析,並在resolver.go
文件中新建空白查詢函數,而咱們的任務就是編寫該文件中的函數,返回對應的數據。json
""" The query root, from which multiple types of requests can be made. """ type Query { """ Look up a specific people by its ID. """ people( """ The ID of the entity. """ id: ID! ): People """ Look up a specific film by its ID. """ film( """ The ID of the entity. """ id: ID! ): Film """ Look up a specific starship by its ID. """ starship( """ The ID of the entity. """ id: ID! ): Starship """ Look up a specific vehicle by its ID. """ vehicle( """ The ID of the entity. """ id: ID! ): Vehicle """ Look up a specific specie by its ID. """ specie( """ The ID of the entity. """ id: ID! ): Specie """ Look up a specific planet by its ID. """ planet( """ The ID of the entity. """ id: ID! ): Planet """ Browse people entities. """ peoples ( """ The number of entities in the connection. """ first: Int """ The connection follows by. """ after: ID ): PeopleConnection! """ Browse film entities. """ films ( """ The number of entities in the connection. """ first: Int """ The connection follows by. """ after: ID ): FilmConnection! """ Browse starship entities. """ starships ( """ The number of entities in the connection. """ first: Int """ The connection follows by. """ after: ID ): StarshipConnection! """ Browse vehicle entities. """ vehicles ( """ The number of entities in the connection. """ first: Int """ The connection follows by. """ after: ID ): VehicleConnection! """ Browse specie entities. """ species ( """ The number of entities in the connection. """ first: Int """ The connection follows by. """ after: ID ): SpecieConnection! """ Browse planet entities. """ planets ( """ The number of entities in the connection. """ first: Int """ The connection follows by. """ after: ID ): PlanetConnection! """ Search for people entities matching the given query. """ peopleSearch ( """ The search field for name, in Lucene search syntax. """ search: String! """ The number of entities in the connection. """ first: Int """ The connection follows by. """ after: ID ): PeopleConnection """ Search for film entities matching the given query. """ filmsSearch ( """ The search field for title, in Lucene search syntax. """ search: String! """ The number of entities in the connection. """ first: Int """ The connection follows by. """ after: ID ): FilmConnection """ Search for starship entities matching the given query. """ starshipsSearch ( """ The search field for name or model, in Lucene search syntax. """ search: String! """ The number of entities in the connection. """ first: Int """ The connection follows by. """ after: ID ): StarshipConnection """ Search for vehicle entities matching the given query. """ vehiclesSearch ( """ The search field for name or model, in Lucene search syntax. """ search: String! """ The number of entities in the connection. """ first: Int """ The connection follows by. """ after: ID ): VehicleConnection """ Search for specie entities matching the given query. """ speciesSearch ( """ The search field for name, in Lucene search syntax. """ search: String! """ The number of entities in the connection. """ first: Int """ The connection follows by. """ after: ID ): SpecieConnection """ Search for planet entities matching the given query. """ planetsSearch ( """ The search field for name, in Lucene search syntax. """ search: String! """ The number of entities in the connection. """ first: Int """ The connection follows by. """ after: ID ): PlanetConnection }
schema.graphql
文件。這裏的schema.graphql
有些臃腫,能夠經過實現共同屬性的interface
來減小定義的工做量。在上述生成的文件中,咱們須要更改的文件主要是resolver.go
,在介紹此文件以前,咱們須要瞭解如下gengql生成的graphql的服務的運行過程:後端
resolver.go
文件下的People()函數:func (r *queryResolver) People(ctx context.Context, id string) (*People, error) { return &people{}, nil // 替換panic(避免運行過程當中退出,利於咱們觀察執行過程) }
go run server/server.go
訪問127.0.0.1:8080
,並進行一次People查詢:api
對於普通的經過ID查詢的函數,直接經過數據庫提供的方法查詢對應ID的對象。跨域
func (r *queryResolver) People(ctx context.Context, id string) (*People, error) { err, people := GetPeopleByID(id, nil) checkErr(err) return people, err }
分頁查詢則須要解析須要的元素數量,起始位置即after
遊標在數據庫中的位置,是否有先後頁及當前頁開始和結束位置元素的遊標,用於客戶端在須要的時候獲取先後頁。
func (r *queryResolver) Peoples(ctx context.Context, first *int, after *string) (PeopleConnection, error) { from := -1 if after != nil { b, err := base64.StdEncoding.DecodeString(*after) if err != nil { return PeopleConnection{}, err } i, err := strconv.Atoi(strings.TrimPrefix(string(b), "cursor")) if err != nil { return PeopleConnection{}, err } from = i } count := 0 startID := "" hasPreviousPage := true hasNextPage := true // 獲取edges edges := []PeopleEdge{} db, err := bolt.Open("./data/data.db", 0600, nil) CheckErr(err) defer db.Close() db.View(func(tx *bolt.Tx) error { c := tx.Bucket([]byte(peopleBucket)).Cursor() // 判斷是否還有前向頁 k, v := c.First() if from == -1 || strconv.Itoa(from) == string(k) { startID = string(k) hasPreviousPage = false } if from == -1 { for k, _ := c.First(); k != nil; k, _ = c.Next() { _, people := GetPeopleByID(string(k), db) edges = append(edges, PeopleEdge{ Node: people, Cursor: encodeCursor(string(k)), }) count++ if count == *first { break } } } else { for k, _ := c.First(); k != nil; k, _ = c.Next() { if strconv.Itoa(from) == string(k) { k, _ = c.Next() startID = string(k) } if startID != "" { _, people := GetPeopleByID(string(k), db) edges = append(edges, PeopleEdge{ Node: people, Cursor: encodeCursor(string(k)), }) count++ if count == *first { break } } } } k, v = c.Next() if k == nil && v == nil { hasNextPage = false } return nil }) if count == 0 { return PeopleConnection{}, nil } // 獲取pageInfo pageInfo := PageInfo{ HasPreviousPage: hasPreviousPage, HasNextPage: hasNextPage, StartCursor: encodeCursor(startID), EndCursor: encodeCursor(edges[count-1].Node.ID), } return PeopleConnection{ PageInfo: pageInfo, Edges: edges, TotalCount: count, }, nil }
其次是基於相關字段的分頁查詢,與普通分頁查詢相似,只是多了一個查詢字段的字符串來限定,獲取對應的頁。
func (r *queryResolver) PeopleSearch(ctx context.Context, search string, first *int, after *string) (*PeopleConnection, error) { if strings.HasPrefix(search, "Name:") { search = strings.TrimPrefix(search, "Name:") } else { return &PeopleConnection{}, errors.New("Search content must be ' Name:<People's Name you want to get> ' ") } from := -1 if after != nil { b, err := base64.StdEncoding.DecodeString(*after) if err != nil { return &PeopleConnection{}, err } i, err := strconv.Atoi(strings.TrimPrefix(string(b), "cursor")) if err != nil { return &PeopleConnection{}, err } from = i } count := 0 hasPreviousPage := false hasNextPage := false // 獲取edges edges := []PeopleEdge{} db, err := bolt.Open("./data/data.db", 0600, nil) CheckErr(err) defer db.Close() db.View(func(tx *bolt.Tx) error { c := tx.Bucket([]byte(peopleBucket)).Cursor() k, _ := c.First() // 判斷是否還有前向頁 if from != -1 { for k != nil { _, people := GetPeopleByID(string(k), db) if people.Name == search { hasPreviousPage = true } if strconv.Itoa(from) == string(k) { k, _ = c.Next() break } k, _ = c.Next() } } // 添加edge for k != nil { _, people := GetPeopleByID(string(k), db) if people.Name == search { edges = append(edges, PeopleEdge{ Node: people, Cursor: encodeCursor(string(k)), }) count++ } k, _ = c.Next() if first != nil && count == *first { break } } // 判斷是否還有後向頁 for k != nil { _, people := GetPeopleByID(string(k), db) if people.Name == search { hasNextPage = true break } k, _ = c.Next() } return nil }) if count == 0 { return &PeopleConnection{}, nil } // 獲取pageInfo pageInfo := PageInfo{ StartCursor: encodeCursor(edges[0].Node.ID), EndCursor: encodeCursor(edges[count-1].Node.ID), HasPreviousPage: hasPreviousPage, HasNextPage: hasNextPage, } return &PeopleConnection{ PageInfo: pageInfo, Edges: edges, TotalCount: count, }, nil }
其餘的查詢函數實現和上述People方法的實現基本相同。
項目結構以下:
GraphQLdemo │ dbOp.go │ generated.go │ gqlgen.yml │ models_gen.go │ resolver.go │ schema.graphql │ ├─data │ data.db │ ├─scripts │ gqlgen.go │ └─server server.go
使用基於 Token 的身份驗證方法,在服務端不須要存儲用戶的登陸記錄。大概的流程是這樣的:
根據官網的定義,JSON Web Token(如下簡稱 JWT)是一套開放的標準(RFC 7519),它定義了一套簡潔(compact)且 URL 安全(URL-safe)的方案,以安全地在客戶端和服務器之間傳輸 JSON 格式的信息。
優勢
支持跨域驗證,多應用於單點登陸
> 單點登陸(Single Sign On):在多個應用系統中,用戶只需登錄一次,就能夠訪問全部相互信任的應用
實現
router.go
中比對固定密碼,實現登出。每次請求到達服務器時,服務器中間件判斷是不是登錄請求。若是不是登錄請求,則獲取保存在請求頭部的Token進行比對,相同則調用正常的HttpHandler,錯誤則返回錯誤回覆。
func TokenMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.RequestURI[1:] != "login" { /* // token位於Authorization中,用此方法 token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor, func(token *jwt.Token) (interface{}, error) { return []byte(SecretKey), nil }) */ tokenStr := "" for k, v := range r.Header { if strings.ToUpper(k) == TokenName { tokenStr = v[0] break } } validToken := false for _, token := range tokens { if token.SW_TOKEN == tokenStr { validToken = true } } if validToken { ctx := context.WithValue(r.Context(), TokenName, tokenStr) next.ServeHTTP(w, r.WithContext(ctx)) } else { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("Unauthorized access to this resource")) //fmt.Fprint(w, "Unauthorized access to this resource") } } else { next.ServeHTTP(w, r) } }) }
jwt.go
中實現了建立Token和比對Token的方法,因爲只有一個Token,因此直接比對便可。
var tokens []Token const TokenName = "SW-TOKEN" const Issuer = "Go-GraphQL-Group" const SecretKey = "StarWars" type Token struct { SW_TOKEN string `json:"SW-TOKEN"` } type jwtCustomClaims struct { jwt.StandardClaims Admin bool `json:"admin"` } func CreateToken(secretKey []byte, issuer string, isAdmin bool) (token Token, err error) { claims := &jwtCustomClaims{ jwt.StandardClaims{ ExpiresAt: int64(time.Now().Add(time.Hour * 1).Unix()), Issuer: issuer, }, isAdmin, } tokenStr, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secretKey) token = Token{ tokenStr, } return } func ParseToken(tokenStr string, secretKey []byte) (claims jwt.Claims, err error) { var token *jwt.Token token, err = jwt.Parse(tokenStr, func(*jwt.Token) (interface{}, error) { return secretKey, nil }) claims = token.Claims return }