GraphQL은 기존 RESTful API 아키텍처의 유력한 대안으로 자리매김하며, 데이터 질의 및 API 조작에 있어 유연성과 효율성을 제공합니다. GraphQL 채택이 증가함에 따라, 무단 접근과 잠재적인 데이터 유출로부터 애플리케이션을 보호하기 위해 GraphQL API 보안을 강화하는 것이 더욱 중요해지고 있습니다.
GraphQL API를 보호하는 핵심적인 방법 중 하나는 JWT(JSON Web Token)를 활용하는 것입니다. JWT는 클라이언트와 API 사이의 안전한 통신을 보장하며, 보호된 리소스에 대한 접근 권한을 부여하고, 허가된 작업만 수행하도록 하는 데 있어 효과적이고 안전한 메커니즘을 제공합니다.
GraphQL API의 인증 및 권한 부여
REST API와 달리, GraphQL API는 클라이언트가 단일 엔드포인트를 통해 다양한 양의 데이터를 동적으로 요청할 수 있는 특징을 가집니다. 이러한 유연성은 장점이지만, 접근 제어 취약성으로 인한 보안 위협의 위험도 높입니다.
이러한 위험을 줄이기 위해서는 접근 권한을 명확하게 정의하는 강력한 인증 및 권한 부여 프로세스를 구축하는 것이 중요합니다. 이를 통해 승인된 사용자만이 보호된 리소스에 접근할 수 있게 하여, 잠재적인 보안 침해와 데이터 손실 위험을 최소화할 수 있습니다.
이 프로젝트의 코드는 다음 GitHub 저장소에서 확인할 수 있습니다.
Express.js Apollo 서버 설정
Apollo Server는 GraphQL API를 위한 널리 사용되는 GraphQL 서버 구현체입니다. Apollo Server를 사용하면 GraphQL 스키마를 쉽게 구축하고, 리졸버를 정의하며, API에서 다양한 데이터 소스를 관리할 수 있습니다.
Express.js Apollo Server를 설정하려면 먼저 프로젝트 폴더를 만들고 해당 폴더로 이동합니다.
mkdir graphql-API-jwt
cd graphql-API-jwt
다음으로, npm(Node Package Manager)을 사용하여 새로운 Node.js 프로젝트를 초기화합니다.
npm init --yes
이제 필요한 패키지들을 설치합니다.
npm install apollo-server graphql mongoose jsonwebtokens dotenv
마지막으로, 루트 디렉토리에 `server.js` 파일을 생성하고 다음 코드를 추가하여 서버를 설정합니다.
const { ApolloServer } = require('apollo-server');
const mongoose = require('mongoose');
require('dotenv').config();const typeDefs = require("./graphql/typeDefs");
const resolvers = require("./graphql/resolvers");const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({ req }),
});const MONGO_URI = process.env.MONGO_URI;
mongoose
.connect(MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("Connected to DB");
return server.listen({ port: 5000 });
})
.then((res) => {
console.log(`Server running at ${res.url}`);
})
.catch(err => {
console.log(err.message);
});
GraphQL 서버는 API가 처리할 스키마와 작업을 명시하는 `typeDefs`와 `resolvers` 매개변수를 사용하여 구성됩니다. `context` 옵션은 각 리졸버의 컨텍스트에 `req` 객체를 추가하여 서버가 헤더 값과 같은 요청 관련 정보에 접근할 수 있도록 합니다.
MongoDB 데이터베이스 생성
데이터베이스 연결을 설정하려면 먼저 MongoDB 데이터베이스를 생성하거나 MongoDB Atlas에 클러스터를 설정해야 합니다. 그런 다음 제공된 데이터베이스 연결 URI 문자열을 복사하여 `.env` 파일을 생성한 후 다음과 같이 연결 문자열을 입력합니다.
MONGO_URI="<mongo_connection_uri>"
데이터 모델 정의
Mongoose를 사용하여 데이터 모델을 정의합니다. 새로운 `models/user.js` 파일을 만들고 다음 코드를 추가합니다.
const {model, Schema} = require('mongoose');const userSchema = new Schema({
name: String,
password: String,
role: String
});module.exports = model('user', userSchema);
GraphQL 스키마 정의
GraphQL API에서 스키마는 질의할 수 있는 데이터 구조를 정의할 뿐만 아니라, API를 통해 데이터와 상호 작용하기 위해 실행할 수 있는 사용 가능한 작업(쿼리 및 변형)을 개략적으로 보여줍니다.
스키마를 정의하려면 프로젝트 루트 디렉토리에 `graphql`이라는 새 폴더를 만듭니다. 이 폴더 안에 `typeDefs.js`와 `resolvers.js` 두 개의 파일을 추가합니다.
`typeDefs.js` 파일에 다음 코드를 추가합니다.
const { gql } = require("apollo-server");const typeDefs = gql`
type User {
id: ID!
name: String!
password: String!
role: String!
}
input UserInput {
name: String!
password: String!
role: String!
}
type TokenResult {
message: String
token: String
}
type Query {
users: [User]
}
type Mutation {
register(userInput: UserInput): User
login(name: String!, password: String!, role: String!): TokenResult
}
`;module.exports = typeDefs;
GraphQL API용 리졸버 생성
리졸버 함수는 클라이언트의 질의 및 변형, 그리고 스키마에 정의된 다른 필드에 대한 응답으로 데이터를 어떻게 가져올지를 결정합니다. 클라이언트가 질의 또는 변형을 보내면 GraphQL 서버는 해당 리졸버를 실행하여 데이터베이스나 API와 같은 여러 소스에서 필요한 데이터를 처리하고 반환합니다.
JWT(JSON Web Token)를 사용하여 인증 및 권한 부여를 구현하려면, 등록 및 로그인 변형에 대한 리졸버를 정의해야 합니다. 이것은 사용자 등록 및 인증 프로세스를 처리합니다. 그런 다음, 인증되고 권한이 부여된 사용자만이 접근할 수 있는 데이터 검색 쿼리 리졸버를 만듭니다.
먼저, JWT를 생성하고 검증하는 함수를 정의합니다. `resolvers.js` 파일에 다음 import 구문을 추가하는 것으로 시작합니다.
const User = require("../models/user");
const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;
JSON Web Token에 서명하는 데 사용할 비밀 키를 `.env` 파일에 추가해야 합니다.
SECRET_KEY = '<my_Secret_Key>';
인증 토큰을 생성하려면, JWT 토큰의 고유 속성(예: 만료 시간)도 지정하는 다음 함수를 포함합니다. 또한 특정 애플리케이션 요구 사항에 따라 발행 시점과 같은 다른 속성을 통합할 수도 있습니다.
function generateToken(user) {
const token = jwt.sign(
{ id: user.id, role: user.role },
secretKey,
{ expiresIn: '1h', algorithm: 'HS256' }
);return token;
}
이제 후속 HTTP 요청에 포함된 JWT 토큰의 유효성을 검사하는 토큰 검증 로직을 구현합니다.
function verifyToken(token) {
if (!token) {
throw new Error('Token not provided');
}try {
const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256'] });
return decoded;
} catch (err) {
throw new Error('Invalid token');
}
}
이 함수는 토큰을 입력으로 받아, 지정된 비밀 키를 사용하여 유효성을 검사하고, 유효하면 디코딩된 토큰을 반환하며, 유효하지 않으면 오류를 발생시킵니다.
API 리졸버 정의
GraphQL API에 대한 리졸버를 정의하려면, 관리할 특정 작업(이 경우 사용자 등록 및 로그인 작업)을 명시해야 합니다. 먼저, 리졸버 함수를 담을 리졸버 객체를 생성한 다음, 다음 변형 작업을 정의합니다.
const resolvers = {
Mutation: {
register: async (_, { userInput: { name, password, role } }) => {
if (!name || !password || !role) {
throw new Error('Name, password, and role are required');
}const newUser = new User({
name: name,
password: password,
role: role,
});try {
const response = await newUser.save();return {
id: response._id,
...response._doc,
};
} catch (error) {
console.error(error);
throw new Error('Failed to create user');
}
},
login: async (_, { name, password }) => {
try {
const user = await User.findOne({ name: name });if (!user) {
throw new Error('User not found');
}if (password !== user.password) {
throw new Error('Incorrect password');
}const token = generateToken(user);
if (!token) {
throw new Error('Failed to generate token');
}return {
message: 'Login successful',
token: token,
};
} catch (error) {
console.error(error);
throw new Error('Login failed');
}
}
},
`register` 변이는 새로운 사용자 데이터를 데이터베이스에 추가하여 등록 프로세스를 처리합니다. `login` 변이는 사용자 로그인을 관리하며, 인증이 성공하면 JWT 토큰을 생성하여 응답으로 성공 메시지와 함께 반환합니다.
이제 사용자 데이터를 검색하기 위한 쿼리 리졸버를 추가합니다. 인증되고 권한이 부여된 사용자만 이 쿼리에 접근할 수 있도록, 관리자 역할을 가진 사용자에게만 액세스를 제한하는 권한 부여 로직을 포함합니다.
기본적으로 이 쿼리는 먼저 토큰을 확인한 후 사용자 역할을 검사합니다. 권한 부여 확인이 성공하면 리졸버 쿼리가 진행되어 데이터베이스에서 사용자 데이터를 가져오고 반환합니다.
Query: {
users: async (parent, args, context) => {
try {
const token = context.req.headers.authorization || '';
const decodedToken = verifyToken(token);if (decodedToken.role !== 'Admin') {
throw new Error('Unauthorized. Only Admins can access this data.');
}const users = await User.find({}, { name: 1, _id: 1, role: 1 });
return users;
} catch (error) {
console.error(error);
throw new Error('Failed to fetch users');
}
},
},
};
마지막으로, 개발 서버를 시작합니다.
node server.js
성공적으로 서버가 실행되었다면, 이제 브라우저에서 Apollo Server API 샌드박스를 사용하여 API 기능을 테스트해 볼 수 있습니다. 예를 들어, `register` 변형을 사용하여 데이터베이스에 새로운 사용자 데이터를 추가한 다음, `login` 변형을 사용하여 사용자를 인증할 수 있습니다.
마지막으로, 인증 헤더 섹션에 JWT 토큰을 추가하고 데이터베이스에서 사용자 데이터를 질의합니다.
GraphQL API 보안
인증과 권한 부여는 GraphQL API 보안의 필수적인 요소이지만, 포괄적인 보안을 보장하기에는 충분하지 않을 수 있습니다. 입력 유효성 검사 및 민감한 데이터 암호화와 같은 추가 보안 조치를 구현해야 합니다.
포괄적인 보안 접근 방식을 채택하면, 다양한 잠재적 공격으로부터 API를 효과적으로 보호할 수 있습니다.