簡體   English   中英

COR 錯誤:從 React 到 Express 的 Google Oauth(PassportJs 驗證)

[英]CORs Error: Google Oauth from React to Express (PassportJs validation)

我正在嘗試使用 Google OAuth 身份驗證設置 React/Redux - NodeJs Express 堆棧。 我的問題是控制台中的 COR 錯誤。 我發現了一些我認為正是我的問題的 Stack Overflow 問題,但解決方案沒有產生任何結果。 特別是這兩個: 帶有 google oauth 的CORS 和帶有 React/Node/Express 和 google OAuth 的 CORS/CORB 問題

因此,我嘗試了各種修復方法,這些修復方法似乎都使我回到了相同的錯誤。 這是其中最直接的:

const corsOptions = {
    origin: 'http://localhost:3000',
    optionsSuccessStatus: 200,
    credentials: true
}
app.use(cors(corsOptions));

這是在我的API.js文件的根目錄中。 我收到的控制台錯誤狀態:

訪問 XMLHttpRequest ' https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fapi%2Foauth%2Fgoogle%2Freturn&scope=profile&client_id=PRIVATE_CLIENT_ID。 .googleusercontent.com '(重定向自 ' http://localhost:5000/api/oauth/google ')來自原點 'null' 已被 CORS 政策阻止:對預檢請求的響應未通過訪問控制檢查:否 '請求的資源上存在 Access-Control-Allow-Origin 標頭。

因此,如果我在開發工具中查看我的網絡日志,我會查看我對 API 路徑的請求,並查看我希望看到的內容:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Origin: http://localhost:3000

所以在我看來,我的問題不在於我的前后溝通。 這讓我相信這可能是 Passport 令牌驗證的問題。 以下是我的簡化路線:

router.post('/oauth/google', passport.authenticate('googleVerification', {
    scope: ['profile']
}), (req, res) => {
    console.log('Passport has verified the oauth token...');
    res.status(200)
});

和回調路線:

router.get('/oauth/google/return', (req, res) => {
    console.log('google oauth return has been reached...')
    res.status(200)
});

最后,簡化策略:

passport.use('googleVerification', new GoogleStrategy({
    clientID: process.env.OAUTH_CLIENT_ID,
    clientSecret: process.env.OAUTH_SECRET,
    callbackURL: 'http://localhost:5000/api/oauth/google/return'
}, (accessToken, refreshToken, profile, cb) => {
    console.log('Passport OAuth Strategy reached');
    cb(null, profile)
}));

我知道所有這些都不會導致任何功能,但我只是盡可能多地撕掉了絨毛,試圖了解我的身份驗證流程中的塊在哪里。 以防萬一它可能有助於縮小范圍,這里是 Redux 中的動作創建者,它在錯誤開始出現之前記錄過程中的最后一步('redux 接受令牌並傳遞給 API:',令牌):

export const signIn = (token) => {
    console.log('redux accepting token and passing to API:', token)
    return async dispatch => {
        const res = await Axios({
            method: 'post',
            url: `${API_ROOT}/api/oauth/google`,
            withCredentials: true,
            data: {
                access_token: token
            }
        })

        console.log('API has returned a response to redux:', res)

        dispatch({
            type: SIGN_IN,
            payload: res
        })
    }
};

這實際上永遠不會到達返回值,並且不會記錄第二個console.log作為記錄。

該 CORS 與向 google 提出請求無關,因為當您在console.developers.google.com 中注冊您的應用程序時,它已經由 google 處理。

問題出在CRA 開發人員服務器express api 服務器之間 您正在從localhost:3000localhost:5000發出請求。 要解決此問題,請使用代理。

在客戶端目錄中:

npm i http-proxy-middleware --save

client/src 中創建setupProxy.js文件。 無需在任何地方導入它。 create-react-app會尋找這個目錄

將您的代理添加到此文件:

module.exports = function(app) {
    app.use(proxy("/auth/google", { target: "http://localhost:5000" }));
    app.use(proxy("/api/**", { target: "http://localhost:5000" }));
};

我們是說創建一個代理,如果有人試圖訪問我們的反應服務器上的 /api 或 /auth/google 路由,自動將請求轉發到localhost:5000

這是更多詳細信息的鏈接:

https://create-react-app.dev/docs/proxying-api-requests-in-development/

默認情況下 password.js 不允許代理請求。

passport.use('googleVerification', new GoogleStrategy({
    clientID: process.env.OAUTH_CLIENT_ID,
    clientSecret: process.env.OAUTH_SECRET,
    callbackURL: 'http://localhost:5000/api/oauth/google/return',
    proxy:true
}

這里重要的一件事是,您應該了解為什么使用代理。 據我了解,從你的代碼來看,從瀏覽器中,你向 express 發出請求,express 將使用 password.js 處理身份驗證。 password.js 運行完所有認證步驟后,它會創建一個 cookie,用 id 填充它,將其交給 express,然后 express 將其發送到瀏覽器。 這是您的應用程序結構:

 BROWSER ==> EXPRESS ==> GOOGLE-SERVER

瀏覽器會自動將 cookie 附加到發出 cookie 的服務器的每個請求中。 因此瀏覽器知道哪個 cookie 屬於哪個服務器,因此當它們向該服務器發出新請求時,它們會附加它。 但是在您的應用程序結構中,瀏覽器並未與 GOOGLE-SERVER 對話。 如果你沒有使用代理,你會通過express從GOOGLE-SERVER獲取cookie,但是由於你不是向GOOGLE-SERVER發出請求,所以cookie不會被使用,它不會被自動附加。 這就是使用 cookie 的要點,瀏覽器會自動附加 cookie。 通過設置代理,現在瀏覽器不知道GOOGLE-SERVER。 據它所知,它正在向快遞服務器發出請求。 所以每次瀏覽器使用相同的端口請求表達時,它都會附加cookie。 我希望這部分是清楚的。

現在 react 只與 express-server 通信。

  BROWSER ==> EXPRESS

由於 react 和 exress 不在同一個端口上,因此您會收到 cors 錯誤。

有2個解決方案。 1 正在使用cors包。

它的設置非常簡單

var express = require('express')
var cors = require('cors')
var app = express()
 
app.use(cors()) // use this before route handlers

第二種解決方案是在路由處理程序之前手動設置中間件

app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader(
    "Access-Control-Allow-Methods",
    "OPTIONS, GET, POST, PUT, PATCH, DELETE"
  );
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
  next(); // dont forget this
});

我在這里提出了同樣的問題,但無法使@Yazmin 提供的解決方案起作用。

我正在嘗試使用 Google 身份驗證和授權創建 React、Express/Nodejs、MongoDB 堆棧。 我目前正在 Windows 10 上開發堆棧,使用 Vs 代碼(在 'localhost:3000 上反應,在 localhost:5000 上使用 Nodejs,在 localhost:27017 上使用 MongoDB。

該應用程序的目的是使用谷歌地圖、谷歌照片 api 和谷歌 Gmail api 在地圖上顯示城市草圖(圖像)。 我將來可能也需要類似的 Facebook 群組訪問權限才能訪問 Urban Sketches。 但是現在我只包含了用於授權的配置文件和電子郵件范圍。

我想在后端保留對第三方資源的所有請求,因為在架構上我理解這是最佳實踐。

從 origin http://localhost:5000 發出的谷歌授權工作正常並返回預期結果。 但是,當我嘗試從客戶端執行相同操作時 - origin Http://localhost:3000 在第一次嘗試訪問 google auth2 api 后,開發人員工具控制台中返回以下錯誤。

訪問 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email%20https%3A %2F%2Fmail.google.com%2F&client_id='(從 'http://localhost:3000/auth/google' 重定向)來自 'http://localhost:3000' 的來源已被 CORS 策略阻止:No 'Access -Control-Allow-Origin' 標頭存在於請求的資源上。 如果不透明響應滿足您的需求,請將請求的模式設置為“no-cors”以在禁用 CORS 的情況下獲取資源。

無論我嘗試什么錯誤信息都是一樣的。

我認為谷歌將回復發送給客戶端(本地主機:3000)而不是服務器。

我知道錯誤是由瀏覽器在收到響應時引發的,其中方案、域和端口與預期的不匹配,並且這在生產中不是問題,因為客戶端代碼是從應用程序服務器加載的。

在其他解決方案中,我嘗試通過引用實現 Yilmaz 的解決方案:“在 client/src 中創建 setupProxy.js 文件。 無需在任何地方導入它。 create-react-app 將查找此目錄”我之前已經通過運行 create-react-app 創建了我的客戶端。 所以我在我的 src 文件夾中添加了 setupProxy.js。

問題:我認為我是正確的,在我重新啟動客戶端后,webpack 將包含包含我的設置的新 setupProxy.ts 文件。

在我看來,我得到的流程不是 BROWSER ==> EXPRESS ==> GOOGLE-SERVER 而是 BROWSER ==> EXPRESS ==> GOOGLE-SERVER ==>BROWSER,它會因 cors 錯誤而停止,如上所示。

為了測試這個理論,我在 client\\node_modules\\http-proxy-middleware\\lib\\index.js 函數“shouldProxy”和“middleware”中放置了一些控制台日志消息,但無法檢測到來自 auth/google 端點的任何活動來自 google 授權服務器響應 ( https://accounts.google.com/o/oauth2/v2/auth )。

所以我的理論是錯誤的,我不知道我將如何讓它發揮作用。

從 React 客戶端向 /auth/google 端點發出請求后,在 VsCode 終端上顯示的控制台日志消息如下...

http-proxy-middleware - 92 HttpProxyMiddleware - shouldProxy
  context [Function: context]
  req.url /auth/google
  req.originalUrl /auth/google
  Trace
      at shouldProxy (C:\Users\User\github\GiveMeHopev2\client\node_modules\http-proxy-middleware\lib\index.js:96:13)
      at middleware (C:\Users\User\github\GiveMeHopev2\client\node_modules\http-proxy-middleware\lib\index.js:49:9)
      at handle (C:\Users\User\github\GiveMeHopev2\client\node_modules\webpack-dev-server\lib\Server.js:322:18)
      at Layer.handle [as handle_request] (C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\layer.js:95:5)
      at trim_prefix (C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\index.js:317:13)
      at C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\index.js:284:7
      at Function.process_params (C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\index.js:335:12)
      at next (C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\index.js:275:10)
      at goNext (C:\Users\User\github\GiveMeHopev2\client\node_modules\webpack-dev-middleware\lib\middleware.js:28:16)
      at processRequest (C:\Users\User\github\GiveMeHopev2\client\node_modules\webpack-dev-middleware\lib\middleware.js:92:26)
http-proxy-middleware - 15 HttpProxyMiddleware - prepareProxyRequest
req localhost

這是我的 nodejs 服務器代碼列表。 我已經為我所做的一些不同嘗試留下了注釋掉的代碼。

import express from 'express'
import mongoose from 'mongoose'
//import cors from 'cors'
import dotenv from 'dotenv'
import cors, { CorsOptions } from 'cors'
import { exit } from 'process';
//imports index 02
import passport from 'passport'

import session from 'express-session' 
import MongoStore from 'connect-mongo'
import morgan from 'morgan' 
//Routes
import GiveMeHopeRouter from './routes/giveMeHope'
//import AuthRouter from './routes/auth'
import process from 'process';

//index 02
 
dotenv.config();
// express
const app = express();
// passport config
require ('./config/passport')(passport)

/*
// CORs
// !todo Access-Control-Allow-Credentials will not accept a boolean true in typescript
// ! hoping a string containing something is in fact true - just a guess. 
app.use('*', (req, res, next) => {
    res.header("Access-Control-Allow-Origin", "http://localhose:3000")
    res.header("Access-Control-Allow-Headers", "X-Requested-with")
    res.header("Access-Control-Allow-Headers", "Content-Type")
    res.header("Access-Control-Allow-Credentials", 'true' )
    next()
})
app.options('*' as any,cors() as any) ;

*/

// logging
if( process.env.NODE_ENV! !== 'production') {
    app.use(morgan('dev'))
}

const conn = process.env.MONGODB_LOCAL_URL!
/**
 * dbConnection and http port initialisation
 */

const dbConnnect = async (conn: string, port: number) => {

    try {
        let connected = false;
        await mongoose.connect(conn, { useNewUrlParser: true, useUnifiedTopology: true })
        app.listen(port, () => console.log(`listening on port ${port}`))
        return connected;
    } catch (error) {
        console.log(error)
        exit(1)
    }
}

const port = process.env.SERVERPORT as unknown as number
dbConnnect(conn, port)
//index 02
// Pre Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }))


const mongoStoreOptions = {
    mongoUrl: conn,
    collectionName: 'sessions'
}

app.use(
    session({
        secret: process.env.SESSIONKEY as string,
        resave: false,
        saveUninitialized: false,
        store: MongoStore.create(mongoStoreOptions),
    })
)

app.use(passport.initialize())
app.use(passport.session())

app.get('/', (req, res) => {
    res.send('Hello World');
})
// Authentication and Authorisation

const emailScope: string = process.env.GOOGLE_EMAIL_SCOPE as string
//GOOGLE_EMAIL_SCOPE=https://www.googleapis.com/auth/gmail/gmail.compose

const scopes = [
    'profile',
    'email',
    emailScope

].join(" ")




// Authenticate user
// !Start of CORs Testing options
/**
 * Cors Test 1 default
 
app.use(cors())
*/
/**
 * CORs Test 2 - Restrict to Origin
 */ 
const googleoptions2 = {
    origin: ['http://localhost:3000', 'https://accounts.google.com'],
    credentials: true,
}
app.use(cors(googleoptions2))

app.get('/auth/google', passport.authenticate('google', {
    scope: [
        'https://www.googleapis.com/auth/userinfo.profile',
        'https://www.googleapis.com/auth/userinfo.email'
    ]
}));



/**
 * CORs Test 3 - Restrict to Origin for multiple origins
 * failed - origin undefined
 

var whitelist = ['http://localhost:3000', 'https://accounts.google.com']
var googleoptions3 = {
  origin: function (origin: string, callback: (arg0: Error | null, arg1: boolean | undefined) => CorsOptions) {
      console.log('origin',origin)
    if (whitelist.indexOf(origin) !== -1) {
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'),undefined)!
    }
  }
}
*/



/**
 * Cors Test 4 with pre-request
 * Note: Tested with GoolgleStrategy Proxy set to true and false
 */
/*
const googleoptions4 = {
    origin: false,
    methods: ["GET"],
    credentials: true,
    maxAge: 3600
  };
app.options('/auth/google',cors(googleoptions4) as any)
*/

/**
 * Cors Test 5 set headers
 * Note: Tested with GoolgleStrategy Proxy set to true and false
 * 
 * see solution  https://stackoverflow.com/questions/59036377/cors-error-google-oauth-from-react-to-express-passportjs-validation
 * by https://stackoverflow.com/users/10262805/yilmaz
 * 
 * Create setupProxy.js file in client/src. No need to import this anywhere. create-react-app will look for this directory
 * ! Todo How to set up setupProxy.js in client folder src and then run create-react-app. create-react-app fails when trying to recreate client
 */
 

// Authentication callback
app.get('/auth/google/callback', passport.authenticate('google', { failureRedirect: '/'}),
    (req, res) => {
        res.redirect('/auth/google/log')
    }
)
// Refresh RefeshToken
app.get('/auth/google/refreshtoken', (req, res) => { 
    console.log(req.user);
    res.send('refreshToken')
})

/**
* Logout
* !!! With passport, req will have a logout method 
*/

app.get('/auth/login', (req, res) => {
  
    
   res.redirect('auth/google')
  
})

/**
 * Logout
 * !!! With passport, req will have a logout method 
 */

app.get('/logout', (req, res) => {
    req.logout()
    res.redirect('/')
})

app.get('/auth/google/log', (req,res) => {
    console.log('/google/log', req)
    res.send('Google Login Successful ')
})

app.use('/givemehope', GiveMeHopeRouter)


我的護照配置:

import { PassportStatic} from 'passport';
import {format, addDays} from 'date-fns'
import { IUserDB, IUserWithRefreshToken, ProfileWithJson} from '../interfaces/clientServer'

 
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const User = require('../models/User')



module.exports   = function (passport:PassportStatic) {

    const clientID: string = process.env.GOOGLE_CLIENTID as string
    const clientSecret: string = process.env.GOOGLE_SECRET as string
    const callbackURL: string = process.env.GOOGLE_AUTH_CALLBACK as string

    const strategy = new GoogleStrategy(
        {
            clientID: clientID,
            clientSecret: clientSecret,
            callbackURL: callbackURL,
            proxy: true
            
        },
        async (_accesstoken: string, _refreshtoken: string,
            profile: ProfileWithJson,
            done: (arg0: null, arg1: any) => void) => {
            console.log('accesstoken' , _accesstoken)
            console.log(',_refreshtoken', _refreshtoken)
              
            const newUser: IUserWithRefreshToken = {
                googleId: profile.id,
                displayName: profile.displayName,
                firstName: profile._json.given_name,
                lastName: profile._json.family_name,
                image: profile._json.picture ,
                email: profile._json.email,
                refreshToken: {
                    value: _refreshtoken || process.env.GOOGLE_REFRESH_TOKEN_INIT!,
                    refreshed: format(new Date(), 'yyyy-MM-dd'),
                    expires: format(addDays(new Date(), 5), 'yyyy-MM-dd') ,
                }
            }
            try {
                let user: IUserDB = await User.findOne({ googleId: profile.id })
                 
                if (user) {
                    // Update refresh token if it is has changed.
                    if (_refreshtoken) {
                        user = await User.update({refreshToken: newUser.refreshToken})
                        done(null, user)
                    }
                    done(null, user)
                } else {
                    user = await User.create(newUser)
                    done(null, user)
                }
                
            } catch (err) {
                console.log('******45 Passport.ts - error thrown ', err)
                done(null,err)
            }
            
        }
    )

    passport.use(strategy)
            
    passport.serializeUser(async (user: any, done) => done(null, user._id))
    passport.deserializeUser(async (_id, done) => done(null, await User.findOne({_id})))
     
}

客戶端 src 文件夾中的 SetupProxy.ts 文件:

import {proxy} from 'http-proxy-middleware'
module.exports = function(app: { use: (arg0: any) => void }) {
     
    
    app.use(proxy("auth/google",{target: "http://localhost:5000"} ))
    app.use(proxy("givemehope/**",{target: "http://localhost:5000"} ))
    app.use(proxy("/o/oauth2/v2/",{target: "http://localhost:5000"} ))
    
}

最后我的獲取請求:

 async function http(request: RequestInfo): Promise<any> {
    try {
      const response = await fetch(request,{credentials: 'include'}) 
      const body = await response.json();
      return body
    } catch (err) { console.log(`Err SignInGoogle`) }
  };


你不能對 /oauth/google 路由進行 axios 調用!
這是我的解決方案......代碼略有不同,但您會明白這個概念。

// step 1:
// onClick handler function of the button should use window.open instead 
// of axios or fetch
const loginHandler = () => window.open("http://[server:port]/auth/google", "_self")

//step 2: 
// on the server's redirect route add this successRedirect object with correct url. 
// Remember! it's your clients root url!!! 
router.get(
    '/google/redirect', 
    passport.authenticate('google',{
        successRedirect: "[your CLIENT root url/ example: http://localhost:3000]"
    })
)

// step 3:
// create a new server route that will send back the user info when called after the authentication 
// is completed. you can use a custom authenticate middleware to make sure that user has indeed 
// been authenticated
router.get('/getUser',authenticated, (req, res)=> res.send(req.user))

// here is an example of a custom authenticate express middleware 
const authenticated = (req,res,next)=>{
    const customError = new Error('you are not logged in');
    customError.statusCode = 401;
    (!req.user) ? next(customError) : next()
}
// step 4: 
// on your client's app.js component make the axios or fetch call to get the user from the 
// route that you have just created. This bit could be done many different ways... your call.
const [user, setUser] = useState()
useEffect(() => {
    axios.get('http://[server:port]/getUser',{withCredentials : true})
    .then(response => response.data && setUser(response.data) )
},[])

解釋....
第 1 步將在您的瀏覽器上加載您的服務器身份驗證 URL 並發出身份驗證請求。
步驟 2然后在身份驗證完成后在瀏覽器上重新加載客戶端 url。
第 3 步使 api 端點可用於收集用戶信息以更新反應狀態
第 4 步調用端點,獲取數據並更新用戶狀態。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM