Преглед изворни кода

docs(core): Add example Redis-based SessionCacheStrategy

Michael Bromley пре 3 година
родитељ
комит
a65557f0c1

+ 1 - 1
docs/content/developer-guide/deployment.md

@@ -58,7 +58,7 @@ For a production Vendure server, there are a few security-related points to cons
 Vendure supports running in a serverless or multi-instance (horizontally scaled) environment. The key consideration in configuring Vendure for this scenario is to ensure that any persistent state is managed externally from the Node process, and is shared by all instances. Namely:
 
 * The JobQueue should be stored externally using the [DefaultJobQueuePlugin]({{< relref "default-job-queue-plugin" >}}) (which stores jobs in the database) or the [BullMQJobQueuePlugin]({{< relref "bull-mqjob-queue-plugin" >}}) (which stores jobs in Redis), or some other custom JobQueueStrategy.
-* A custom [SessionCacheStrategy]({{< relref "session-cache-strategy" >}}) must be used which stores the session cache externally (such as in the database or Redis), since the default strategy stores the cache in-memory and will cause inconsistencies in multi-instance setups.
+* A custom [SessionCacheStrategy]({{< relref "session-cache-strategy" >}}) must be used which stores the session cache externally (such as in the database or Redis), since the default strategy stores the cache in-memory and will cause inconsistencies in multi-instance setups. [Example Redis-based SessionCacheStrategy]({{< relref "session-cache-strategy" >}})
 * When using cookies to manage sessions, make sure all instances are using the _same_ cookie secret:
     ```TypeScript
     const config: VendureConfig = {

+ 75 - 1
packages/core/src/config/session-cache/session-cache-strategy.ts

@@ -47,9 +47,83 @@ export type CachedSession = {
  * @description
  * This strategy defines how sessions get cached. Since most requests will need the Session
  * object for permissions data, it can become a bottleneck to go to the database and do a multi-join
- * SQL query each time. Therefore we cache the session data only perform the SQL query once and upon
+ * SQL query each time. Therefore, we cache the session data only perform the SQL query once and upon
  * invalidation of the cache.
  *
+ * The Vendure default is to use a the {@link InMemorySessionCacheStrategy}, which is fast and suitable for
+ * single-instance deployments. However, for multi-instance deployments (horizontally scaled, serverless etc.),
+ * you will need to define a custom strategy that stores the session cache in a shared data store, such as in the
+ * DB or in Redis.
+ *
+ * Here's an example implementation using Redis. To use this, you need to add the
+ * [ioredis package](https://www.npmjs.com/package/ioredis) as a dependency.
+ *
+ * @example
+ * ```TypeScript
+ * import { CachedSession, Logger, SessionCacheStrategy, VendurePlugin } from '\@vendure/core';
+ * import IORedis from 'ioredis';
+ *
+ * export interface RedisSessionCachePluginOptions {
+ *   namespace?: string;
+ *   redisOptions?: IORedis.RedisOptions;
+ * }
+ * const loggerCtx = 'RedisSessionCacheStrategy';
+ * const DEFAULT_NAMESPACE = 'vendure-session-cache';
+ *
+ * export class RedisSessionCacheStrategy implements SessionCacheStrategy {
+ *   private client: IORedis.Redis;
+ *   constructor(private options: RedisSessionCachePluginOptions) {}
+ *
+ *   init() {
+ *     this.client = new IORedis(this.options.redisOptions);
+ *     this.client.on('error', err => Logger.error(err.message, loggerCtx, err.stack));
+ *   }
+ *
+ *   async get(sessionToken: string): Promise<CachedSession | undefined> {
+ *     const retrieved = await this.client.get(this.namespace(sessionToken));
+ *     if (retrieved) {
+ *       try {
+ *         return JSON.parse(retrieved);
+ *       } catch (e) {
+ *         Logger.error(`Could not parse cached session data: ${e.message}`, loggerCtx);
+ *       }
+ *     }
+ *   }
+ *
+ *   async set(session: CachedSession) {
+ *     await this.client.set(this.namespace(session.token), JSON.stringify(session));
+ *   }
+ *
+ *   async delete(sessionToken: string) {
+ *     await this.client.del(this.namespace(sessionToken));
+ *   }
+ *
+ *   clear() {
+ *     // not implemented
+ *   }
+ *
+ *   private namespace(key: string) {
+ *     return `${this.options.namespace ?? DEFAULT_NAMESPACE}:${key}`;
+ *   }
+ * }
+ *
+ * \@VendurePlugin({
+ *   configuration: config => {
+ *     config.authOptions.sessionCacheStrategy = new RedisSessionCacheStrategy(
+ *       RedisSessionCachePlugin.options,
+ *     );
+ *     return config;
+ *   },
+ * })
+ * export class RedisSessionCachePlugin {
+ *   static options: RedisSessionCachePluginOptions;
+ *   static init(options: RedisSessionCachePluginOptions) {
+ *     this.options = options;
+ *     return this;
+ *   }
+ * }
+ * ```
+ *
  * @docsCategory auth
  * @docsPage SessionCacheStrategy
  * @docsWeight 0

+ 64 - 0
packages/dev-server/test-plugins/redis-session-cache-plugin.ts

@@ -0,0 +1,64 @@
+import { CachedSession, Logger, SessionCacheStrategy, VendurePlugin } from '@vendure/core';
+import IORedis from 'ioredis';
+
+const loggerCtx = 'RedisSessionCacheStrategy';
+const DEFAULT_NAMESPACE = 'vendure-session-cache';
+
+export class RedisSessionCacheStrategy implements SessionCacheStrategy {
+    private client: IORedis.Redis;
+
+    constructor(private options: RedisSessionCachePluginOptions) {}
+
+    init() {
+        this.client = new IORedis(this.options.redisOptions);
+        this.client.on('error', err => Logger.error(err.message, loggerCtx, err.stack));
+    }
+
+    async get(sessionToken: string): Promise<CachedSession | undefined> {
+        const retrieved = await this.client.get(this.namespace(sessionToken));
+        if (retrieved) {
+            try {
+                return JSON.parse(retrieved);
+            } catch (e) {
+                Logger.error(`Could not parse cached session data: ${e.message}`, loggerCtx);
+            }
+        }
+    }
+
+    async set(session: CachedSession) {
+        await this.client.set(this.namespace(session.token), JSON.stringify(session));
+    }
+
+    async delete(sessionToken: string) {
+        await this.client.del(this.namespace(sessionToken));
+    }
+
+    clear() {
+        // not implemented
+    }
+
+    private namespace(key: string) {
+        return `${this.options.namespace ?? DEFAULT_NAMESPACE}:${key}`;
+    }
+}
+
+export interface RedisSessionCachePluginOptions {
+    namespace?: string;
+    redisOptions?: IORedis.RedisOptions;
+}
+
+@VendurePlugin({
+    configuration: config => {
+        config.authOptions.sessionCacheStrategy = new RedisSessionCacheStrategy(
+            RedisSessionCachePlugin.options,
+        );
+        return config;
+    },
+})
+export class RedisSessionCachePlugin {
+    static options: RedisSessionCachePluginOptions;
+    static init(options: RedisSessionCachePluginOptions) {
+        this.options = options;
+        return this;
+    }
+}