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

为何使用passport与express-session时需解码会话Cookie?

问题:Next.js App Router与Express Session/Passport认证的Cookie解码问题

技术栈概述

  • 客户端:Next.js App Router
  • 服务端:Express + express-session(Redis存储)+ Passport

服务端核心配置代码

index.ts(主配置)

app.use(helmet());
app.use(limiter);
app.use(cors({ credentials: true, origin: "http://localhost:3000" }));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(
  session({
    store: redisStore,
    secret: process.env.REDIS_SECRET || "secret",
    resave: false,
    saveUninitialized: false,
    proxy: process.env.NODE_ENV === "development" ? false : true,
    name: "_session",
    cookie: {
      sameSite: process.env.NODE_ENV === "development" ? "none" : "none",
      secure: process.env.NODE_ENV === "development" ? false : true,
      maxAge: 1000 * 60 * 60 * 24 * 1,
      httpOnly: process.env.NODE_ENV === "development" ? false : true,
      path: "/",
      domain: process.env.NODE_ENV === "development" ? "localhost:3000" : "",
    },
  })
);

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

authMiddleware.ts(认证中间件)

export const authUser = (req: Request, res: Response, next: NextFunction) => {
  if (req.isAuthenticated()) {
    next();
    return;
  }

  return res.status(401).json({ success: false, message: "Unauthorized" });
};

Passport序列化/反序列化

passport.serializeUser<any, any>((user: User, done: any) => {
  const safeUser: SafeUser = {
    id: user.id,
    email: user.email,
    name: user.name,
    bio: user.bio,
    image: user.image,
    googleId: user.googleId,
    appleId: user.appleId,
  };

  done(null, safeUser);
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
passport.deserializeUser<any, any>((user: User, done: any) => {
  done(null, user);
});

路由示例

router.get('/search/:search/', authUser, async (req: Request, res: Response, next: NextFunction) => {
 const userData = req.user as SafeUser;
 const email = userData.email
 try {
  const categories = await findMatchedCategory(req.params.search, email)
  res.json(categories);
 } catch (error) {
  next(error);
 }
})

问题现象

  • Postman测试所有接口正常,req.isAuthenticated()能正确返回true
  • Next.js中使用服务端动作或客户端fetch调用时出现认证失败:
    1. 服务端动作获取用户:必须手动对_session Cookie值执行decodeURIComponent才能解析出有效会话ID,否则无法从Redis获取会话
      export const getUser = async () => {
       const userCookie = cookies().get("_session");
       const rawSession: any = userCookie?.value;
       const decodeRawSession = decodeURIComponent(rawSession);
      
       if (!rawSession) return [];
       const sessionId = cookieParser.signedCookie(decodeRawSession, String(process.env.REDIS_SECRET) || "secret");
       const session = await redis.get(`shub:${sessionId}`);
       if (!session) return [];
       const user = (session as any).passport.user;
       console.log('user from action', user)
       return user;
      }
      
    2. 客户端fetch请求:请求头已携带Cookie,但服务端未手动解码时,authUser中间件中req.isAuthenticated()始终返回false
      const followedCategories = await fetch(`http://localhost:3005/categories/search/${search}`, {
              method: 'GET',
              credentials: 'include',
              headers: {
                'Content-Type': 'application/json',
              },
            })
            const data = followedCategories ? await followedCategories.json() : [];
      

当前临时解决方案

在服务端添加中间件手动解码_session Cookie:

app.use((req, res, next) => {
  const { headers: { cookie } } = req;
  if (cookie) {
    const allCookies = cookie.split(';').map(c => {
      if (!c.startsWith('_session')) return c;
      return `_session=${decodeURIComponent(c.split('=')[1])}`
    });
    req.headers.cookie = allCookies.join('; ');
    next();
  }
  else {
    next();
  }
})

该方案可行,但属于临时hack。曾尝试在session配置的cookie中添加encode: (v) => v,但无效。

疑问

想知道出现该问题的根本原因,或者是我的配置中存在哪些错误?

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 08:15:08