GraphQL 网关`预检请求未通过访问控制检查`

Posted

技术标签:

【中文标题】GraphQL 网关`预检请求未通过访问控制检查`【英文标题】:GraphQL Gateway `preflight request doesn't pass access control check` 【发布时间】:2021-10-25 12:25:16 【问题描述】:

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

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

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 网关的代码

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

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 来解决此问题。

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

链接

https://github.com/apollographql/apollo-client/issues/5764

【问题讨论】:

如果对问题有帮助,请点赞并回答。谢谢 【参考方案1】:

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

响应中带有Content-Length: 8 的OPTIONS 请求的屏幕截图似乎暗示这里发生了这种情况。

【讨论】:

这正是问题所在。我在会话中间件之前移动了cors 中间件,这很有效!!!明天将奖励赏金。它现在不允许我这样做。

以上是关于GraphQL 网关`预检请求未通过访问控制检查`的主要内容,如果未能解决你的问题,请参考以下文章

“对预检请求的响应未通过访问控制检查”是啥意思?

对预检请求的响应未通过访问控制检查错误

对预检请求的响应未通过访问控制检查

对预检请求的响应未通过访问控制检查

对预检请求的响应未通过访问控制检查

CORS:对预检请求的响应未通过访问控制检查:预检请求不允许重定向