Passport.js+Google登录:未认证自动重定向触发CORS错误求助
我用passport.js实现了Web应用的Google账号登录,点击登录按钮能正常跳转到Google账号选择页完成登录。现在想实现:用户未认证时自动重定向到登录页,前端React向Node.js后端发GET/POST请求时,未登录状态下要先跳转到Google登录页,再执行后续操作。
但未认证时,后端尝试重定向到/auth/google触发Google认证,Chrome报错:
Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3500%2Fauth%2Fgoogle%2Fcallback&scope=profile&client_id=734494171389-f8kk3r9n7e3crt61ilpvmal20uku9qq4.apps.googleusercontent.com' (redirected from 'http://localhost:3500/repositories') from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
奇怪的是,点击登录按钮调用相同的Google URL能正常工作,自动重定向时却出CORS错误,求分析原因。
server.ts
... const app: Express = express() app.use( session({ secret: [process.env.COOKIE_SECRET!], cookie: { secure: process.env.NODE_ENV === "production" ? true : false, sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", maxAge: 30 * 24 * 60 * 60 * 1000, }, resave: false, saveUninitialized: false, }) ) app.use(cookieParser()); app.use(passport.initialize()); app.use(passport.session()); app.use(cors({ methods: "GET,HEAD,PUT,PATCH,POST,DELETE", credentials: true, origin: process.env.CLIENT_URL! })) app.use("/auth", authRouter); const ensureAuthenticated = (req: any, res: any, next: any) => { if (req.isAuthenticated()) { logger.info(`It's authenticated.`) return next() } else { logger.info(`It is not authenticated, redirecting to '/auth/google'.`) res.redirect('/auth/google') } } app.all('*', function(req,res,next) { if (req.path === '/' || req.path.startsWith('/auth')) next(); else ensureAuthenticated(req,res,next); }); // routes app.use('/extractions', extractionRouter) app.use('/prog-langs', progLangRouter) app.use('/repositories', repositoryRouter) app.use('/skills', skillRouter) app.use('/developers', developerRouter) app.listen(PORT, async () => { await initDB() logger.info(`Server running on port ${PORT}`) })
auth.ts
... passport.use(new GoogleStrategy( { clientID: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, callbackURL: 'http://localhost:3500/auth/google/callback', }, async(accessToken, refreshToken, profile, done) => { const userData = profile._json; let user = {}; try { const currentUserQuery = await AuthUserModel.findByPk(userData.sub) if (currentUserQuery) { user = { user_id: currentUserQuery.id, username: currentUserQuery.name, } } else { const newAuthUser: AuthUserModel = await AuthUserModel.create({ id: userData.sub, name: userData.name }) user = { user_id: newAuthUser.id, username: newAuthUser.name } } done(null, user); } catch (error: any) { done(error, false, error.message) } } )) passport.serializeUser((user, done) => { done(null, user); }); passport.deserializeUser((user, done) => { logger.info(`deserializeUser: ${JSON.stringify(user)}`) done(null, user!) }) const authRouter = express.Router() authRouter.get("/google", passport.authenticate("google", { scope: "profile", }) ) authRouter.get( "/google/callback", passport.authenticate( "google", { successRedirect: process.env.CLIENT_URL!, failureRedirect: "/auth/login/failed" } ) ) authRouter.get("/login/success", (req, res) => { if (req.user) { res.json({ success: true, message: "user has successfully authenticated", user: req.user, cookies: req.cookies }); } }); // when login failed, send failed msg authRouter.get("/login/failed", (req, res) => { res.status(401).json({ success: false, message: "user failed to authenticate." }); }); // When logout, redirect to client authRouter.post("/logout", (req, res, next) => { req.logout(function(err) { if (err) { return next(err); } res.redirect(process.env.CLIENT_URL!); }) }); export default authRouter
前端登录按钮代码
export default function Login() { const handleOAuth = () => { window.open(`${endpointBackEnd}/auth/google`, "_self"); }; return ( <span> <button onClick={handleOAuth}> Login with Google </button> </span> ) }
核心原因
两种触发方式的请求类型完全不同:
- 点击登录按钮:通过
window.open发起的是浏览器顶层导航请求,浏览器会直接处理跳转,不会受CORS限制(CORS只针对XMLHttpRequest/Fetch这类跨域AJAX请求)。 - 未认证时后端重定向:前端发起的是AJAX请求(比如Fetch/axios调用
/repositories),后端返回302重定向后,浏览器会自动用AJAX请求去访问Google的认证URL,而Google的服务器不会给你的前端域名(localhost:5173)设置Access-Control-Allow-Origin头,所以触发CORS错误。
解决方案
不能在AJAX请求的响应里直接返回重定向,而是要让前端主动处理未认证的情况:
1. 修改后端ensureAuthenticated逻辑
当检测到未认证且是AJAX请求时,返回401状态码,而不是重定向:
const ensureAuthenticated = (req: any, res: any, next: any) => { if (req.isAuthenticated()) { logger.info(`It's authenticated.`) return next() } // 判断是否是AJAX请求(通过X-Requested-With头,前端请求时需要设置) if (req.xhr || req.headers['x-requested-with'] === 'XMLHttpRequest') { return res.status(401).json({ message: '未认证,请登录' }); } else { logger.info(`It is not authenticated, redirecting to '/auth/google'.`) res.redirect('/auth/google') } }
2. 前端全局拦截请求
在前端的请求拦截器(比如axios的interceptors)里,捕获401响应,然后主动跳转到后端的/auth/google地址:
// 以axios为例 axios.interceptors.response.use( response => response, error => { if (error.response?.status === 401) { // 跳转到后端登录地址,让浏览器处理顶层导航 window.location.href = `${endpointBackEnd}/auth/google`; } return Promise.reject(error); } );
3. 前端请求时添加标识头
确保前端发起请求时带上X-Requested-With头,让后端识别AJAX请求:
// axios全局配置 axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
这样处理后,未认证时AJAX请求会收到401,前端主动触发浏览器顶层导航到Google登录页,就不会有CORS问题了,和点击登录按钮的逻辑一致。
内容的提问来源于stack exchange,提问作者Viktor

