Durante los últimos meses he estado escribiendo una serie de GraphQL utilizando el lenguaje de programación Go. Primero vimos cómo empezar con GraphQL y Goseguido de una forma alternativa de manejar las relaciones de datos mediante el uso de resolvers en objetos GraphQL. Yendo un paso más allá vimos cómo incluir Tokens web JSON (JWT) para autorización sobre objetos GraphQL, pero sin base de datos.
El siguiente paso lógico en este viaje de GraphQL con Golang sería cablear Couchbase a una API con GraphQL completamente funcional que incluye autorización con tokens web JSON (JWT). Vamos a ver cómo manejar la creación de cuentas, validación JWT, y trabajar con datos en vivo a través de Consultas GraphQL.
Antes de sumergirte en algo de diseño y desarrollo, si no has visto mis tutoriales anteriores sobre el tema, probablemente deberías. Yo no recomendaría entrar en el lado JWT de las cosas hasta que tenga una comprensión de la utilización de GraphQL con Golang.
Inclusión de Couchbase en una aplicación GraphQL con JWT
En lugar de reiterar sobre el proceso de creación de una aplicación alimentada GraphQL, vamos a empezar desde donde lo dejamos en la serie. El tutorial anterior sobre JWT de la serie nos dejó el siguiente código:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
package main import ( "context" "encoding/json" "errors" "fmt" "net/http" jwt "github.com/dgrijalva/jwt-go" "github.com/graphql-go/graphql" "github.com/mitchellh/mapstructure" ) type User struct { Id string `json:"id"` Username string `json:"username"` Password string `json:"password"` } type Blog struct { Id string `json:"id"` Title string `json:"title"` Content string `json:"content"` Author string `json:"author"` Pageviews int32 `json:"pageviews"` } var jwtSecret []byte = []byte("thepolyglotdeveloper") var accountsMock []User = []User{ User{ Id: "1", Username: "nraboy", Password: "1234", }, User{ Id: "2", Username: "mraboy", Password: "5678", }, } var blogsMock []Blog = []Blog{ Blog{ Id: "1", Author: "nraboy", Title: "Sample Article", Content: "This is a sample article written by Nic Raboy", Pageviews: 1000, }, } var accountType *graphql.Object = graphql.NewObject(graphql.ObjectConfig{ Name: "Account", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.String, }, "username": &graphql.Field{ Type: graphql.String, }, "password": &graphql.Field{ Type: graphql.String, }, }, }) var blogType *graphql.Object = graphql.NewObject(graphql.ObjectConfig{ Name: "Blog", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.String, }, "title": &graphql.Field{ Type: graphql.String, }, "content": &graphql.Field{ Type: graphql.String, }, "author": &graphql.Field{ Type: graphql.String, }, "pageviews": &graphql.Field{ Type: graphql.Int, Resolve: func(params graphql.ResolveParams) (interface{}, error) { _, err := ValidateJWT(params.Context.Value("token").(string)) if err != nil { return nil, err } return params.Source.(Blog).Pageviews, nil }, }, }, }) func ValidateJWT(t string) (interface{}, error) { if t == "" { return nil, errors.New("Authorization token must be present") } token, _ := jwt.Parse(t, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("There was an error") } return jwtSecret, nil }) if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { var decodedToken interface{} mapstructure.Decode(claims, &decodedToken) return decodedToken, nil } else { return nil, errors.New("Invalid authorization token") } } func CreateTokenEndpoint(response http.ResponseWriter, request *http.Request) { var user User _ = json.NewDecoder(request.Body).Decode(&user) token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "username": user.Username, "password": user.Password, }) tokenString, error := token.SignedString(jwtSecret) if error != nil { fmt.Println(error) } response.Header().Set("content-type", "application/json") response.Write([]byte(`{ "token": "` + tokenString + `" }`)) } func main() { fmt.Println("Starting the application at :12345...") rootQuery := graphql.NewObject(graphql.ObjectConfig{ Name: "Query", Fields: graphql.Fields{ "account": &graphql.Field{ Type: accountType, Resolve: func(params graphql.ResolveParams) (interface{}, error) { account, err := ValidateJWT(params.Context.Value("token").(string)) if err != nil { return nil, err } for _, accountMock := range accountsMock { if accountMock.Username == account.(User).Username { return accountMock, nil } } return &User{}, nil }, }, "blogs": &graphql.Field{ Type: graphql.NewList(blogType), Resolve: func(params graphql.ResolveParams) (interface{}, error) { return blogsMock, nil }, }, }, }) schema, _ := graphql.NewSchema(graphql.SchemaConfig{ Query: rootQuery, }) http.HandleFunc("/graphql", func(response http.ResponseWriter, request *http.Request) { result := graphql.Do(graphql.Params{ Schema: schema, RequestString: request.URL.Query().Get("query"), Context: context.WithValue(context.Background(), "token", request.URL.Query().Get("token")), }) json.NewEncoder(response).Encode(result) }) http.HandleFunc("/login", CreateTokenEndpoint) http.ListenAndServe(":12345", nil) } |
Nuestro objetivo ahora es cambiar todos esos datos simulados por datos reales que existen en Couchbase. No nos preocuparemos de crear datos de blog en este tutorial, pero si quieres aprender sobre mutaciones, echa un vistazo a uno de los tutoriales anteriores.
El primer paso obvio hacia el uso de datos dinámicos es configurar nuestra base de datos, Couchbase. Cree la siguiente variable global que se utilizará en cada uno de nuestros objetos GraphQL:
|
1 |
var bucket *gocb.Bucket |
Con la referencia global Bucket creada, vamos a establecer una conexión con nuestro cluster Couchbase y abrir un bucket. Esto se puede hacer en nuestro proyecto principal función:
|
1 2 3 |
cluster, _ := gocb.Connect("couchbase://localhost") cluster.Authenticate(gocb.PasswordAuthenticator{Username: "example", Password: "123456"}) bucket, _ = cluster.OpenBucket("example", "") |
El código anterior asume un cluster ejecutándose localmente y RBAC así como la información de Bucket ya creada y definida. Si no has configurado correctamente tu instancia de Couchbase para esta aplicación, tómate un momento para hacerlo.
Dado que estamos trabajando con una base de datos NoSQL y ya no simulamos datos, nuestras estructuras Go nativas deben cambiar ligeramente:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
type User struct { Id string `json:"id,omitempty"` Username string `json:"username"` Password string `json:"password"` Type string `json:"type"` } type Blog struct { Id string `json:"id,omitempty"` Title string `json:"title"` Content string `json:"content"` Author string `json:"author"` Pageviews int32 `json:"pageviews"` Type string `json:"type"` } |
Al añadir un Tipo podemos escribir mejores consultas porque podemos diferenciar nuestros datos. Cambiar las estructuras de datos de Go no significa que tengamos que actualizar nuestros objetos GraphQL. Lo que esperamos devolver frente a con lo que esperamos trabajar puede ser diferente.
En el ejemplo anterior estábamos generando nuestro token web JSON con información pasada. En realidad, queremos generar nuestro JWT con la información real de la cuenta. Para hacer esto posible, necesitamos crear un endpoint para la creación de cuentas:
|
1 2 3 4 5 6 7 8 9 10 |
func CreateAccountEndpoint(response http.ResponseWriter, request *http.Request) { response.Header().Set("content-type", "application/json") var account User json.NewDecoder(request.Body).Decode(&account) hash, _ := bcrypt.GenerateFromPassword([]byte(account.Password), 10) account.Password = string(hash) id, _ := uuid.NewV4() bucket.Insert(id.String(), account, 0) response.Write([]byte(`{ "id": "` + id.String() + `" }`)) } |
La función anterior tomará un nombre de usuario y contraseña, hash de la contraseña con bcrypt, y la inserta en la base de datos. Estaremos consultando la base de datos para esta cuenta y comparando el hash con una contraseña como medio de autenticación. Para ello, probablemente deberíamos actualizar nuestro CreateTokenEndpoint función:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
func CreateTokenEndpoint(response http.ResponseWriter, request *http.Request) { response.Header().Set("content-type", "application/json") var user User _ = json.NewDecoder(request.Body).Decode(&user) query := gocb.NewN1qlQuery("SELECT example.* FROM example WHERE type = 'account' AND username = $1") var params []interface{} params = append(params, user.Username) results, _ := bucket.ExecuteN1qlQuery(query, params) var account User results.One(&account) if bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(user.Password)) != nil { response.Write([]byte(`{ "message": "incorrect password" }`)) return } token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "Username": account.Username, }) tokenString, error := token.SignedString(jwtSecret) if error != nil { fmt.Println(error) } response.Write([]byte(`{ "token": "` + tokenString + `" }`)) } |
Fíjate que en lugar de tomar el nombre de usuario y contraseña pasados y crear un JWT a partir de ellos, estamos haciendo una consulta a la base de datos. Si la información no coincide con lo que se pasó, vamos a devolver un error, de lo contrario vamos a seguir para crear un JWT basado en nuestro nombre de usuario.
Asumiendo que tenemos una forma sólida de crear cuentas y generar tokens web JSON a partir de ellas, podemos empezar a alterar nuestros objetos GraphQL para usar Couchbase en lugar de datos mock.
Dentro de la principal tenemos una función rootQuery con un objeto blogs así como una consulta cuenta consulta. Definiremos nuestra blogs y sería algo parecido a esto:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
"blogs": &graphql.Field{ Type: graphql.NewList(blogType), Resolve: func(params graphql.ResolveParams) (interface{}, error) { query := gocb.NewN1qlQuery("SELECT example.* FROM example WHERE type = 'blog'") results, _ := bucket.ExecuteN1qlQuery(query, nil) var result Blog var blogs []Blog for results.Next(&result) { blogs = append(blogs, result) } return blogs, nil }, }, |
En lugar de devolver una lista simulada de datos del blog estamos haciendo una consulta N1QL y devolviendo los resultados. La estructura de datos Go se asigna a nuestro objeto GraphQL.
Aunque estemos devolviendo datos del blog a través de nuestra consulta N1QL, la función páginas vistas sigue estando protegida con JWT como se define en el objeto.
La consulta final que tenemos es algo parecido a esto:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
"account": &graphql.Field{ Type: accountType, Resolve: func(params graphql.ResolveParams) (interface{}, error) { account, err := ValidateJWT(params.Context.Value("token").(string)) if err != nil { return nil, err } var user User mapstructure.Decode(account, &user) query := gocb.NewN1qlQuery("SELECT example.* FROM example WHERE type = 'account' AND username = $1") var n1qlParams []interface{} n1qlParams = append(n1qlParams, user.Username) results, _ := bucket.ExecuteN1qlQuery(query, n1qlParams) results.One(&user) return user, nil }, }, |
Observa que estamos recuperando la información decodificada del token y usándola como parámetro en nuestra consulta N1QL. Así es como podemos consultar una cuenta en particular basándonos en los datos del token, o en el usuario que ha iniciado sesión actualmente.
Prueba a crear algunos datos en la base de datos y mira a ver qué pasa.
Conclusión
Terminamos nuestra serie GraphQL con Go configurando Couchbase en nuestro ejemplo de autorización JWT. En realidad, añadir Couchbase no cambió nada de nuestro ejemplo JWT, sólo nos dio una fuente de datos para ser utilizada. Si profundizas en los tutoriales anteriores de esta serie, te adentrarás en GraphQL, que incluye consultas, mutaciones y protección de consultas, así como de fragmentos de datos. Todas las cosas que esperarías en una API lista para producción, pero con GraphQL en lugar de un enfoque tradicional de API REST.