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 |
paquete principal importar ( "contexto" "encoding/json" "errores" "fmt" "net/http" jwt "github.com/dgrijalva/jwt-go" "github.com/graphql-go/graphql" "github.com/mitchellh/mapstructure" ) tipo Usuario struct { Id cadena `json:"id"` Nombre de usuario cadena `json:"nombre de usuario"` Contraseña cadena `json:"contraseña"` } tipo Blog struct { Id cadena `json:"id"` Título cadena `json:"título` Contenido cadena `json:"contenido"` Autor cadena `json:"autor"` Páginas vistas int32 `json:"páginas vistas"` } var jwtSecret []byte = []byte("thepolyglotdeveloper") var cuentasMock []Usuario = []Usuario{ Usuario{ Id: "1", Nombre de usuario: "nraboy", Contraseña: "1234", }, Usuario{ Id: "2", Nombre de usuario: "mraboy", Contraseña: "5678", }, } var blogsMock []Blog = []Blog{ Blog{ Id: "1", Autor: "nraboy", Título: "Artículo de muestra", Contenido: "Este es un artículo de muestra escrito por Nic Raboy", Páginas vistas: 1000, }, } var tipo de cuenta *graphql.Objeto = graphql.NuevoObjeto(graphql.ObjectConfig{ Nombre: "Cuenta", Campos: graphql.Campos{ "id": &graphql.Campo{ Tipo: graphql.Cadena, }, "nombre de usuario": &graphql.Campo{ Tipo: graphql.Cadena, }, "contraseña": &graphql.Campo{ Tipo: graphql.Cadena, }, }, }) var blogTipo *graphql.Objeto = graphql.NuevoObjeto(graphql.ObjectConfig{ Nombre: "Blog, Campos: graphql.Campos{ "id": &graphql.Campo{ Tipo: graphql.Cadena, }, "título: &graphql.Campo{ Tipo: graphql.Cadena, }, "contenido": &graphql.Campo{ Tipo: graphql.Cadena, }, "autor": &graphql.Campo{ Tipo: graphql.Cadena, }, "páginas vistas": &graphql.Campo{ Tipo: graphql.Int, Resolver: func(parámetros graphql.ResolverParámetros) (interfaz{}, error) { _, err := ValidarJWT(parámetros.Contexto.Valor("token").(cadena)) si err != nil { devolver nil, err } devolver parámetros.Fuente.(Blog).Páginas vistas, nil }, }, }, }) func ValidarJWT(t cadena) (interfaz{}, error) { si t == "" { devolver nil, errores.Nuevo("El token de autorización debe estar presente") } ficha, _ := jwt.Analice(t, func(ficha *jwt.Ficha) (interfaz{}, error) { si _, ok := ficha.Método.(*jwt.SigningMethodHMAC); !ok { devolver nil, fmt.Errorf("Hubo un error") } devolver jwtSecret, nil }) si reclamaciones, ok := ficha.Reclamaciones.(jwt.MapaReclamaciones); ok && ficha.Válido { var decodedToken interfaz{} mapaestructura.Descodifique(reclamaciones, &decodedToken) devolver decodedToken, nil } si no { devolver nil, errores.Nuevo("Token de autorización no válido") } } func CreateTokenEndpoint(respuesta http.EscritorRespuesta, solicitar *http.Solicitar) { var usuario Usuario _ = json.NuevoDecodificador(solicitar.Cuerpo).Descodifique(&usuario) ficha := jwt.NuevoConDemandas(jwt.Método de firmaHS256, jwt.MapaReclamaciones{ "nombre de usuario": usuario.Nombre de usuario, "contraseña": usuario.Contraseña, }) tokenString, error := ficha.SignedString(jwtSecret) si error != nil { fmt.Imprimir(error) } respuesta.Cabecera().Establecer("tipo de contenido", "application/json") respuesta.Escriba a([]byte(`{ "token": "` + tokenString + `" }`)) } func principal() { fmt.Imprimir("Iniciando la aplicación en :12345...") rootQuery := graphql.NuevoObjeto(graphql.ObjectConfig{ Nombre: "Consulta", Campos: graphql.Campos{ "cuenta": &graphql.Campo{ Tipo: accountType, Resolver: func(parámetros graphql.ResolverParámetros) (interfaz{}, error) { cuenta, err := ValidarJWT(parámetros.Contexto.Valor("token").(cadena)) si err != nil { devolver nil, err } para _, accountMock := gama cuentasMock { si accountMock.Nombre de usuario == cuenta.(Usuario).Nombre de usuario { devolver accountMock, nil } } devolver &Usuario{}, nil }, }, "blogs": &graphql.Campo{ Tipo: graphql.NuevaLista(blogTipo), Resolver: func(parámetros graphql.ResolverParámetros) (interfaz{}, error) { devolver blogsMock, nil }, }, }, }) esquema, _ := graphql.NuevoEsquema(graphql.SchemaConfig{ Consulta: rootQuery, }) http.HandleFunc("/graphql", func(respuesta http.EscritorRespuesta, solicitar *http.Solicitar) { resultado := graphql.Visite(graphql.Parámetros{ Esquema: esquema, RequestString: solicitar.URL.Consulta().Visite("consulta"), Contexto: contexto.ConValor(contexto.Fondo(), "token", solicitar.URL.Consulta().Visite("token")), }) json.Nuevo codificador(respuesta).Codificar(resultado) }) 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 cubo *gocb.Cubo |
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 |
grupo, _ := gocb.Conectar("couchbase://localhost") grupo.Autentificar(gocb.PasswordAuthenticator{Nombre de usuario: "ejemplo", Contraseña: "123456"}) cubo, _ = grupo.OpenBucket("ejemplo", "") |
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 |
tipo Usuario struct { Id cadena `json:"id,omitempty"` Nombre de usuario cadena `json:"nombre de usuario"` Contraseña cadena `json:"contraseña"` Tipo cadena `json:"tipo"` } tipo Blog struct { Id cadena `json:"id,omitempty"` Título cadena `json:"título` Contenido cadena `json:"contenido"` Autor cadena `json:"autor"` Páginas vistas int32 `json:"páginas vistas"` Tipo cadena `json:"tipo"` } |
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(respuesta http.EscritorRespuesta, solicitar *http.Solicitar) { respuesta.Cabecera().Establecer("tipo de contenido", "application/json") var cuenta Usuario json.NuevoDecodificador(solicitar.Cuerpo).Descodifique(&cuenta) hash, _ := bcrypt.GenerarDesdeContraseña([]byte(cuenta.Contraseña), 10) cuenta.Contraseña = cadena(hash) id, _ := uuid.NuevoV4() cubo.Inserte(id.Cadena(), cuenta, 0) respuesta.Escriba a([]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(respuesta http.EscritorRespuesta, solicitar *http.Solicitar) { respuesta.Cabecera().Establecer("tipo de contenido", "application/json") var usuario Usuario _ = json.NuevoDecodificador(solicitar.Cuerpo).Descodifique(&usuario) consulta := gocb.NuevoN1qlQuery("SELECT ejemplo.* FROM ejemplo WHERE tipo = 'cuenta' AND nombre usuario = $1") var parámetros []interfaz{} parámetros = añadir(parámetros, usuario.Nombre de usuario) resultados, _ := cubo.EjecutarN1qlQuery(consulta, parámetros) var cuenta Usuario resultados.Un(&cuenta) si bcrypt.CompareHashAndPassword([]byte(cuenta.Contraseña), []byte(usuario.Contraseña)) != nil { respuesta.Escriba a([]byte(`{ "mensaje": "contraseña incorrecta" }`)) devolver } ficha := jwt.NuevoConDemandas(jwt.Método de firmaHS256, jwt.MapaReclamaciones{ "Nombre de usuario": cuenta.Nombre de usuario, }) tokenString, error := ficha.SignedString(jwtSecret) si error != nil { fmt.Imprimir(error) } respuesta.Escriba a([]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.Campo{ Tipo: graphql.NuevaLista(blogTipo), Resolver: func(parámetros graphql.ResolverParámetros) (interfaz{}, error) { consulta := gocb.NuevoN1qlQuery("SELECT ejemplo.* FROM ejemplo WHERE tipo = 'blog'") resultados, _ := cubo.EjecutarN1qlQuery(consulta, nil) var resultado Blog var blogs []Blog para resultados.Siguiente(&resultado) { blogs = añadir(blogs, resultado) } devolver 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 |
"cuenta": &graphql.Campo{ Tipo: accountType, Resolver: func(parámetros graphql.ResolverParámetros) (interfaz{}, error) { cuenta, err := ValidarJWT(parámetros.Contexto.Valor("token").(cadena)) si err != nil { devolver nil, err } var usuario Usuario mapaestructura.Descodifique(cuenta, &usuario) consulta := gocb.NuevoN1qlQuery("SELECT ejemplo.* FROM ejemplo WHERE tipo = 'cuenta' AND nombre usuario = $1") var n1qlParámetros []interfaz{} n1qlParámetros = añadir(n1qlParámetros, usuario.Nombre de usuario) resultados, _ := cubo.EjecutarN1qlQuery(consulta, n1qlParámetros) resultados.Un(&usuario) devolver usuario, 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.