express-session 在设置 cookie 时会导致服务器崩溃?

Posted

技术标签:

【中文标题】express-session 在设置 cookie 时会导致服务器崩溃?【英文标题】:express-session crashes server on setting cookie? 【发布时间】:2022-01-12 21:53:55 【问题描述】:

基本上这是另一个帖子Express-session does not set cookie?,我正在关注 Ben Awad 的全栈教程。 cookie 被创建,但服务器崩溃,这是错误

node:internal/errors:464
ErrorCaptureStackTrace(err);
^

TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Array
    at new NodeError (node:internal/errors:371:5)
    at _write (node:internal/streams/writable:312:13)
    at Socket.Writable.write (node:internal/streams/writable:334:10)
    at RedisSocket.writeCommand (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/@node-redis/client/dist/lib/client/socket.js:57:130)
    at Commander._RedisClient_tick (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/@node-redis/client/dist/lib/client/index.js:415:64)
    at Commander._RedisClient_sendCommand (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/@node-redis/client/dist/lib/client/index.js:396:82)
    at Commander.commandsExecutor (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/@node-redis/client/dist/lib/client/index.js:160:154)
    at Commander.BaseClass.<computed> [as set] (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/@node-redis/client/dist/lib/commander.js:8:29)
    at RedisStore.set (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/connect-redis/lib/connect-redis.js:65:21)
    at Session.save (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/express-session/session/session.js:72:25)
    at Session.save (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/express-session/index.js:406:15)
    at ServerResponse.end (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/express-session/index.js:335:21)
    at ServerResponse.send (/home/kuratar/github/milestone-4-Kuratar/server/node_modules/express/lib/response.js:221:10)
    at /home/kuratar/github/milestone-4-Kuratar/server/node_modules/apollo-server-express/dist/ApolloServer.js:89:25 
  code: 'ERR_INVALID_ARG_TYPE'

我注意到 user.ts 中有这行特定的代码:

req.session.userId = user.id

当它被注释掉时,错误不会发生,但 cookie 没有设置。响应标头中没有 set-cookie 选项。 我的文件与我链接的帖子中的其他人几乎相同。

index.ts

import "reflect-metadata";
import  MikroORM  from "@mikro-orm/core";
import  __prod__  from "./constants";
import microConfig from "./mikro-orm.config";
import express from "express";
import  ApolloServer  from "apollo-server-express";
import  ApolloServerPluginLandingPageGraphQLPlayground  from "apollo-server-core";
import  buildSchema  from "type-graphql";
import  HelloResolver  from "./resolvers/hello";
import  PostResolver  from "./resolvers/post";
import  UserResolver  from "./resolvers/user";
import * as redis from "redis";
import session from "express-session";
import connectRedis from "connect-redis";
import  MyContext  from "./types";

// start postgresql server on wsl - sudo service postgresql start
//    stop - sudo service postgresql stop
// start redis server on wsl - redis-server
//    sudo /etc/init.d/redis-server restart
//    stop, start
// watch ts changes - npm run watch
// run server - npm run dev

const main = async () => 
  const orm = await MikroORM.init(microConfig); // initialize database
  await orm.getMigrator().up(); // run migrations before anything else

  const app = express();
  app.set("trust proxy", 1); // trust first proxy

  // this comes before applyMiddleware since use session middleware inside apollo
  const RedisStore = connectRedis(session);
  const redisClient = redis.createClient(); // TODO: TypeError: Cannot read properties of undefined (reading 'createClient')
  redisClient.on("error", (err) => console.log("Redis Client Error", err));
  await redisClient.connect();
  app.use(
    session(
      name: "qid",
      // touch - make request to redis to reset the user's session
      //    if user does something, it means they are active and should reset the timer of automatically logging them out
      //    after 24 hours for example
      // disableTouch: true - keep session forever, can change this later to timed sessions
      store: new RedisStore( client: redisClient, disableTouch: true ), // tell express session using redis
      cookie: 
        maxAge: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years
        httpOnly: true,
        sameSite: "lax", // csrf
        secure: __prod__, // only works in https
      ,
      saveUninitialized: false,
      secret: "askljdhfjkalshdjlf", // want to keep this secret separately
      resave: true,
      rolling: true,
    )
  );

  // app.use(function (req, res, next) 
  //   res.header(
  //     "Access-Control-Allow-Origin",
  //     "https://studio.apollographql.com"
  //   );
  //   res.header("Access-Control-Allow-Credentials", "true");
  //   next();
  // );

  const apolloServer = new ApolloServer(
    schema: await buildSchema(
      resolvers: [HelloResolver, PostResolver, UserResolver],
      validate: false,
    ),
    // object that is accessible by resolvers, basically pass the database itself
    context: ( req, res ): MyContext => ( em: orm.em, req, res ),
    plugins: [
      ApolloServerPluginLandingPageGraphQLPlayground(
        settings:  "request.credentials": "include" ,
      ),
    ],
  );

  await apolloServer.start();
  const corsOptions = 
    origin: new RegExp("/*/"),
    credentials: true,
  ;
  apolloServer.applyMiddleware( app, cors: corsOptions ); // create graphql endpoint on express
  app.listen(4000, () => 
    console.log("Server started on localhost:4000");
  );
;

main().catch((error) => 
  console.log("----------MAIN CATCHED ERROR----------");
  console.error(error);
  console.log("-----------------END------------------");
);

user.ts

import 
  Resolver,
  Arg,
  Mutation,
  InputType,
  Field,
  Ctx,
  ObjectType,
 from "type-graphql";
import  User  from "../entities/User";
import  MyContext  from "../types";
import argon2 from "argon2";

// another way to implementing arguments for methods instead of @Arg()
@InputType()
class UsernamePasswordInput 
  @Field()
  username: string;
  @Field()
  password: string;


@ObjectType()
class FieldError 
  @Field()
  field: string;
  @Field()
  message: string;


@ObjectType()
class UserResponse 
  @Field(() => [FieldError],  nullable: true )
  errors?: FieldError[];
  @Field(() => User,  nullable: true )
  user?: User;


@Resolver()
export class UserResolver 
  @Mutation(() => UserResponse)
  async register(
    @Arg("options") options: UsernamePasswordInput,
    @Ctx()  em : MyContext
  ): Promise<UserResponse> 
    if (options.username.length <= 2) 
      return 
        errors: [
           field: "username", message: "length must be greater than 2" ,
        ],
      ;
    
    if (options.password.length <= 2) 
      return 
        errors: [
           field: "password", message: "length must be greater than 2" ,
        ],
      ;
    
    // argon2 is a password hasher package
    const hashedPassword = await argon2.hash(options.password);
    const user = em.create(User, 
      username: options.username,
      password: hashedPassword,
    );
    try 
      await em.persistAndFlush(user);
     catch (error) 
      // duplicate username error
      if (error.code === "23505") 
        // || error.detail.includes("already exists")
        return 
          errors: [ field: "username", message: "Username already taken" ],
        ;
      
    

    // return user in an object since response is now a response object - UserResponse
    return  user ;
  

  @Mutation(() => UserResponse)
  async login(
    @Arg("options") options: UsernamePasswordInput,
    @Ctx()  em, req : MyContext
  ): Promise<UserResponse> 
    // argon2 is a password hasher package
    const user = await em.findOne(User, 
      username: options.username,
    );
    // can give same field error message like invalid login
    if (!user) 
      return 
        errors: [ field: "username", message: "That username doesn't exist" ],
      ;
    
    const valid = await argon2.verify(user.password, options.password);
    if (!valid) 
      return 
        errors: [ field: "password", message: "Incorrect password" ],
      ;
    
    // mutation 
    //   login(options: username: "eric", password: "eric") 
    //     errors 
    //       field
    //       message
    //     
    //     user 
    //       id
    //       username
    //     
    //   
    // 
    console.log(req.session)
    console.log(user.id)
    req.session.userId = user.id
    console.log(req.session)
    console.log(req.session.id)
    // console.log(req.session.userId)

    // return user in an object since response is now a response object - UserResponse
    return  user ;
  

types.ts

import  EntityManager, IDatabaseDriver, Connection  from "@mikro-orm/core";
import  Request, Response  from "express";
import  Session, SessionData  from "express-session";

// this is the type of orm.em from index.ts
// extracted to make code look cleaner in post.ts
export type MyContext = 
  em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>;
  req: Request & 
    session: Session & Partial<SessionData> &  userId?: number ;
  ;
  res: Response;
;

【问题讨论】:

【参考方案1】:

为了更具体地了解 Bernardo,Ben 还在 github 存储库中将其更改为 ioredis。所以需要安装ioredis并添加这些行

import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);

并删除/注释掉旧的 redisClient 代码行。

【讨论】:

【参考方案2】:

我遇到了同样的错误。在我的情况下,我可以通过将 redis 客户端更改为 ioredis(我使用的是 redis)来修复它。

【讨论】:

正如目前所写,您的答案尚不清楚。请edit 添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。你可以找到更多关于如何写好答案的信息in the help center。

以上是关于express-session 在设置 cookie 时会导致服务器崩溃?的主要内容,如果未能解决你的问题,请参考以下文章

如何通过express-session在node.js上设置持久性cookie?

express-session整理

什么时候应该将 cookie-parser 与 express-session 一起使用?

express-session 和 cookie-session 有啥区别?

在带有 express-session 的 NodeJS 中使用安全 cookie 时会话不持久

Node.js、Angular、express-session:由于 cookie 策略(sameSite cookie),Chrome 80 不保存会话