You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Passport.js+Google登录:未认证自动重定向触发CORS错误求助

问题:未认证时后端重定向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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 02:39:56