利用 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 }