Служба аутентификации с Apollo Gateway
Я разрабатываю приложение на основе микросервисов с использованием Apollo Gateway. Каждая служба написана на Node.js и использует Graphql для построения федеративной схемы. Я также использую Mongoose для взаимодействия с базой данных MongoDB, совместно используемой службами. Основная цель разработки этого приложения - изучить и получить опыт использования новых для меня инструментов и технологий, таких как Graphql, микросервисы и Node.js.
У меня вопрос по аутентификации. Я решил использовать аутентификацию на основе JWT с дополнительными сеансами в базе данных для каждого пользователя. Таким образом, я могу отслеживать активные сеансы для каждого пользователя и отменять доступ, отключив сеанс, связанный с токеном. Все это управляется службой Auth, которая отвечает за аутентификацию, создание новых пользователей и функции входа / выхода. Служба Auth предоставляет одну конечную точку REST для проверки токена jwt следующим образом.
...
app.post('verify', async (req, res, next) => {
const token = req.body.jwt;
if(!token) {
res.status(403).send({ error: 'No token provided.' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findOne({ _id: decoded.sub});
if (!user) {
res.status(401).send({ error: 'No user found.' });
}
const session = await Session.findOne({
'_id': {
$in: user.sessions
}
});
if(!session) {
res.status(401).send({ error: 'Session not found or expired.' });
}
if(!session.valid) {
res.status(401).send({ error: 'Session not valid.' });
}
res.send({
userId: user.id,
scopes: user.scopes
});
}
const server = new ApolloServer({
schema: buildFederatedSchema([
{
typeDefs,
resolvers
}
]),
context: ({ req }) => {
return {
// headers
userId: req.get['user-id'] || 0,
scopes: req.get['user-scopes'] ? req.get['user-scopes'].split(',') : [],
// Mongoose models
models: {
User,
Session
}
}
}
});
server.applyMiddleware({ app, cors: false });
...
Мой шлюз API основан на шлюзе Apollo для построения федеративной схемы. Аутентификация проверяется службой Auth и передается всем остальным службам через заголовки запросов, установленные шлюзом.
...
// Set authenticated user id in request for other services
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
willSendRequest({ request, context }) {
// pass the user's id from the context to underlying services
// as a header called `user-id`
request.http.headers.set('user-id', context.userId);
request.http.headers.set('user-scopes', context.scopes.join(','));
}
}
const gateway = new ApolloGateway({
serviceList: [
{ name: 'auth', url: 'https://auth:4000' }
],
buildService: ({ name, url }) => {
return AuthenticatedDataSource({ url });
}
});
// Apollo server middleware - last applied
const server = new ApolloServer({
gateway,
// not supported
subscriptions: false,
context: async ({ req }) => {
try {
// Send auth query to Auth service REST api
const response = await axios.post('https://auth:4000/verify', {
jwt: req.cookies['plottwist_login']
});
// save auth data in context
return {
userId: response.data.userId,
scopes: response.data.scopes
}
} catch(e) {
// deal with error
}
}
});
server.applyMiddleware({ app, path, cors: false });
...
Таким образом, поток будет следующим:
- Шлюз API получает запрос запроса Graphql от клиента.
- Шлюз API запрашивает у службы аутентификации аутентификацию пользователя с использованием единственной конечной точки REST, предлагаемой службой аутентификации (копирование маркера cookie из полученного запроса).
- Сервис аутентификации аутентифицирует пользователя и отправляет данные обратно.
- Шлюз получает ответ, создает дополнительные заголовки запроса и продолжает работу, управляя исходным запросом Graphql.
Это связано с затратами на дополнительный вызов, который шлюз выполняет перед управлением каждым запросом Graphql, поступающим от клиента. Интересно, жизнеспособен ли это вариант или в моих рассуждениях есть серьезные недостатки.