Nos últimos meses, escrevi uma série sobre GraphQL usando a linguagem de programação Go. Primeiro, vimos como Comece a usar GraphQL e Goseguido de uma maneira alternativa de lidar com relacionamentos de dados usando resolvedores em objetos GraphQL. Dando um passo adiante, vimos como incluir Tokens da Web JSON (JWT) para autorização em objetos GraphQL, mas sem um banco de dados.
A próxima etapa lógica nessa jornada do GraphQL com Golang seria conectar o Couchbase para uma API com tecnologia GraphQL totalmente funcional que inclui autorização com tokens da Web JSON (JWT). Veremos como lidar com a criação de contas, a validação de JWT e o trabalho com dados em tempo real por meio de Consultas GraphQL.
Antes de mergulhar no design e no desenvolvimento, se você ainda não viu meus tutoriais anteriores sobre o assunto, provavelmente deveria ver. Eu não recomendaria entrar na parte do JWT até que você tenha um conhecimento sobre o uso de GraphQL com a Golang.
Inclusão do Couchbase em um aplicativo GraphQL com JWT
Em vez de reiterar o processo de criação de um aplicativo alimentado por GraphQL, vamos começar de onde paramos na série. O tutorial anterior sobre JWT na série nos deixou com o seguinte 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 |
pacote principal importação ( "contexto" "encoding/json" "erros" "fmt" "net/http" jwt "github.com/dgrijalva/jwt-go" "github.com/graphql-go/graphql" "github.com/mitchellh/mapstructure" ) tipo Usuário estrutura { Id string `json:"id"` Nome de usuário string `json:"nome de usuário"` Senha string `json:"senha"` } tipo Blog estrutura { Id string `json:"id"` Título string `json:"título"` Conteúdo string `json:"content" (conteúdo)` Autor string `json:"autor"` Visualizações de página int32 `json:"pageviews"` } var jwtSecret []byte = []byte("thepolyglotdeveloper") var contasMock []Usuário = []Usuário{ Usuário{ Id: "1", Nome de usuário: "nraboy", Senha: "1234", }, Usuário{ Id: "2", Nome de usuário: "mraboy", Senha: "5678", }, } var blogsMock []Blog = []Blog{ Blog{ Id: "1", Autor: "nraboy", Título: "Artigo de amostra", Conteúdo: "Este é um exemplo de artigo escrito por Nic Raboy", Visualizações de página: 1000, }, } var accountType *graphql.Objeto = graphql.Novo objeto(graphql.ObjectConfig{ Nome: "Conta", Campos: graphql.Campos{ "id": &graphql.Campo{ Tipo: graphql.Cordas, }, "nome de usuário": &graphql.Campo{ Tipo: graphql.Cordas, }, "senha": &graphql.Campo{ Tipo: graphql.Cordas, }, }, }) var blogType *graphql.Objeto = graphql.Novo objeto(graphql.ObjectConfig{ Nome: "Blog", Campos: graphql.Campos{ "id": &graphql.Campo{ Tipo: graphql.Cordas, }, "título": &graphql.Campo{ Tipo: graphql.Cordas, }, "content" (conteúdo): &graphql.Campo{ Tipo: graphql.Cordas, }, "autor": &graphql.Campo{ Tipo: graphql.Cordas, }, "pageviews": &graphql.Campo{ Tipo: graphql.Int, Resolver: func(parâmetros graphql.ResolveParams) (interface{}, erro) { _, erro := ValidarJWT(parâmetros.Contexto.Valor("token").(string)) se erro != nulo { retorno nulo, erro } retorno parâmetros.Fonte.(Blog).Visualizações de página, nulo }, }, }, }) func ValidarJWT(t string) (interface{}, erro) { se t == "" { retorno nulo, erros.Novo("O token de autorização deve estar presente") } token, _ := jwt.Analisar(t, func(token *jwt.Token) (interface{}, erro) { se _, ok := token.Método.(*jwt.SigningMethodHMAC); !ok { retorno nulo, fmt.Errof("Houve um erro") } retorno jwtSecret, nulo }) se reivindicações, ok := token.Reclamações.(jwt.MapClaims); ok && token.Válido { var decodedToken interface{} estrutura do mapa.Decodificar(reivindicações, &decodedToken) retorno decodedToken, nulo } mais { retorno nulo, erros.Novo("Token de autorização inválido") } } func CreateTokenEndpoint(resposta http.Escritor de respostas, solicitação *http.Solicitação) { var usuário Usuário _ = json.NewDecoder(solicitação.Corpo).Decodificar(&usuário) token := jwt.NovoComReclamações(jwt.SigningMethodHS256, jwt.MapClaims{ "nome de usuário": usuário.Nome de usuário, "senha": usuário.Senha, }) tokenString, erro := token.SignedString(jwtSecret) se erro != nulo { fmt.Println(erro) } resposta.Cabeçalho().Conjunto("content-type", "application/json") resposta.Escrever([]byte(`{ "token": "` + tokenString + `" }`)) } func principal() { fmt.Println("Iniciando o aplicativo em :12345...") rootQuery := graphql.Novo objeto(graphql.ObjectConfig{ Nome: "Consulta", Campos: graphql.Campos{ "account" (conta): &graphql.Campo{ Tipo: accountType, Resolver: func(parâmetros graphql.ResolveParams) (interface{}, erro) { conta, erro := ValidarJWT(parâmetros.Contexto.Valor("token").(string)) se erro != nulo { retorno nulo, erro } para _, accountMock := alcance contasMock { se accountMock.Nome de usuário == conta.(Usuário).Nome de usuário { retorno accountMock, nulo } } retorno &Usuário{}, nulo }, }, "blogs": &graphql.Campo{ Tipo: graphql.Nova lista(blogType), Resolver: func(parâmetros graphql.ResolveParams) (interface{}, erro) { retorno blogsMock, nulo }, }, }, }) esquema, _ := graphql.Novo esquema(graphql.SchemaConfig{ Consulta: rootQuery, }) http.HandleFunc("/graphql", func(resposta http.Escritor de respostas, solicitação *http.Solicitação) { resultado := graphql.Fazer(graphql.Parâmetros{ Esquema: esquema, RequestString: solicitação.URL.Consulta().Obter("query" (consulta)), Contexto: contexto.WithValue(contexto.Histórico(), "token", solicitação.URL.Consulta().Obter("token")), }) json.Novo codificador(resposta).Codificar(resultado) }) http.HandleFunc("/login", CreateTokenEndpoint) http.Ouvir e servir(":12345", nulo) } |
Nosso objetivo agora é trocar todos esses dados simulados por dados reais que existem no Couchbase. Não nos preocuparemos com a criação de dados de blog neste tutorial, mas se você quiser aprender sobre mutações, consulte um dos tutoriais anteriores.
A primeira etapa óbvia para usar dados dinâmicos é configurar nosso banco de dados, o Couchbase. Crie a seguinte variável global para ser usada em cada um de nossos objetos GraphQL:
|
1 |
var balde *gocb.Balde |
Com a referência global do Bucket criada, vamos estabelecer uma conexão com nosso cluster do Couchbase e abrir um bucket. Isso pode ser feito na seção principal função:
|
1 2 3 |
agrupamento, _ := gocb.Conectar("couchbase://localhost") agrupamento.Autenticar(gocb.PasswordAuthenticator{Nome de usuário: "exemplo", Senha: "123456"}) balde, _ = agrupamento.OpenBucket("exemplo", "") |
O código acima pressupõe um cluster em execução localmente e RBAC, bem como informações de Bucket já criadas e definidas. Se você não tiver configurado adequadamente sua instância do Couchbase para esse aplicativo, reserve um momento para fazer isso.
Como estamos trabalhando com um banco de dados NoSQL e não mais com dados simulados, nossas estruturas nativas em Go precisam ser ligeiramente alteradas:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
tipo Usuário estrutura { Id string `json:"id,omitempty"` Nome de usuário string `json:"nome de usuário"` Senha string `json:"senha"` Tipo string `json:"tipo"` } tipo Blog estrutura { Id string `json:"id,omitempty"` Título string `json:"título"` Conteúdo string `json:"content" (conteúdo)` Autor string `json:"autor"` Visualizações de página int32 `json:"pageviews"` Tipo string `json:"tipo"` } |
Ao adicionar um Tipo podemos escrever consultas melhores porque podemos diferenciar nossos dados. Alterar as estruturas de dados Go não significa que precisamos atualizar nossos objetos GraphQL. O que esperamos retornar em comparação com o que esperamos trabalhar pode ser diferente.
No exemplo anterior, estávamos gerando nosso token da Web JSON com informações passadas. Na realidade, queremos gerar nosso JWT com informações reais da conta. Para que isso seja possível, precisamos criar um ponto de extremidade para a criação de contas:
|
1 2 3 4 5 6 7 8 9 10 |
func CreateAccountEndpoint(resposta http.Escritor de respostas, solicitação *http.Solicitação) { resposta.Cabeçalho().Conjunto("content-type", "application/json") var conta Usuário json.NewDecoder(solicitação.Corpo).Decodificar(&conta) hash, _ := bcrypt.GenerateFromPassword([]byte(conta.Senha), 10) conta.Senha = string(hash) id, _ := uuid.NovoV4() balde.Inserir(id.Cordas(), conta, 0) resposta.Escrever([]byte(`{ "id": "` + id.String() + `" }`)) } |
A função acima receberá um nome de usuário e uma senha, fará o hash da senha com bcrypt e a inserirá no banco de dados. Consultaremos o banco de dados para essa conta e compararemos o hash com uma senha como meio de autenticação. Para fazer isso, provavelmente devemos atualizar nosso CreateTokenEndpoint função:
|
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(resposta http.Escritor de respostas, solicitação *http.Solicitação) { resposta.Cabeçalho().Conjunto("content-type", "application/json") var usuário Usuário _ = json.NewDecoder(solicitação.Corpo).Decodificar(&usuário) consulta := gocb.NewN1qlQuery("SELECT example.* FROM example WHERE type = 'account' AND username = $1") var parâmetros []interface{} parâmetros = anexar(parâmetros, usuário.Nome de usuário) resultados, _ := balde.ExecutarN1qlQuery(consulta, parâmetros) var conta Usuário resultados.Um(&conta) se bcrypt.CompareHashAndPassword([]byte(conta.Senha), []byte(usuário.Senha)) != nulo { resposta.Escrever([]byte(`{ "mensagem": "senha incorreta" }`)) retorno } token := jwt.NovoComReclamações(jwt.SigningMethodHS256, jwt.MapClaims{ "Nome de usuário": conta.Nome de usuário, }) tokenString, erro := token.SignedString(jwtSecret) se erro != nulo { fmt.Println(erro) } resposta.Escrever([]byte(`{ "token": "` + tokenString + `" }`)) } |
Observe que, em vez de pegar o nome de usuário e a senha passados e criar um JWT a partir deles, estamos fazendo uma consulta ao banco de dados. Se as informações não corresponderem ao que foi passado, retornaremos um erro; caso contrário, continuaremos a criar um JWT com base em nosso nome de usuário.
Supondo que tenhamos uma maneira sólida de criar contas e gerar tokens da Web JSON a partir delas, podemos começar a alterar nossos objetos GraphQL para usar o Couchbase em vez de dados simulados.
Dentro do principal temos uma função rootQuery com um objeto blogs bem como uma consulta conta consulta. Definiremos nossa blogs primeiro, e seria algo parecido com isto:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
"blogs": &graphql.Campo{ Tipo: graphql.Nova lista(blogType), Resolver: func(parâmetros graphql.ResolveParams) (interface{}, erro) { consulta := gocb.NewN1qlQuery("SELECT example.* FROM example WHERE type = 'blog'") resultados, _ := balde.ExecutarN1qlQuery(consulta, nulo) var resultado Blog var blogs []Blog para resultados.Próximo(&resultado) { blogs = anexar(blogs, resultado) } retorno blogs, nulo }, }, |
Em vez de retornar uma lista simulada de dados do blog, estamos fazendo uma consulta N1QL e retornando os resultados. A estrutura de dados Go é mapeada para o nosso objeto GraphQL.
Embora estejamos retornando dados de blog por meio de nossa consulta N1QL, o visualizações de página ainda está protegida com o JWT, conforme definido no objeto.
A consulta final que temos é mais ou menos assim:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
"account" (conta): &graphql.Campo{ Tipo: accountType, Resolver: func(parâmetros graphql.ResolveParams) (interface{}, erro) { conta, erro := ValidarJWT(parâmetros.Contexto.Valor("token").(string)) se erro != nulo { retorno nulo, erro } var usuário Usuário estrutura do mapa.Decodificar(conta, &usuário) consulta := gocb.NewN1qlQuery("SELECT example.* FROM example WHERE type = 'account' AND username = $1") var n1qlParams []interface{} n1qlParams = anexar(n1qlParams, usuário.Nome de usuário) resultados, _ := balde.ExecutarN1qlQuery(consulta, n1qlParams) resultados.Um(&usuário) retorno usuário, nulo }, }, |
Observe que estamos recuperando as informações do token decodificado e usando-as como parâmetro em nossa consulta N1QL. É assim que podemos consultar uma determinada conta com base nos dados do token ou no usuário conectado no momento.
Tente criar alguns dados no banco de dados e veja o que acontece.
Conclusão
Encerramos nossa série sobre GraphQL com Go configurando Couchbase em nosso exemplo de autorização JWT. Na realidade, adicionar o Couchbase não alterou nada do nosso exemplo de JWT, apenas nos deu uma fonte de dados a ser usada. Se você pesquisar os tutoriais anteriores desta série, conhecerá a fundo o GraphQL, que inclui consultas, mutações e proteção de consultas, além de partes de dados. Tudo o que você esperaria de uma API pronta para produção, mas com GraphQL em vez de uma abordagem tradicional de API REST.