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

PassportJS Google登录后设置HttpOnly Cookie并重定向报错求助

PassportJS Google策略中同时设置HttpOnly Cookie和重定向的问题解决

问题现象

使用PassportJS的Google OAuth2策略时,需要在服务端设置HttpOnly类型的Refresh Token Cookie,之后重定向到客户端URL,但无法同时完成这两个操作,抛出错误:

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at new NodeError (node:internal/errors:371:5)
at ServerResponse.setHeader (node:_http_outgoing:576:11)
at ServerResponse.header (H:\1HF_solution\location\server\node_modules\express\lib\response.js:794:10)
at ServerResponse.location (H:\1HF_solution\location\server\node_modules\express\lib\response.js:915:15)
at ServerResponse.redirect (H:\1HF_solution\location\server\node_modules\express\lib\response.js:953:18)

当前仅能单独执行设置Cookie或重定向操作,无法同时实现两者。

相关代码

google-strategy.ts

import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { IGoogleDto } from '../interfaces/user-interface';
import userService, { IServerData } from '../service/user-service';

passport.use(
  new GoogleStrategy(
    {
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: '/api/oauth/google/callback',
    },
    async function (
      accessToken: any,
      refreshToken: any,
      profile: { _json: IGoogleDto },
      done: (arg0: any, arg1: boolean | IServerData | { registrationToken: string }, arg2: undefined) => void
    ) {
      try {
        const googleDto: IGoogleDto = profile._json;
        const result = await userService.findOrCreateForGoogle(googleDto);
        // result: {refreshToken:'',userDto:'user data object'}
        done(null, result, null);
      } catch (err) {
        done(err, null, null);
      }
    }
  )
);

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

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

oauth-router.ts

import { Router } from 'express';
import passport from 'passport';
import { IRTRequest } from '../interfaces/user-interface';
import { IServerData } from '../service/user-service';
require('../strategy/google-strategy');

const router = Router();
const CLIENT_URL = process.env.CLIENT_URL;

router.get('/logout', (req, res, next) => {
  req.logout(function (err) {
    if (err) {
      next(err);
    }
  });
  res.clearCookie('refreshToken');
  res.redirect(CLIENT_URL);
});

router.get('/google/success', (req: IRTRequest, res, next) => {
  if (req.user) {
    res.status(200).json({
      success: true,
      message: 'successfully',
      user: req.user,
    });

    res.cookie('refreshToken', req.user.refreshToken, {
      maxAge: 30 * 24 * 60 * 60 * 1000,
      httpOnly: true,
    });
  }
});

router.get('/google/failed', (req, res) => {
  res.status(401).json({
    success: false,
    message: 'failure',
  });
});

router.get(
  '/google/callback',
  passport.authenticate('google', {
    successRedirect: 'success',
    failureRedirect: 'failed',
  })
);

router.get('/google', passport.authenticate('google', { scope: ['email', 'profile'] }));

客户端调用代码

const GoogleHandler = () => {
  window.open(process.env.REACT_APP_GOOGLE_OAUTH_URL, '_self');
};

尝试过的修改(未解决)

router.get('/google/callback', async (req: IRTRequest, res, next) => {
  return await passport.authenticate(
    'google',
    {
      successRedirect: 'login/success',
      failureRedirect: 'failed',
    },
    async (err, data: IServerData) => {
      // set cookie here and redirect but i have error
    }
  )(req, res);
});

错误原因

  1. 响应顺序错误:原/google/success路由中,先调用res.status(200).json(...)发送完整响应(包含响应头和体),之后再执行res.cookie(...)时,响应已完成,无法再修改响应头(Cookie属于响应头字段),触发报错。
  2. 选项冲突:修改回调路由时,同时使用successRedirect选项和自定义回调函数。Passport的authenticate中间件会优先执行successRedirect发送重定向响应,后续自定义回调中再操作响应会导致重复发送头部。

解决方案

方案1:自定义回调中处理Cookie与重定向(推荐)

放弃successRedirect和failureRedirect选项,使用Passport自定义回调函数,手动控制Cookie设置和重定向流程:

修改oauth-router.ts中的/google/callback路由:

router.get('/google/callback', (req: IRTRequest, res, next) => {
  passport.authenticate('google', async (err, user: IServerData) => {
    // 处理认证错误
    if (err) return next(err);
    // 认证失败,跳转到失败页
    if (!user) return res.redirect('/api/oauth/google/failed');

    // 设置HttpOnly Refresh Token Cookie
    res.cookie('refreshToken', user.refreshToken, {
      maxAge: 30 * 24 * 60 * 60 * 1000, // 30天有效期
      httpOnly: true, // 禁止前端JS读取,防XSS
      secure: process.env.NODE_ENV === 'production', // 生产环境仅HTTPS传输
      sameSite: 'strict' // 防CSRF,根据需求可选'lax'
    });

    // 重定向到客户端应用
    res.redirect(CLIENT_URL);
  })(req, res, next);
});

同时删除原/google/success路由(因为直接重定向到客户端,无需返回JSON),/google/failed路由可保留用于认证失败场景。

方案2:调整响应顺序(适用于需返回JSON的场景)

如果业务需要在认证成功时返回JSON响应(如AJAX请求场景),必须先设置Cookie再发送响应:

修改/google/success路由:

router.get('/google/success', (req: IRTRequest, res, next) => {
  if (req.user) {
    // 先设置Cookie
    res.cookie('refreshToken', req.user.refreshToken, {
      maxAge: 30 * 24 * 60 * 60 * 1000,
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict'
    });
    // 再发送JSON响应
    res.status(200).json({
      success: true,
      message: 'successfully',
      user: req.user,
    });
  } else {
    res.redirect('/api/oauth/google/failed');
  }
});

注意:当前客户端是通过window.open直接跳转,此方案会让浏览器显示JSON内容而非回到客户端应用,因此方案1更符合需求。

补充说明

  • HttpOnly Cookie是安全设计,避免Refresh Token被XSS攻击窃取,不可通过前端JS访问。
  • 生产环境务必开启secure: true,确保Cookie仅通过HTTPS协议传输。
  • sameSite属性可根据业务需求选择strict或lax,降低CSRF攻击风险。

内容的提问来源于stack exchange,提问作者Nikita Kruglyakov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 08:40:24