Die Authentifizierung ist eine der schwierigsten Aufgaben für Entwickler, die gerade erst mit GraphQL beginnen. Es gibt viele technische Überlegungen, darunter, welcher ORM einfach einzurichten ist, wie sichere Tokens generiert und Passwörter gehasht werden und sogar welche HTTP-Bibliothek verwendet werden soll und wie sie zu verwenden ist.
In diesem Artikel konzentrieren wir uns auf die lokale Authentifizierung. Sie ist vielleicht die beliebteste Methode zur Handhabung der Authentifizierung auf modernen Websites und geschieht durch die Abfrage von E-Mail und Passwort des Benutzers (im Gegensatz zur Verwendung von beispielsweise Google Auth).
Darüber hinaus verwendet dieser Artikel Apollo Server 2, JSON Web Tokens (JWT) und Sequelize ORM, um eine Authentifizierungs-API mit Node zu erstellen.
Authentifizierung verwalten
Als in, ein Anmeldesystem
- Authentifizierung identifiziert oder verifiziert einen Benutzer.
- Autorisierung validiert die Routen (oder Teile der App), auf die der authentifizierte Benutzer Zugriff haben kann.
Der Ablauf zur Implementierung ist
- Der Benutzer registriert sich mit Passwort und E-Mail
- Die Anmeldedaten des Benutzers werden in einer Datenbank gespeichert
- Der Benutzer wird nach Abschluss der Registrierung zum Login weitergeleitet
- Dem Benutzer wird Zugriff auf bestimmte Ressourcen gewährt, wenn er authentifiziert ist
- Der Zustand des Benutzers wird in einem der Browser-Speichermedien (z. B.
localStorage, Cookies, Session) oder JWT gespeichert.
Voraussetzungen
Bevor wir in die Implementierung einsteigen, hier sind ein paar Dinge, die Sie benötigen werden, um mitzukommen.
- Node 6 oder höher
- Yarn (empfohlen) oder NPM
- GraphQL Playground
- Grundkenntnisse in GraphQL und Node
- ...ein neugieriger Geist!
Abhängigkeiten
Das ist eine lange Liste, also legen wir los
- Apollo Server: Ein Open-Source-GraphQL-Server, der mit jeder Art von GraphQL-Client kompatibel ist. Wir werden in diesem Projekt keinen Express für unseren Server verwenden. Stattdessen nutzen wir die Leistung von Apollo Server, um unsere GraphQL-API bereitzustellen.
- bcryptjs: Wir möchten die Benutzerpasswörter in unserer Datenbank hashen. Deshalb verwenden wir bcrypt. Es stützt sich auf die
getRandomValues-Schnittstelle der Web Crypto API, um sichere Zufallszahlen zu erhalten. - dotenv: Wir verwenden dotenv, um Umgebungsvariablen aus unserer
.env-Datei zu laden. - jsonwebtoken: Sobald der Benutzer angemeldet ist, enthält jede nachfolgende Anfrage die JWT, die es dem Benutzer ermöglicht, auf Routen, Dienste und Ressourcen zuzugreifen, die mit diesem Token gestattet sind.
jsonwebtokenwird verwendet, um eine JWT zu generieren, die zur Authentifizierung von Benutzern verwendet wird. - nodemon: Ein Tool, das die Entwicklung von Node-basierten Anwendungen unterstützt, indem es die Node-Anwendung automatisch neu startet, wenn Änderungen im Verzeichnis erkannt werden. Wir möchten den Server nicht jedes Mal schließen und starten, wenn sich etwas in unserem Code ändert. Nodemon prüft jedes Mal Änderungen in unserer App und startet den Server automatisch neu.
- mysql2: Ein SQL-Client für Node. Wir benötigen ihn, um uns mit unserem SQL-Server zu verbinden, damit wir Migrationen ausführen können.
- sequelize: Sequelize ist ein Promise-basierter Node ORM für Postgres, MySQL, MariaDB, SQLite und Microsoft SQL Server. Wir werden Sequelize verwenden, um unsere Migrationen und Modelle automatisch zu generieren.
- sequelize cli: Wir werden Sequelize CLI verwenden, um Sequelize-Befehle auszuführen. Installieren Sie es global mit
yarn add --global sequelize-cliim Terminal.
Einrichtung der Verzeichnisstruktur und der Entwicklungsumgebung
Lass uns ein brandneues Projekt erstellen. Erstellen Sie einen neuen Ordner und fügen Sie Folgendes hinzu
yarn init -y
Das Flag -y bedeutet, dass wir bei allen Fragen von yarn init Ja auswählen und die Standardeinstellungen verwenden.
Wir sollten auch eine package.json-Datei in den Ordner legen, also installieren wir die Projekt-Abhängigkeiten
yarn add apollo-server bcrpytjs dotenv jsonwebtoken nodemon sequelize sqlite3
Als nächstes fügen wir Babel unserer Entwicklungsumgebung hinzu
yarn add babel-cli babel-preset-env babel-preset-stage-0 --dev
Jetzt konfigurieren wir Babel. Führen Sie touch .babelrc im Terminal aus. Das erstellt und öffnet eine Babel-Konfigurationsdatei, in die wir Folgendes einfügen:
{
"presets": ["env", "stage-0"]
}
Es wäre auch schön, wenn unser Server startet und auch Daten migriert. Das können wir automatisieren, indem wir package.json damit aktualisieren:
"scripts": {
"migrate": " sequelize db:migrate",
"dev": "nodemon src/server --exec babel-node -e js",
"start": "node src/server",
"test": "echo \"Error: no test specified\" && exit 1"
},
Hier ist unsere package.json-Datei zu diesem Zeitpunkt in ihrer Gesamtheit:
{
"name": "graphql-auth",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"migrate": " sequelize db:migrate",
"dev": "nodemon src/server --exec babel-node -e js",
"start": "node src/server",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"apollo-server": "^2.17.0",
"bcryptjs": "^2.4.3",
"dotenv": "^8.2.0",
"jsonwebtoken": "^8.5.1",
"nodemon": "^2.0.4",
"sequelize": "^6.3.5",
"sqlite3": "^5.0.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-0": "^6.24.1"
}
}
Nachdem nun unsere Entwicklungsumgebung eingerichtet ist, wenden wir uns der Datenbank zu, in der wir Dinge speichern werden.
Datenbank-Einrichtung
Wir werden MySQL als unsere Datenbank und Sequelize ORM für unsere Beziehungen verwenden. Führen Sie sequelize init aus (vorausgesetzt, Sie haben es zuvor global installiert). Der Befehl sollte drei Ordner erstellen: /config /models und /migrations. Zu diesem Zeitpunkt nimmt unsere Projektverzeichnisstruktur Gestalt an.
Konfigurieren wir unsere Datenbank. Erstellen Sie zuerst eine .env-Datei im Stammverzeichnis des Projekts und fügen Sie Folgendes ein:
NODE_ENV=development
DB_HOST=localhost
DB_USERNAME=
DB_PASSWORD=
DB_NAME=
Gehen Sie dann zum Ordner /config, den wir gerade erstellt haben, und benennen Sie die Datei config.json darin in config.js um. Fügen Sie dann diesen Code ein:
require('dotenv').config()
const dbDetails = {
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
dialect: 'mysql'
}
module.exports = {
development: dbDetails,
production: dbDetails
}
Hier lesen wir die Datenbankdetails aus, die wir in unserer .env-Datei festgelegt haben. process.env ist eine globale Variable, die von Node injiziert wird, und sie wird verwendet, um den aktuellen Zustand der Systemumgebung darzustellen.
Aktualisieren wir unsere Datenbankdetails mit den entsprechenden Daten. Öffnen Sie die SQL-Datenbank und erstellen Sie eine Tabelle namens graphql_auth. Ich verwende Laragon als meinen lokalen Server und phpmyadmin zur Verwaltung von Datenbanktabellen.
Was auch immer Sie verwenden, wir werden die .env-Datei mit den neuesten Informationen aktualisieren.
NODE_ENV=development
DB_HOST=localhost
DB_USERNAME=graphql_auth
DB_PASSWORD=
DB_NAME=<your_db_username_here>
Konfigurieren wir Sequelize. Erstellen Sie eine .sequelizerc-Datei im Stammverzeichnis des Projekts und fügen Sie Folgendes ein:
const path = require('path')
module.exports = {
config: path.resolve('config', 'config.js')
}
Jetzt integrieren wir unsere Konfiguration in die Modelle. Gehen Sie zu index.js im Ordner /models und bearbeiten Sie die config-Variable.
const config = require(__dirname + '/../../config/config.js')[env]
Schließlich schreiben wir unser Modell. Für dieses Projekt benötigen wir ein User-Modell. Lassen Sie uns Sequelize verwenden, um das Modell automatisch zu generieren. Hier ist, was wir im Terminal ausführen müssen, um das einzurichten:
sequelize model:generate --name User --attributes username:string,email:string,password:string
Lassen Sie uns das Modell bearbeiten, das für uns erstellt wird. Gehen Sie zu user.js im Ordner /models und fügen Sie Folgendes ein:
'use strict';
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
username: {
type: DataTypes.STRING,
},
email: {
type: DataTypes.STRING,
},
password: {
type: DataTypes.STRING,
}
}, {});
return User;
};
Hier haben wir Attribute und Felder für Benutzername, E-Mail und Passwort erstellt. Lassen Sie uns eine Migration ausführen, um Änderungen an unserem Schema zu verfolgen.
yarn migrate
Schreiben wir jetzt das Schema und die Resolver.
Schema und Resolver mit dem GraphQL-Server integrieren
In diesem Abschnitt definieren wir unser Schema, schreiben Resolverfunktionen und stellen sie auf unserem Server bereit.
Das Schema
Erstellen Sie im src-Ordner einen neuen Ordner namens /schema und darin eine Datei namens schema.js. Fügen Sie den folgenden Code ein:
const { gql } = require('apollo-server')
const typeDefs = gql`
type User {
id: Int!
username: String
email: String!
}
type AuthPayload {
token: String!
user: User!
}
type Query {
user(id: Int!): User
allUsers: [User!]!
me: User
}
type Mutation {
registerUser(username: String, email: String!, password: String!): AuthPayload!
login (email: String!, password: String!): AuthPayload!
}
`
module.exports = typeDefs
Hier haben wir graphql-tag von apollo-server importiert. Apollo Server erfordert das Umhüllen unseres Schemas mit gql.
Die Resolver
Erstellen Sie im src-Ordner einen neuen Ordner namens /resolvers und darin eine Datei namens resolver.js. Fügen Sie den folgenden Code ein:
const bcrypt = require('bcryptjs')
const jsonwebtoken = require('jsonwebtoken')
const models = require('../models')
require('dotenv').config()
const resolvers = {
Query: {
async me(_, args, { user }) {
if(!user) throw new Error('You are not authenticated')
return await models.User.findByPk(user.id)
},
async user(root, { id }, { user }) {
try {
if(!user) throw new Error('You are not authenticated!')
return models.User.findByPk(id)
} catch (error) {
throw new Error(error.message)
}
},
async allUsers(root, args, { user }) {
try {
if (!user) throw new Error('You are not authenticated!')
return models.User.findAll()
} catch (error) {
throw new Error(error.message)
}
}
},
Mutation: {
async registerUser(root, { username, email, password }) {
try {
const user = await models.User.create({
username,
email,
password: await bcrypt.hash(password, 10)
})
const token = jsonwebtoken.sign(
{ id: user.id, email: user.email},
process.env.JWT_SECRET,
{ expiresIn: '1y' }
)
return {
token, id: user.id, username: user.username, email: user.email, message: "Authentication succesfull"
}
} catch (error) {
throw new Error(error.message)
}
},
async login(_, { email, password }) {
try {
const user = await models.User.findOne({ where: { email }})
if (!user) {
throw new Error('No user with that email')
}
const isValid = await bcrypt.compare(password, user.password)
if (!isValid) {
throw new Error('Incorrect password')
}
// return jwt
const token = jsonwebtoken.sign(
{ id: user.id, email: user.email},
process.env.JWT_SECRET,
{ expiresIn: '1d'}
)
return {
token, user
}
} catch (error) {
throw new Error(error.message)
}
}
},
}
module.exports = resolvers
Das ist viel Code, also sehen wir uns an, was dort passiert.
Zuerst haben wir unsere Modelle, bcrypt und jsonwebtoken importiert und dann unsere Umgebungsvariablen initialisiert.
Als nächstes kommen die Resolverfunktionen. Im Query-Resolver haben wir drei Funktionen (me, user und allUsers)
- Die
me-Abfrage ruft die Details des aktuelleingelogtenBenutzers ab. Sie akzeptiert einuser-Objekt als Kontext-Argument. Der Kontext wird verwendet, um Zugriff auf unsere Datenbank zu gewähren, die verwendet wird, um die Daten für einen Benutzer anhand der ID zu laden, die als Argument in der Abfrage angegeben wird. - Die
user-Abfrage ruft die Details eines Benutzers anhand seiner ID ab. Sie akzeptiertidals Kontext-Argument und einuser-Objekt. - Die
alluser-Abfrage gibt die Details aller Benutzer zurück.
user wäre ein Objekt, wenn der Benutzerstatus eingeloggt wäre, und null, wenn nicht. Wir würden diesen Benutzer in unseren Mutationen erstellen.
Im Mutations-Resolver haben wir zwei Funktionen (registerUser und loginUser)
registerUserakzeptiert denusername, dieemailund daspassworddesusersund erstellt eine neue Zeile mit diesen Feldern in unserer Datenbank. Es ist wichtig zu beachten, dass wir das bcryptjs-Paket verwendet haben, um das Passwort des Benutzers mitbcrypt.hash(password, 10)zu hashen.jsonwebtoken.signsigniert die gegebene Nutzlast synchron in einen JSON Web Token-String (in diesem Fall die Benutzer-idundemail). Schließlich gibtregisterUserden JWT-String und das Benutzerprofil zurück, wenn erfolgreich, und gibt eine Fehlermeldung zurück, wenn etwas schiefgeht.loginakzeptiertemailundpasswordund prüft, ob diese Details mit den angegebenen übereinstimmen. Zuerst prüfen wir, ob deremail-Wert bereits irgendwo in der Benutzerdatenbank vorhanden ist.
models.User.findOne({ where: { email }})
if (!user) {
throw new Error('No user with that email')
}
Dann verwenden wir die bcrypt.compare-Methode von bcrypt, um zu prüfen, ob das Passwort übereinstimmt.
const isValid = await bcrypt.compare(password, user.password)
if (!isValid) {
throw new Error('Incorrect password')
}
Dann, genau wie wir es zuvor in registerUser getan haben, verwenden wir jsonwebtoken.sign, um einen JWT-String zu generieren. Die login-Mutation gibt den Token und das user-Objekt zurück.
Fügen wir nun den JWT_SECRET zu unserer .env-Datei hinzu.
JWT_SECRET=somereallylongsecret
Der Server
Endlich der Server! Erstellen Sie eine server.js-Datei im Stammverzeichnis des Projekts und fügen Sie diesen Code ein:
const { ApolloServer } = require('apollo-server')
const jwt = require('jsonwebtoken')
const typeDefs = require('./schema/schema')
const resolvers = require('./resolvers/resolvers')
require('dotenv').config()
const { JWT_SECRET, PORT } = process.env
const getUser = token => {
try {
if (token) {
return jwt.verify(token, JWT_SECRET)
}
return null
} catch (error) {
return null
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.get('Authorization') || ''
return { user: getUser(token.replace('Bearer', ''))}
},
introspection: true,
playground: true
})
server.listen({ port: process.env.PORT || 4000 }).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
Hier importieren wir das Schema, die Resolver und jwt und initialisieren unsere Umgebungsvariablen. Zuerst verifizieren wir den JWT-Token mit verify. jwt.verify akzeptiert den Token und das JWT-Secret als Parameter.
Als nächstes erstellen wir unseren Server mit einer ApolloServer-Instanz, die typeDefs und Resolver akzeptiert.
Wir haben einen Server! Lassen Sie ihn laufen, indem Sie yarn dev im Terminal ausführen.
Testen der API
Lassen Sie uns nun die GraphQL-API mit GraphQL Playground testen. Wir sollten uns registrieren, anmelden und alle Benutzer anzeigen können – einschließlich eines einzelnen Benutzers – per ID.
Wir beginnen damit, die GraphQL Playground-App zu öffnen oder einfach localhost://4000 im Browser zu öffnen, um darauf zuzugreifen.
Mutation für Benutzerregistrierung
mutation {
registerUser(username: "Wizzy", email: "[email protected]", password: "wizzyekpot" ){
token
}
}
Wir sollten etwas wie das hier erhalten.
{
"data": {
"registerUser": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImVtYWlsIjoiZWtwb3RAZ21haWwuY29tIiwiaWF0IjoxNTk5MjQwMzAwLCJleHAiOjE2MzA3OTc5MDB9.gmeynGR9Zwng8cIJR75Qrob9bovnRQT242n6vfBt5PY"
}
}
}
Mutation für Login
Lassen Sie uns nun mit den gerade erstellten Benutzerdaten anmelden.
mutation {
login(email:"[email protected]" password:"wizzyekpot"){
token
}
}
Wir sollten etwas wie das hier erhalten.
{
"data": {
"login": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImVtYWlsIjoiZWtwb3RAZ21haWwuY29tIiwiaWF0IjoxNTk5MjQwMzcwLCJleHAiOjE1OTkzMjY3NzB9.PDiBKyq58nWxlgTOQYzbtKJ-HkzxemVppLA5nBdm4nc"
}
}
}
Fantastisch!
Abfrage für einen einzelnen Benutzer
Um einen einzelnen Benutzer abzufragen, müssen wir den Benutzertoken als Autorisierungsheader übergeben. Gehen Sie zum Reiter HTTP Headers.

...und fügen Sie dies ein:
{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImVtYWlsIjoiZWtwb3RAZ21haWwuY29tIiwiaWF0IjoxNTk5MjQwMzcwLCJleHAiOjE1OTkzMjY3NzB9.PDiBKyq58nWxlgTOQYzbtKJ-HkzxemVppLA5nBdm4nc"
}
Hier ist die Abfrage:
query myself{
me {
id
email
username
}
}
Und wir sollten etwas wie das hier erhalten:
{
"data": {
"me": {
"id": 15,
"email": "[email protected]",
"username": "Wizzy"
}
}
}
Großartig! Holen wir uns nun einen Benutzer nach ID.
query singleUser{
user(id:15){
id
email
username
}
}
Und hier ist die Abfrage, um alle Benutzer zu erhalten.
{
allUsers{
id
username
email
}
}
Zusammenfassung
Die Authentifizierung ist eine der schwierigsten Aufgaben beim Erstellen von Websites, die sie erfordern. GraphQL ermöglichte es uns, eine gesamte Authentifizierungs-API mit nur einem Endpunkt zu erstellen. Sequelize ORM macht die Erstellung von Beziehungen zu unserer SQL-Datenbank so einfach, dass wir uns kaum um unsere Modelle kümmern mussten. Es ist auch bemerkenswert, dass wir keine HTTP-Server-Bibliothek (wie Express) benötigen und Apollo GraphQL als Middleware verwenden. Apollo Server 2 ermöglicht es uns nun, unsere eigenen bibliotheksunabhängigen GraphQL-Server zu erstellen!
Sehen Sie sich den Quellcode für dieses Tutorial auf GitHub an.
Es gibt viele Code-Smells
if(!user) throw new Error('You are not authenticated!'),Gibt es nicht einen besseren Weg, dies zu tun?