몇 달 전 GraphQL에 대해 처음 배우기 시작했을 때, 저는 이전 튜토리얼 를 사용하여 Couchbase 및 Node.js와 함께 사용할 수 있습니다. 이 튜토리얼은 GraphQL 객체를 만들고 NoSQL 데이터베이스에서 해당 객체를 쿼리하는 등 기본 사항에 중점을 두었습니다, 카우치베이스. 조금 앞으로 나아가서 저는 튜토리얼을 작성했습니다. 대체 방법 데이터베이스 계층이 강조되지 않았음에도 불구하고 Node.js와 함께 GraphQL을 사용하도록 했습니다.
API를 사용하거나 만들 때 모든 사람이 모든 데이터에 액세스하는 것을 원하지 않는 시나리오가 종종 있습니다. 이러한 시나리오에서는 권한 부여 및 API 토큰을 통해 일종의 규제가 필요할 것입니다. RESTful API와 마찬가지로 이 작업은 JSON 웹 토큰(JWT)을 통해 쉽게 수행할 수 있습니다.
JWT를 사용하는 방법을 살펴보겠습니다. GraphQL 애플리케이션을 사용하여 전체 또는 일부 데이터가 아닌 특정 데이터를 보호할 수 있습니다.
앞으로는 JSON 웹 토큰(JWT)이 어떻게 작동하는지에 대해 어느 정도 이해하는 것이 좋습니다. 빠른 시작 가이드를 확인하고 싶으시다면 제 튜토리얼을 참조하세요, Node.js 기반 API의 JWT 인증. 이 튜토리얼의 초점은 GraphQL이 아니지만 여전히 좋은 시작점으로 작동합니다.
이 튜토리얼의 목표는 GraphQL을 사용하는 API를 만드는 것입니다. 계정을 만들고, 토큰을 얻고, 해당 토큰을 사용하여 API의 보호된 부분에 액세스할 수 있습니다. 이 튜토리얼에서는 Couchbase 설치 및 구성에 대한 지침은 다루지 않지만 호환성을 위해 특별히 수행해야 할 작업은 없습니다.
프로젝트 종속성이 있는 새 Node.js 애플리케이션 만들기
프로젝트의 더 무겁고 복잡한 부분으로 들어가기 전에 모든 종속성과 상용구 코드가 포함된 새 프로젝트를 만들어 보겠습니다. 이미 Node.js가 설치 및 구성되었다고 가정하고 CLI에서 다음을 실행합니다:
|
1 2 3 4 5 6 7 8 9 |
npm init -y npm install express --save npm install express-graphql --save npm install graphql --save npm install jsonwebtoken --save npm install uuid --save npm install couchbase --save npm install body-parser --save npm install bcryptjs --save |
위의 명령은 새로운 package.json 파일을 열고 필요한 각 패키지를 설치하세요. 이러한 패키지에는 GraphQL 라이브러리와 Express 프레임워크용 GraphQL 확장 프로그램이 포함되어 있습니다. 또한 비밀번호 데이터를 해시하고, JWT를 생성하고, 모든 것을 Couchbase에 저장하는 방법도 포함되어 있습니다. 이 작업을 한 줄로 할 수 있었나요? 네, 하지만 쪼개면 더 읽기 쉬울 것 같았습니다.
프로젝트가 생성되었으면 계속해서 app.js 파일에 다음 코드가 포함되어 있습니다:
|
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 |
const Express = require("express"); const Couchbase = require("couchbase"); const BodyParser = require("body-parser"); const JsonWebToken = require("jsonwebtoken"); const Bcrypt = require("bcryptjs"); const ExpressGraphQL = require("express-graphql"); const GraphQLObjectType = require("graphql").GraphQLObjectType; const GraphQLID = require("graphql").GraphQLID; const GraphQLString = require("graphql").GraphQLString; const GraphQLSchema = require("graphql").GraphQLSchema; const GraphQLList = require("graphql").GraphQLList; const UUID = require("uuid"); var cluster = new Couchbase.Cluster("couchbase://localhost"); cluster.authenticate("example", "123456"); var bucket = cluster.openBucket("example"); var app = Express(); app.use(BodyParser.json()); app.set("jwt-secret", "polyglotdeveloper"); app.use("/graphql", ExpressGraphQL({ graphiql: true })); app.post("/register", (request, response) => { }); app.post("/login", (request, response) => { }); app.listen(3000, () => { console.log("Listening at :3000"); }); |
위의 코드를 상용구 코드라고 부르겠습니다. 기본적으로 몇 가지 설정을 하는 것일 뿐 최종 목표와는 전혀 관련이 없습니다. 다운로드한 패키지를 가져오고, Couchbase에 연결을 설정하고, Express Framework를 구성하고, 엔드포인트 3개를 정의하고, 포트 3000에서 서비스하고 있습니다.
제가 사용한 정보가 아닌 여러분만의 Couchbase 정보를 사용하세요. 또한 보안을 강화하려면 jwt-secret 따라서 여러분의 토큰은 저보다 덜 예측 가능한 방식으로 해시됩니다.
로그인 및 등록을 위한 간편 계정 API 엔드포인트 디자인하기
상용구 코드를 보면 등록과 로그인 모두에 대한 엔드포인트가 있다는 것을 알 수 있습니다. 우리가 하려는 일을 달성하는 방법은 여러 가지가 있으며, 아마도 제 솔루션보다 더 나은 방법도 있을 것입니다. 하지만 저는 목표에서 너무 멀리 벗어나지 않으면서도 논리적이고 구현하기 쉬운 방법을 찾았습니다.
부터 시작하여 /등록 엔드포인트에는 다음이 있습니다:
|
1 2 3 4 5 6 7 8 9 10 11 |
app.post("/register", (request, response) => { var id = UUID.v4(); request.body.type = "account"; request.body.password = Bcrypt.hashSync(request.body.password, 10); bucket.insert(id, request.body, (error, result) => { if(error) { return response.status(500).send({ code: error.code, message: error.message }); } response.send(request.body); }); }); |
사용자가 다음과 함께 POST 요청을 보낼 때 사용자 이름 그리고 비밀번호 본문에서 먼저 Couchbase 문서 키로 사용할 새 UUID를 생성한 다음 다음과 같이 Bcrypt를 사용하여 암호를 해싱하고 있습니다. 이전에 시연된 제가 작성한 튜토리얼에서
해시가 생성되면 프로필 데이터를 Couchbase에 저장하고 반환합니다. 전체 목표는 /등록 엔드포인트는 인증할 데이터를 제공해야 합니다. 인증을 위해 /로그인 엔드포인트와 같이 표시됩니다:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
app.post("/login", (request, response) => { var statement = "SELECT META(account).id, account.username, account.`password` FROM `" + bucket._name + "` AS account WHERE account.type = 'account' AND account.username = $username"; var query = Couchbase.N1qlQuery.fromString(statement); bucket.query(query, { "username": request.body.username }, (error, account) => { if(error) { return response.status(500).send({ code: error.code, message: error.message }); } Bcrypt.compare(request.body.password, account[0].password, function(error, result) { if(error || !result) { return response.status(401).send({ "success": false, "message": "Invalid username and password" }); } var token = JsonWebToken.sign(account[0].id, app.get("jwt-secret"), {}); response.send({"token": token}); }); }); }); |
위 코드에서는 요청과 함께 전달된 사용자 이름과 일치하는 사용자 이름이 포함된 문서를 찾기 위해 N1QL 쿼리를 만들고 있습니다. 모든 종류의 SQL 인젝션 공격을 방지하기 위해 매개변수화된 쿼리를 사용하고 있습니다.
문서가 발견되면 저장된 해시된 비밀번호를 요청과 함께 전달된 비밀번호와 비교합니다. 일치하면 비밀번호를 사용하여 사용자 아이디에 서명하여 향후 요청에 사용할 JWT를 제공합니다.
두 가지 엔드포인트에서 주목해야 할 몇 가지 사항이 있습니다:
- 데이터 유효성 검사를 수행하지 않습니다. 이것은 예시일 뿐이며 단순하게 유지하려고 노력하고 있습니다.
- JWT에 만료 기한을 설정하지 않습니다. 일반적으로 1시간 이내에 만료되도록 설정하지만 이것은 간단한 예일 뿐입니다.
현재로서는 GraphQL로 아무것도 하지 않았습니다. 사용자를 생성하고 JSON 웹 토큰을 얻기 위한 기반을 마련했을 뿐입니다. JWT를 생성하고 있지만 현재로서는 정확한지 검증하지 않고 있습니다.
Express 프레임워크 함수로 JSON 웹 토큰(JWT) 유효성 검사하기
보호된 데이터로 작업할 때는 JWT가 있는 것만으로는 충분하지 않습니다. 서명을 확인하여 토큰이 실제로 유효한지 확인해야 합니다. 또한 JWT를 전달하기 위한 솔루션도 필요합니다.
Express 프레임워크에는 JSON 웹 토큰 작업을 위한 미들웨어가 있지만 문서가 부족하다는 것을 알았습니다. 대신 직접 유효성 검사를 위한 메서드를 만드는 것이 더 쉬워 보였습니다. 다음을 살펴보세요:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
app.use((request, response, next) => { var authHeader = request.headers["authorization"]; if(authHeader) { var bearerToken = authHeader.split(" "); if(bearerToken.length == 2 && bearerToken[0].toLowerCase() == "bearer") { JsonWebToken.verify(bearerToken[1], app.get("jwt-secret"), function(error, decodedToken) { if(error) { return response.status(401).send("Invalid authorization token"); } request.decodedToken = decodedToken; next(); }); } else { next(); } } else { next(); } }); |
그렇다면 위의 코드에서는 무엇을 하고 있을까요?
우리는 app.use를 설정하면 모든 요청에 대해 이 함수가 호출됩니다. 이 함수가 호출되면 현재 요청 헤더를 살펴보고 권한 부여 헤더를 찾습니다. 권한 부여 헤더가 존재하면 무기명 토큰인지 확인하고 실제 토큰 값을 확인에 사용합니다. 실제 토큰은 JWT입니다. 토큰이 유효하면 디코딩된 값을 요청에 추가하여 다음 단계의 요청에서 액세스할 수 있게 됩니다. 토큰이 없으면 아무 일도 일어나지 않으므로 걱정할 필요가 없습니다. 모든 데이터 포인트가 보호되는 것은 아니므로 로직은 토큰이 존재하는 경우에만 토큰을 확인합니다.
요청에서 JWT 토큰을 가져와서 유효성을 검사하는 더 좋은 방법이 있나요? 아마도 그렇겠지만 위의 코드는 제가 테스트했을 때 작동했고 그리 복잡하지 않았습니다.
GraphQL 및 JavaScript를 사용하여 부분적으로 보호되는 API 개발하기
이제 JWT 로직이 마련되었으므로 API 개발에 집중할 수 있습니다. 기술적으로 /로그인 그리고 /등록 엔드포인트는 API의 일부이지만 초점은 GraphQL에 맞춰져 있습니다.
쿼리에 대해 걱정하기 전에 GraphQL 객체에 대해 집중해 보겠습니다:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const AccountType = new GraphQLObjectType({ name: "Account", fields: { id: { type: GraphQLID }, username: { type: GraphQLString }, password: { type: GraphQLString } } }); const CourseType = new GraphQLObjectType({ name: "Course", fields: { id: { type: GraphQLID }, title: { type: GraphQLString }, length: { type: GraphQLString }, author: { type: AccountType } } }); |
제 이전 카우치베이스와 그래프QL 튜토리얼를 사용하면 위의 개체들이 조금 다르게 보일 수 있습니다. 이는 제가 만든 대체 GraphQL 튜토리얼. 기본적으로 하나는 계정 데이터용이고 다른 하나는 코스 데이터용인 두 개의 객체가 있습니다. 가장 좋은 예는 아닐지 모르지만 작동하도록 만들겠습니다.
코스 데이터는 코스 작성자의 계정을 참조합니다. 작성자 정보에 대한 리졸버 메서드를 만들 예정이지만 아직은 아닙니다. 먼저 계정 데이터에 대한 쿼리를 별도로 생성해 보겠습니다:
|
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 |
const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { account: { type: AccountType, resolve: (root, args, context, info) => { return new Promise((resolve, reject) => { if(!context.decodedToken) { return reject("A valid authorization token is required"); } var statement = "SELECT META(account).id, account.username, account.`password` FROM `" + bucket._name + "` AS account WHERE account.type = 'account' AND META(account).id = $id"; var query = Couchbase.N1qlQuery.fromString(statement); bucket.query(query, { "id": context.decodedToken }, (error, result) => { if(error) { return reject(error.message); } resolve(result[0]); }); }); } } } }) }); |
다음과 같은 쿼리가 있을 때만 의미가 있습니다. 계정를 사용하여 특정 계정에 대한 데이터를 가져오고자 하는 경우 해당 계정은 자신의 계정이어야 합니다. 자신의 계정을 쿼리하는 경우 유효한 JWT가 있어야 합니다.
우리가 어떻게 사용하고 있는지 주목하세요. context.decodeToken 를 추가합니다. 그리고 컨텍스트 를 사용하면 요청에서 데이터를 가져올 수 있으며, 이 경우 디코딩된 토큰 데이터가 요청에 존재할 수 있습니다. 토큰이 존재하지 않는다면 유효한 토큰이 이 쿼리의 요구 사항이므로 오류를 발생시켜야 합니다.
유효한 토큰이 있으면 N1QL 쿼리를 생성하고 사용자 ID를 사용하여 계정을 쿼리하고 반환할 수 있습니다.
나쁘지 않죠?
쿼리를 좀 더 확장해 보겠습니다. 내에서 필드 객체에 또 다른 쿼리를 추가할 예정이지만 이번에는 코스 데이터를 쿼리하게 되며 코스 데이터가 반드시 보호되지는 않습니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
courses: { type: GraphQLList(CourseType), resolve: (root, args, context, info) => { return new Promise((resolve, reject) => { var statement = "SELECT META(course).id, course.title, course.length, course.author FROM `" + bucket._name + "` AS course WHERE course.type = 'course'"; var query = Couchbase.N1qlQuery.fromString(statement); bucket.query(query, (error, result) => { if(error) { return reject(error.message); } resolve(result); }); }); } } |
위의 코드에서는 데이터베이스에 저장된 모든 코스에 대해 간단한 N1QL 쿼리를 수행하고 있습니다. 여기서 문제는 작성자 데이터의 경우 보호되는 전체 로드된 계정 데이터가 아니라 키만 가져온다는 것입니다.
실제로 GraphQL 객체에서 다음과 같은 몇 가지 데이터 조작을 수행하겠습니다. 코스 유형 대신
|
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 |
const CourseType = new GraphQLObjectType({ name: "Course", fields: { id: { type: GraphQLID }, title: { type: GraphQLString }, length: { type: GraphQLString }, author: { type: AccountType, resolve: (root, args, context, info) => { return new Promise((resolve, reject) => { if(!context.decodedToken || context.decodedToken != root.author) { return reject("A valid authorization token is required"); } var statement = "SELECT META(account).id, account.username, account.`password` FROM `" + bucket._name + "` AS account WHERE account.type = 'account' AND META(account).id = $id"; var query = Couchbase.N1qlQuery.fromString(statement); bucket.query(query, { "id": root.author }, (error, result) => { if(error) { return reject({ code: error.code, message: error.message }); } resolve(result[0]); }); }); } } } }); |
이제 해결 함수의 작성자 속성을 호출합니다. 이 함수에서는 유효한 토큰이 있는지 확인하고 토큰을 찾지 못하면 오류를 반환합니다. 이 검사 및 오류는 작성자 데이터가 요청됩니다. 쿼리에서 다음을 요청하지 않는 경우 작성자 데이터는 보호되지 않기 때문에 토큰 없이도 다른 정보를 얻을 수 있습니다.
여기에는 또 다른 문제가 있습니다. 유효한 JWT가 존재하는지 확인하는 것뿐만 아니라 JWT가 N1QL 쿼리에서 반환된 데이터와 일치하는지 확인해야 합니다. 즉, 작성자가 여러 명인 경우 권한이 있는 작성자만 성공합니다. 다시 말하지만, 가장 좋은 예는 아니지만 우리의 요점을 증명합니다.
미진한 부분을 정리하기 위해서는 /graphql 엔드포인트:
|
1 2 3 4 |
app.use("/graphql", ExpressGraphQL({ schema: schema, graphiql: true })); |
우리가 한 일은 스키마 여기에는 두 가지 가능한 쿼리가 포함됩니다.
결론
예제는 간단했지만 GraphQL을 사용하여 보호된 쿼리와 데이터 조각을 시연할 수 있었습니다, 카우치베이스및 JSON 웹 토큰(JWT)을 사용할 수 있습니다. 기억해야 할 몇 가지 핵심 사항입니다:
- GraphQL에서 요청 데이터에 액세스할 수 있습니다.
컨텍스트변수입니다. - JSON 웹 토큰과 계정은 GraphQL 변형을 사용하는 것이 아니라 별도의 엔드포인트를 통해 만들어야 합니다.
- JWT로 데이터 속성뿐만 아니라 쿼리도 제한할 수 있습니다. 이것은 전부 아니면 전무의 일련의 이벤트가 아닙니다.
GraphQL에 대해 더 자세히 알아보시려면 이전 튜토리얼 를 참조하세요. Node.js가 포함된 Couchbase에 대해 자세히 알아보려면 다음을 확인하세요. 카우치베이스 개발자 포털.