首页 > 解决方案 > GraphQL 网关`预检请求未通过访问控制检查`

问题描述

我有一个GraphQL 网关来联合几个子图。网关跨这些子图执行传入操作。

在浏览器控制台中观察到错误

OPTIONS 方法被触发(预检),后端日志中没有任何内容。在下面的浏览器中观察到的错误

在此处输入图像描述

Access to fetch at 'http://dev.gateway.mydomain.net:5000/graphql' 
from origin 'http://localhost:3000' has been blocked by CORS policy: 
Response to preflight request doesn't pass access control check: 
No 'Access-Control-Allow-Origin' header is present on the requested resource. 
If an opaque response serves your needs, set the request's mode to 'no-cors' 
to fetch the resource with CORS disabled.

网络

Access-Control-Allow-Origin: *

在此处输入图像描述

选项方法

在此处输入图像描述

GraphQL 网关服务器

下面是我的 Apollo 网关的代码

我已将我为解决我的 CORS 问题而尝试的所有内容(TRY 1..7)标记为评论

const { ApolloServer } = require('apollo-server-express');
const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway');
const express = require('express');
const path = require('path');
const expressJwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');
const expressSession = require('express-session');
const passport = require('passport');
const Auth0Strategy = require('passport-auth0');
const jsonwebtoken = require('jsonwebtoken');
const dotenv = require('dotenv');
const cors = require('cors');
const userInViews = require('./lib/middleware/userInViews');
const authRouter = require('./routes/auth');
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
const graphqlRouter = require('./routes/graphql');
const headersRouter = require('./routes/headers');

dotenv.config({ path: `./gateway.${process.env.NODE_ENV}.env` });

const port = 5000;

const strategy = new Auth0Strategy(
  {
    domain: process.env.AUTH0_DOMAIN,
    clientID: process.env.AUTH0_CLIENT_ID,
    clientSecret: process.env.AUTH0_CLIENT_SECRET,
    callbackURL: process.env.AUTH0_CALLBACK_URL || `http://localhost:${port}/callback`,
  },
  (accessToken, refreshToken, extraParams, profile, done) => {
    console.log('ACCESS TOKEN', accessToken);
    const decoded = jsonwebtoken.decode(accessToken);
    const customProfile = profile;
    customProfile.permissions = decoded.permissions;
    customProfile.authorization = `Bearer ${accessToken}`;
    return done(null, customProfile);
  },
);

passport.use(strategy);

passport.serializeUser((user, done) => {
  done(null, user);
});

passport.deserializeUser((user, done) => {
  done(null, user);
});

const session = {
  secret: 'FILTERED',
  cookie: {},
  resave: false,
  saveUninitialized: true,
};

const app = express();
app.use(express.static('assets'));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

if (app.get('env') === 'production') {
  session.cookie.secure = true;
  app.set('trust proxy', 1);
}

app.use(expressSession(session));
app.use(passport.initialize());
app.use(passport.session());
app.use(userInViews()); // Application level middleware: Add user to view
app.use('/', authRouter);
app.use('/', indexRouter);
app.use('/', usersRouter);
app.use('/', graphqlRouter);
app.use('/', headersRouter);
app.set('json spaces', 2);

const localUserLoginMutationCheck = expressJwt({
  secret: 'f.....Y',
  algorithms: ['HS256'],
  credentialsRequired: false,
});

const auth0AuthenticationCheck = expressJwt({
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`,
  }),
  audience: 'http://dev.gateway.mydomain.net:5000',
  issuer: [`https://${process.env.AUTH0_DOMAIN}/`],
  algorithms: ['RS256'],
  credentialsRequired: false,
});

app.use(
  auth0AuthenticationCheck,
);

/**
* TRY 1
*/
const corsOptions = {
  origin: '*',
  credentials: true, // access-control-allow-credentials:true
  optionSuccessStatus: 200,
};
app.use(cors(corsOptions));

/**
* TRY 2
*/
app.use((req, res, next) => {
  console.log('INCOMING REQUEST HEADERS', req.headers);
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  console.log('INCOMING REQUEST HEADERS AFTER', req.headers);
  next();
});

const gateway = new ApolloGateway({
  serviceList: [
    { name: 'siebel', url: 'http://localhost:4000/graphql' },
    { name: 'sciencelogic', url: 'http://localhost:4001/graphql' },
  ],
  buildService({ name, url }) {
    return new RemoteGraphQLDataSource({
      url,
      willSendRequest({ request, context }) {
        console.log('CONTEXT', context);
        request.http.headers.set(
          'user',
          context.user
            ? JSON.stringify(context.user)
            : null,
        );
      },
    });
  },
});


const gqlServer = new ApolloServer({
  gateway,
  /**
  * TRY 3
  */
  // cors: true,

  /**
  * TRY 4
  */
  // cors: {
  //   origin: '*',
  //   methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
  //   preflightContinue: false,
  //   optionsSuccessStatus: 204,
  //   credentials: true,
  // },

  /**
  * TRY 5
  */
  // cors: {
  //   credentials: true,
  //   origin: (origin, callback) => {
  //     const whitelist = [
  //       'http://localhost:3000',
  //       'https://stage-tool.mydomain.net',
  //       'https://stage.tool.mydomain.net',
  //       'https://dsr.mydomain.net',
  //     ];
  //     if (whitelist.indexOf(origin) !== -1) {
  //       callback(null, true);
  //     } else {
  //       callback(new Error('Not allowed by CORS'));
  //     }
  //   },
  // },

  subscriptions: false,
  context: ({ req }) => {
    const user = req.user || null;
    return { user };
  },
  introspection: true,
});


// const corsOptions = {
//   origin: 'http://localhost:3000',
//   credentials: true,
// };
gqlServer.applyMiddleware({
  app,
  /**
  * TRY 6
  */
  cors: false, 

  /**
  * TRY 7
  */
  // cors: corsOptions,
});

app.listen({ port }, () => {
  console.log(`Gateway ready at http://localhost:${port}${gqlServer.graphqlPath}`);
});

子图服务器

const stackTrace = require('stack-trace');
const { ApolloServer } = require('apollo-server');
const { applyMiddleware } = require('graphql-middleware');
const { buildFederatedSchema } = require('@apollo/federation');
const jwt = require('jsonwebtoken');
const nodemailer = require('nodemailer');
const GqlHelpers = require('../gql-helpers');
const { siebelPermissions } = require('../../permissions');

const SiebelContactIntegration = require('./lib/ads-contact');

console.debug(`env ${JSON.stringify(process.env, null, 2)}`);
const resolvers = require(`./lib/gql/siebel.resolvers.${process.env.NODE_ENV}`);
const siebelSchema = GqlHelpers.gqlImport(__dirname, `./lib/gql/siebel.${process.env.NODE_ENV}.graphql`);
const typeDefs = GqlHelpers.gqlWrapper([siebelSchema]);

const federatedSchema = buildFederatedSchema([{ typeDefs, resolvers }]);

const server = new ApolloServer(
  {
    cors: true,
    schema: applyMiddleware(
      federatedSchema,
      siebelPermissions,
    ),
    context: ({ req }) => {
      const jwtToken = req.headers.authorization ? req.headers.authorization : null;
      console.log('HEADERS (SIEBEL)', req.headers);
      const jwtTokenArray = jwtToken ? jwtToken.split(' ') : [];
      const decodedJwt = jwtTokenArray.length > 0 ? jwt.decode(jwtTokenArray[1]) : null;
      const jwtIssuer = decodedJwt && decodedJwt.iss ? decodedJwt.iss : null;
      const jwtSubject = decodedJwt && decodedJwt.sub ? decodedJwt.sub : null;

      const user = req.headers.user ? JSON.parse(req.headers.user) : null;
      return { user, jwtIssuer, jwtSubject };
    },
    dataSources: () => ({
      siebelContactIntegration: new SiebelContactIntegration(),
    }),
    cacheControl: {
      defaultMaxAge: 3600,
    },
    introspection: true,
  },
);

server.listen(
  {
    port: 4000,
  },
  () => {
    console.log(` Siebel GraphQL Server ready at ${process.env.SAPI_URL}${server.graphqlPath}`);
  },
);

ReactJs 客户端使用apollo-client

注意获取选项设置为no-cors

import React, { useEffect, useState } from "react";
import {
  ApolloClient,
  InMemoryCache,
  HttpLink,
  ApolloProvider,
} from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { useAuth0 } from "@auth0/auth0-react";

function ApolloWrapper({ children }) {
  const { isAuthenticated, getAccessTokenSilently } = useAuth0();
  const [bearerToken, setBearerToken] = useState("");
  const httpLink = new HttpLink({
    uri: "http://dev.gateway.mydomain.net:5000/graphql",
  });

  useEffect(() => {
    const getToken = async () => {
      const token = isAuthenticated ? await getAccessTokenSilently() : "";
      setBearerToken(token);
      console.log(token);
    };
    getToken();
  }, [getAccessTokenSilently, isAuthenticated]);

  const authLink = setContext((_, { headers, ...rest }) => {
    if (!bearerToken) return { headers, ...rest };
    return {
      ...rest,
      headers: {
        ...headers,
        authorization: `Bearer ${bearerToken}` || null,
      },
    };
  });
  const client = new ApolloClient({
    cache: new InMemoryCache(),
    link: authLink.concat(httpLink),
    fetchOptions: {
      mode: 'no-cors',   // <== Note the fetch options
    },
  });

  return <ApolloProvider client={client}>{children}</ApolloProvider>;
}

export default ApolloWrapper;

使用 Apollo 客户端useLazyQuery执行 GraphQL 查询。

import { useLazyQuery } from "@apollo/client";

const [getEmail, { loading, error, data }] = useLazyQuery(
  gqlSiebelFindContact,
  {
    errorPolicy: "all",
  }
);

任何指针?

我试图弄清楚我为什么会得到这个问题,preflight request doesn't pass access control check 以及这个问题是要在前端还是后端代码上解决。

我可以通过将 Chrome 浏览器配置为no CORS.

非常欢迎任何进一步的故障排除和发现问题的指示

链接

标签: node.jsexpressgraphqlcors

解决方案


错误消息指向失败的预检请求,这是一个没有凭据的 OPTIONS 请求。此类预检请求应由app.use(cors(corsOptions))中间件处理。app.use(passport.session())但是这个中间件来得比较晚,所以如果任何早期的中间件(例如, )已经响应了这个 OPTIONS 请求,这将不起作用。

响应中的 OPTIONS 请求的屏幕截图Content-Length: 8似乎暗示这发生在这里。


推荐阅读