npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xllentai/sso-server

v0.1.18

Published

Xllent SSO Server SDK for NestJS 应用

Readme

@xllentai/sso-server

Xllent 平台服务端 SSO SDK,为 NestJS 应用提供 Keycloak 单点登录集成能力。

npm version npm downloads TypeScript License

✨ 特性

  • 🚀 开箱即用 - NestJS 依赖注入集成,约 60 行配置
  • 🔐 多存储方案 - 支持 JWT / Redis / Database 三种 Session 存储
  • 🔒 安全可靠 - JWT 签名验证、Cookie 安全配置、Session 吊销
  • 🎯 TypeScript 优先 - 完整的类型定义,智能提示友好
  • 灵活扩展 - 接口抽象,支持自定义存储实现
  • 🧪 测试完备 - 三种存储方案单元测试覆盖

📦 安装

# npm
npm install @xllentai/sso-server

# pnpm
pnpm add @xllentai/sso-server

# yarn
yarn add @xllentai/sso-server

Peer Dependencies

# NestJS 核心依赖
npm install @nestjs/common @nestjs/core @nestjs/jwt

# 可选依赖(根据需要安装)
npm install ioredis    # 使用 Redis 存储时
npm install @prisma/client  # 使用 Database 存储时

🚀 快速开始

1. 配置环境变量

# .env
USER_SERVICE_BASE_URL=http://localhost:3000
JWT_SECRET=your-super-secret-key-at-least-32-characters
NODE_ENV=development

2. 在 NestJS Module 中注册

// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { XllentSSOServer } from '@xllentai/sso-server';

@Module({
  imports: [ConfigModule],
  providers: [
    {
      provide: 'SSO_SERVER',
      useFactory: (configService: ConfigService) => {
        return new XllentSSOServer({
          userServiceUrl: configService.get<string>('USER_SERVICE_BASE_URL')!,
          jwtSecret: configService.get<string>('JWT_SECRET')!,
          session: {
            issuer: 'xllent',
            audience: 'xllent-portal',
            expiresInSeconds: 7 * 24 * 60 * 60, // 7天
          },
          sessionStore: { type: 'jwt' }, // 开发环境使用JWT
        });
      },
      inject: [ConfigService],
    },
  ],
  exports: ['SSO_SERVER'],
})
export class AuthModule {}

3. 在 Controller 中使用

// auth/auth.controller.ts
import { Controller, Get, Post, Inject, Query, Req, Res, UnauthorizedException } from '@nestjs/common';
import { Request, Response } from 'express';
import { XllentSSOServer } from '@xllentai/sso-server';

@Controller('auth')
export class AuthController {
  constructor(
    @Inject('SSO_SERVER')
    private readonly ssoServer: XllentSSOServer,
  ) {}

  @Get('callback')
  async handleCallback(
    @Query('temp_token') tempToken: string,
    @Res({ passthrough: true }) res: Response,
  ) {
    const session = await this.ssoServer.createSessionFromTempToken(tempToken);

    res.cookie(session.cookieOptions.name, session.sessionJwt, {
      ...session.cookieOptions,
      httpOnly: true,
    });

    if (session.refreshToken) {
      res.cookie('x-refresh-token', session.refreshToken, {
        httpOnly: true,
        secure: session.cookieOptions.secure,
        sameSite: 'strict',
        path: '/',
        maxAge: 30 * 24 * 60 * 60 * 1000,
      });
    }

    return res.redirect('https://portal.example.com');
  }

  @Get('me')
  async getCurrentUser(@Req() req: Request) {
    const sessionJwt = req.cookies['x-session-id'];
    if (!sessionJwt) {
      throw new UnauthorizedException('未登录');
    }

    const session = await this.ssoServer.verifySession(sessionJwt);
    return {
      id: session.claims.sub,
      username: session.claims.username,
      email: session.claims.email,
    };
  }

  @Post('logout')
  async logout(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
    const sessionJwt = req.cookies['x-session-id'];
    if (sessionJwt) {
      await this.ssoServer.revokeSession(sessionJwt);
    }

    // 清除 Cookie
    res.clearCookie('x-session-id');

    return { success: true };
  }
}

📖 核心功能

Session 创建

createSessionFromTempToken(tempToken, options?): Promise<CreateSessionResult>

兑换临时 Token,创建平台 Session。

const session = await ssoServer.createSessionFromTempToken(
  'temp-abc123',
  { traceId: 'request-uuid' } // 可选:链路追踪ID
);

console.log('Session JWT:', session.sessionJwt);
console.log('Session ID:', session.sessionId);
console.log('用户信息:', session.userInfo);
console.log('租户ID:', session.tenantId);
console.log('Cookie配置:', session.cookieOptions);

返回值 CreateSessionResult:

interface CreateSessionResult {
  sessionJwt: string;                    // Session JWT(存储到Cookie)
  sessionId: string;                     // Session ID (jti)
  cookieOptions: SessionCookieOptions;   // Cookie配置(直接传给 res.cookie)
  tenantId: string;                      // 租户ID
  userInfo: {                            // 用户信息
    id: string;
    username: string;
    email: string;
    displayName: string;
    avatar?: string;
    authUserId: string;                  // Keycloak用户ID
  };
  accessToken: string;                   // User-Service返回的accessToken
  refreshToken: string;                  // User-Service返回的refreshToken
  claims: SessionClaims;                 // JWT载荷
}

Session 验证

verifySession(sessionJwt): Promise<VerifySessionResult>

验证 Session JWT,返回载荷信息。

try {
  const session = await ssoServer.verifySession(sessionJwt);

  console.log('Session ID:', session.sessionId);
  console.log('用户ID:', session.claims.sub);
  console.log('租户ID:', session.claims.tenantId);
  console.log('过期时间:', new Date(session.expiresAt));
} catch (error) {
  console.error('Session无效:', error.message);
  // Session 无效或已过期
}

返回值 VerifySessionResult:

interface VerifySessionResult {
  claims: SessionClaims;  // JWT载荷
  expiresAt: number;      // 过期时间戳(毫秒)
  sessionId: string;      // Session ID (jti)
}

interface SessionClaims {
  sub: string;            // 用户ID
  jti: string;            // Session ID
  tenantId: string;       // 租户ID
  authUserId: string;     // Keycloak用户ID
  username: string;       // 用户名
  email?: string;         // 邮箱
  displayName?: string;   // 显示名称
  sessionVersion: string; // Session版本
  type: 'session';        // 类型标识
  traceId?: string;       // 链路追踪ID
  iat: number;            // 签发时间
  exp: number;            // 过期时间
  iss: string;            // 签发者
  aud?: string;           // 受众
}

Session 吊销

revokeSession(sessionJwt): Promise<void>

吊销 Session(仅 Redis / Database 模式有效)。

await ssoServer.revokeSession(sessionJwt);
console.log('Session已吊销');

注意:

  • ⚠️ JWT 模式无法主动吊销(无状态设计)
  • ✅ Redis / Database 模式支持立即吊销
  • 生产环境推荐使用 Redis 或 Database 模式

⚙️ 配置选项

XllentSSOServerOptions

interface XllentSSOServerOptions {
  /** User-Service 服务根地址(必填) */
  userServiceUrl: string;

  /** JWT 签名密钥(必填,建议32字符以上) */
  jwtSecret: string;

  /** Axios 配置(可选) */
  axiosConfig?: {
    timeout?: number;      // 超时时间(默认8000ms)
    headers?: Record<string, string>;
  };

  /** 接口路径配置(可选) */
  endpoints?: {
    verifyTempToken?: string;  // 默认: /oauth/verify-temp-token
  };

  /** Session 配置(可选) */
  session?: {
    issuer?: string;           // JWT签发者(默认: xllent)
    audience?: string;         // JWT受众(可选)
    expiresInSeconds?: number; // 过期时间(默认: 7天)
    cookie?: Partial<SessionCookieConfig>;
  };

  /** Session 存储方案(可选,默认JWT) */
  sessionStore?: SessionStoreOption;
}

Session 存储方案选择

方案1: JWT 模式(开发/测试环境)

特点:

  • ✅ 无需额外存储依赖
  • ✅ 部署简单,无状态
  • ❌ 无法主动吊销 Session
  • ❌ 用户强制下线需等待过期

配置:

{
  sessionStore: { type: 'jwt' }
}

适用场景:

  • 开发环境快速验证
  • 小型项目(不需要强制登出)
  • 短期效 Session(<1小时)

方案2: Redis 模式(生产环境推荐)

特点:

  • ✅ 支持 Session 主动吊销
  • ✅ 高性能(内存存储)
  • ✅ 支持分布式部署
  • ✅ 自动过期清理

配置:

import Redis from 'ioredis';

const redis = new Redis({
  host: 'localhost',
  port: 6379,
});

{
  sessionStore: {
    type: 'redis',
    redis, // 实现 RedisLikeClient 接口即可
    keyPrefix: 'sso:session:' // 可选,默认 'sso:session:'
  }
}

RedisLikeClient 接口:

interface RedisLikeClient {
  set(key: string, value: string, options: { PX: number }): Promise<void>;
  get(key: string): Promise<string | null>;
  del(key: string): Promise<number>;
}

适用场景:

  • 生产环境(推荐)
  • 需要强制下线功能
  • 分布式部署
  • 高并发场景

方案3: Database 模式(持久化需求)

特点:

  • ✅ 支持 Session 主动吊销
  • ✅ 数据持久化(审计需求)
  • ✅ 支持复杂查询(如:查询某用户所有Session)
  • ⚠️ 性能略低于 Redis

配置:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

const repository: DatabaseSessionRepository = {
  async save(record) {
    await prisma.session.create({
      data: {
        jti: record.claims.jti!,
        sessionJwt: record.sessionJwt,
        userId: record.claims.sub,
        tenantId: record.claims.tenantId,
        expiresAt: new Date(record.expiresAt),
      },
    });
  },
  async exists(jti) {
    const session = await prisma.session.findUnique({
      where: { jti, expiresAt: { gt: new Date() } },
    });
    return !!session;
  },
  async delete(jti) {
    await prisma.session.delete({ where: { jti } });
  },
};

{
  sessionStore: {
    type: 'database',
    repository
  }
}

DatabaseSessionRepository 接口:

interface DatabaseSessionRepository {
  save(record: SessionRecord): Promise<void>;
  exists(jti: string): Promise<boolean>;
  delete(jti: string): Promise<void>;
}

interface SessionRecord {
  sessionJwt: string;
  claims: SessionClaims;
  expiresAt: number;
}

适用场景:

  • 审计日志需求
  • 需要查询历史 Session
  • 数据持久化要求
  • 不想引入 Redis 依赖

Cookie 配置

interface SessionCookieConfig {
  name: string;           // Cookie名称(默认: x-session-id)
  httpOnly: boolean;      // 仅HTTP访问(默认: true)
  secure: boolean;        // 仅HTTPS传输(默认: true)
  sameSite: 'lax' | 'strict' | 'none';  // CSRF防护(默认: lax)
  path: string;           // Cookie路径(默认: /)
  domain?: string;        // Cookie域名(可选)
}

生产环境推荐配置:

{
  session: {
    cookie: {
      name: 'x-session-id',
      httpOnly: true,      // ✅ 防止JavaScript读取
      secure: true,        // ✅ 仅HTTPS传输
      sameSite: 'strict',  // ✅ 严格CSRF防护
      path: '/',
      domain: '.example.com' // ✅ 支持子域名共享
    }
  }
}

🔒 安全最佳实践

⚠️ JWT 模式的安全局限性

问题: JWT 模式无法主动吊销 Session,存在安全风险。

风险场景:

// 用户点击"登出"
await ssoServer.revokeSession(sessionJwt); // ❌ JWT模式无效

// 但攻击者仍可使用旧的JWT访问API(直到过期)

解决方案:

  1. 生产环境使用 Redis / Database 模式

    {
      sessionStore: {
        type: 'redis',
        redis: new Redis(/* ... */),
      }
    }
  2. 缩短 JWT 有效期

    {
      session: {
        expiresInSeconds: 15 * 60, // 15分钟
      }
    }
  3. 添加环境检查

    const isProduction = process.env.NODE_ENV === 'production';
    
    if (isProduction && storeConfig.type === 'jwt') {
      throw new Error('生产环境不允许使用JWT模式,请切换到Redis或Database');
    }

🛡️ JWT 密钥强度

问题: 弱密钥容易被破解。

推荐:

// ❌ 不安全
jwtSecret: '123456'

// ❌ 不安全(太短)
jwtSecret: 'my-secret'

// ✅ 推荐(32字符以上,随机生成)
jwtSecret: crypto.randomBytes(32).toString('hex')
// 示例: a3f9c8b2e1d4f7a6c5b8e3d2f1a9b7c4e6d8f2a1b3c5e7d9f1a2b4c6e8d0f2a4

验证密钥强度:

if (jwtSecret.length < 32) {
  throw new Error('JWT密钥长度至少32字符');
}

🛡️ 防止信息泄露

问题: 错误消息可能泄露系统信息。

不推荐:

throw new Error('Session 缺少 jti,无法验证 Redis 存储');

推荐:

// 生产环境返回通用错误
const message = isProduction
  ? 'Session 无效'
  : 'Session 缺少 jti,无法验证 Redis 存储';
throw new Error(message);

🛡️ CORS 配置

跨域请求需要正确配置:

// main.ts
app.enableCors({
  origin: ['https://app.example.com'],
  credentials: true, // ✅ 允许携带Cookie
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
});

📚 完整示例

NestJS 完整集成(生产级)

// config/configuration.ts
export default () => ({
  env: {
    isProduction: process.env.NODE_ENV === 'production',
    userService: {
      baseUrl: process.env.USER_SERVICE_BASE_URL,
    },
    jwt: {
      secret: process.env.JWT_SECRET,
    },
    redis: {
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT || '6379', 10),
    },
  },
});

// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { XllentSSOServer } from '@xllentai/sso-server';
import Redis from 'ioredis';

@Module({
  imports: [ConfigModule],
  providers: [
    {
      provide: 'REDIS_CLIENT',
      useFactory: (configService: ConfigService) => {
        return new Redis({
          host: configService.get<string>('env.redis.host'),
          port: configService.get<number>('env.redis.port'),
        });
      },
      inject: [ConfigService],
    },
    {
      provide: 'SSO_SERVER',
      useFactory: (configService: ConfigService, redis: Redis) => {
        const isProduction = configService.get<boolean>('env.isProduction');

        return new XllentSSOServer({
          userServiceUrl: configService.get<string>('env.userService.baseUrl')!,
          jwtSecret: configService.get<string>('env.jwt.secret')!,
          session: {
            issuer: 'xllent',
            audience: 'xllent-portal',
            expiresInSeconds: 7 * 24 * 60 * 60,
            cookie: {
              name: 'x-session-id',
              httpOnly: true,
              secure: isProduction,
              sameSite: isProduction ? 'strict' : 'lax',
              path: '/',
              domain: isProduction ? '.example.com' : undefined,
            },
          },
          sessionStore: {
            type: 'redis',
            redis,
            keyPrefix: 'sso:session:',
          },
        });
      },
      inject: [ConfigService, 'REDIS_CLIENT'],
    },
  ],
  exports: ['SSO_SERVER'],
})
export class AuthModule {}

// auth/auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, Inject } from '@nestjs/common';
import { XllentSSOServer } from '@xllentai/sso-server';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    @Inject('SSO_SERVER')
    private readonly ssoServer: XllentSSOServer,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const sessionJwt = request.cookies['x-session-id'];

    if (!sessionJwt) {
      return false;
    }

    try {
      const session = await this.ssoServer.verifySession(sessionJwt);
      request.user = session.claims; // 注入用户信息到请求
      return true;
    } catch {
      return false;
    }
  }
}

// user/user.controller.ts
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '../auth/auth.guard';

@Controller('user')
@UseGuards(AuthGuard)
export class UserController {
  @Get('profile')
  getProfile(@Req() req) {
    return {
      id: req.user.sub,
      username: req.user.username,
      email: req.user.email,
      tenantId: req.user.tenantId,
    };
  }
}

❓ 常见问题

Q: 如何选择 Session 存储方案?

| 场景 | 推荐方案 | 原因 | |------|---------|------| | 开发环境 | JWT | 无需额外依赖,快速验证 | | 小型项目(<1000用户) | JWT | 部署简单,成本低 | | 生产环境 | Redis | 高性能 + 支持吊销 | | 审计需求 | Database | 数据持久化,可查询历史 | | 分布式部署 | Redis | 支持多实例共享Session |

Q: 如何实现"强制下线"功能?

// 1. 使用 Redis / Database 模式
{
  sessionStore: {
    type: 'redis',
    redis: redisClient,
  }
}

// 2. 在管理端提供接口
@Post('admin/force-logout')
async forceLogout(@Body('userId') userId: string) {
  // 查询该用户的所有Session(需自行维护userId->sessionId映射)
  const sessionIds = await this.getSessionsByUserId(userId);

  // 批量吊销
  for (const sessionId of sessionIds) {
    await redis.del(`sso:session:${sessionId}`);
  }

  return { success: true };
}

Q: 如何实现"单点登出"(其他应用同步下线)?

// 1. 使用共享Redis
const sharedRedis = new Redis({ /* 所有应用共享 */ });

// 2. 登出时发布消息
@Post('logout')
async logout(@Req() req, @Res() res) {
  const sessionJwt = req.cookies['x-session-id'];
  if (sessionJwt) {
    await this.ssoServer.revokeSession(sessionJwt);

    // 发布登出事件
    await redis.publish('sso:logout', JSON.stringify({
      sessionId: session.claims.jti,
      userId: session.claims.sub,
    }));
  }

  res.clearCookie('x-session-id');
  return { success: true };
}

// 3. 订阅登出事件
redis.subscribe('sso:logout');
redis.on('message', (channel, message) => {
  const { sessionId } = JSON.parse(message);
  // 清理本地缓存(如有)
});

Q: 如何处理 Cookie 跨域问题?

// 1. 后端设置 CORS
app.enableCors({
  origin: 'https://app.example.com',
  credentials: true, // ✅ 必须设置
});

// 2. 前端请求携带凭证
fetch('https://api.example.com/auth/me', {
  credentials: 'include', // ✅ 必须设置
});

// 3. Cookie 配置域名
{
  session: {
    cookie: {
      domain: '.example.com', // ✅ 支持子域名共享
      sameSite: 'none',       // ✅ 跨域必须设置为none
      secure: true,           // ✅ sameSite=none时必须secure
    }
  }
}

Q: 如何在单元测试中 Mock?

import { Test } from '@nestjs/testing';
import { XllentSSOServer } from '@xllentai/sso-server';

const mockSSOServer = {
  createSessionFromTempToken: jest.fn(),
  verifySession: jest.fn(),
  revokeSession: jest.fn(),
};

const module = await Test.createTestingModule({
  controllers: [AuthController],
  providers: [
    {
      provide: 'SSO_SERVER',
      useValue: mockSSOServer,
    },
  ],
}).compile();

// 测试
mockSSOServer.verifySession.mockResolvedValue({
  claims: { sub: 'user-1', username: 'test' },
  sessionId: 'session-1',
  expiresAt: Date.now() + 3600000,
});

🔗 相关链接

📄 许可证

MIT License


维护团队: Xllent Platform Team 问题反馈: GitHub Issues