基于 Redis + JWT 的跨系统身份共享方案

单点登录(SSO)的核心目标是**"一次登录,多系统访问"**,需通过"统一认证中心"串联多个业务系统,实现身份凭证的跨系统共享与验证。以下基于 NestJS + Redis + JWT 实现完整的 SSO 流程,包含"统一认证中心(Auth Server)"和"业务系统(Client Server)"两端代码,覆盖"登录→凭证共享→跨系统验证→登出"全链路。

一、SSO 整体架构与核心逻辑

在开始代码实现前,需明确 SSO 的核心组件与数据流转:

  1. 统一认证中心(Auth Server):唯一的身份验证入口,负责用户登录、JWT 令牌生成、SSO 会话管理(Redis 存储);
  2. 业务系统(Client Server):如订单系统、会员系统等,自身不处理登录,依赖 Auth Server 验证用户身份;
  3. Redis:存储 SSO 全局会话(关联用户 ID 与所有登录系统)、JWT 黑名单(登出/失效令牌);
  4. JWT 令牌 :分为两种------
    • SSO 令牌(SSO Token):由 Auth Server 生成,全局唯一,代表用户的 SSO 会话;
    • 业务令牌(Client Token):业务系统接收 SSO Token 后,生成自身系统的 JWT(可选,减少跨系统请求 Auth Server 的频率)。

核心流程

flowchart LR A[用户] -->|1. 访问业务系统A| B[业务系统A] B -->|2. 未登录,重定向到Auth Server| C[统一认证中心] A -->|3. 输入账号密码登录| C C -->|4. 验证通过,生成SSO Token+用户信息| D[Redis存储SSO会话
(key: SSO_TOKEN, value: 用户ID+系统列表)] C -->|5. 重定向回业务系统A,携带SSO Token| B B -->|6. 向Auth Server验证SSO Token| C C -->|7. 验证通过,返回用户信息| B B -->|8. 生成业务令牌(可选),允许访问| A A -->|9. 访问业务系统B| E[业务系统B] E -->|10. 未登录,重定向到Auth Server| C C -->|11. 检测到SSO会话已存在| E E -->|12. 向Auth Server验证SSO Token| C C -->|13. 验证通过,允许访问| A A -->|14. 登出(任意系统)| F[删除Redis中的SSO会话] F -->|15. 所有业务系统同步登出| A

二、前置准备:环境与依赖

1. 技术栈

  • 后端框架:NestJS(Auth Server 与 Client Server 均基于此);
  • 数据库:Redis(存储 SSO 会话、JWT 黑名单)、MySQL/PostgreSQL(存储用户基础信息);
  • 核心依赖:@nestjs/jwt(JWT 生成/验证)、ioredis(Redis 操作)、passport-jwt(身份验证)、cookie-parser(处理 Cookie,可选)。

2. 安装依赖(Auth Server 与 Client Server 均需安装)

shell 复制代码
# 核心依赖
pnpm add @nestjs/jwt @nestjs/passport passport-jwt ioredis @nestjs-modules/ioredis bcryptjs
# 开发依赖
pnpm add -D @types/passport-jwt @types/ioredis @types/bcryptjs

三、统一认证中心(Auth Server)代码实现

Auth Server 是 SSO 的核心,需实现"用户登录""SSO Token 生成""跨系统令牌验证""SSO 会话管理(登出/失效)"四大功能。

1. 配置 Redis 与 JWT 模块

首先在 Auth Server 中配置 Redis(存储 SSO 会话)和 JWT(生成 SSO Token):

typescript 复制代码
// src/app.module.ts(Auth Server)
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { RedisModule } from '@nestjs-modules/ioredis';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from './auth/auth.module';
import { User } from './users/entities/user.entity'; // 用户实体(id/username/password/role)

@Module({
  imports: [
    // 配置环境变量(存储JWT密钥、Redis地址、业务系统白名单等)
    ConfigModule.forRoot({ isGlobal: true }),
    // 配置Redis(存储SSO会话、JWT黑名单)
    RedisModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        config: {
          host: config.get('REDIS_HOST', 'localhost'),
          port: config.get('REDIS_PORT', 6379),
          password: config.get('REDIS_PASSWORD', ''),
          db: 0,
        },
      }),
    }),
    // 配置JWT(生成SSO Token)
    JwtModule.registerAsync({
      global: true,
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        secret: config.get('SSO_JWT_SECRET', 'sso-prod-secret-2024'), // SSO专用密钥(需复杂)
        signOptions: { expiresIn: '8h' }, // SSO Token有效期(8小时,可根据需求调整)
      }),
    }),
    // 配置数据库(存储用户信息)
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'mysql',
        host: config.get('DB_HOST'),
        port: config.get('DB_PORT'),
        username: config.get('DB_USERNAME'),
        password: config.get('DB_PASSWORD'),
        database: config.get('DB_NAME'),
        entities: [User],
        synchronize: false, // 生产环境关闭自动同步
      }),
    }),
    AuthModule,
  ],
})
export class AppModule {}

2. 实现 SSO 核心服务(AuthService)

封装"用户登录验证""SSO Token 生成""SSO Token 验证""SSO 会话登出"等核心逻辑:

typescript 复制代码
// src/auth/auth.service.ts(Auth Server)
import { Injectable, UnauthorizedException, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { InjectRedis } from '@nestjs-modules/ioredis';
import { Repository } from 'typeorm';
import { JwtService } from '@nestjs/jwt';
import { Redis } from 'ioredis';
import { compareSync } from 'bcryptjs';
import { ConfigService } from '@nestjs/config';
import { User } from '../users/entities/user.entity';

// SSO会话在Redis中的key前缀(避免key冲突)
const SSO_SESSION_PREFIX = 'sso:session:';
// JWT黑名单在Redis中的key前缀(登出/失效的Token)
const JWT_BLACKLIST_PREFIX = 'sso:jwt:blacklist:';

@Injectable()
export class AuthService {
  // 业务系统白名单(仅允许白名单内的系统访问SSO验证接口,防止恶意请求)
  private readonly clientWhitelist: string[];

  constructor(
    @InjectRepository(User) private userRepo: Repository<User>,
    private jwtService: JwtService,
    @InjectRedis() private redis: Redis,
    private configService: ConfigService,
  ) {
    // 从环境变量读取业务系统白名单(如"https://order-system.com,https://member-system.com")
    this.clientWhitelist = this.configService
      .get('SSO_CLIENT_WHITELIST', '')
      .split(',')
      .filter(Boolean);
  }

  /**
   * 1. 用户登录:验证账号密码,生成SSO Token与SSO会话
   * @param username 用户名
   * @param password 密码
   * @param clientId 业务系统标识(如"order-system",用于记录哪个系统登录)
   */
  async ssoLogin(username: string, password: string, clientId: string) {
    // ① 验证业务系统是否在白名单内
    if (!this.clientWhitelist.includes(clientId)) {
      throw new ForbiddenException('非法业务系统,禁止访问SSO');
    }

    // ② 验证用户账号密码
    const user = await this.userRepo
      .createQueryBuilder('user')
      .addSelect('user.password') // 显式查询加密后的密码
      .where('user.username = :username', { username })
      .getOne();

    if (!user || !compareSync(password, user.password)) {
      throw new UnauthorizedException('账号或密码错误');
    }

    // ③ 生成SSO Token(Payload包含用户ID、角色、业务系统ID)
    const ssoToken = this.jwtService.sign({
      sub: user.id, // 用户ID
      username: user.username,
      role: user.role,
      clientId, // 当前登录的业务系统标识
    });

    // ④ 存储SSO会话到Redis(key: SSO_TOKEN, value: 包含用户ID+已登录系统列表的JSON)
    const ssoSessionKey = `${SSO_SESSION_PREFIX}${ssoToken}`;
    // 查询当前用户是否已有SSO会话(若有,追加当前业务系统)
    const existingSession = await this.redis.get(ssoSessionKey);
    const loginClients = existingSession 
      ? [...JSON.parse(existingSession).loginClients, clientId] 
      : [clientId];

    // 存储SSO会话(有效期与SSO Token一致,8小时)
    await this.redis.set(
      ssoSessionKey,
      JSON.stringify({
        userId: user.id,
        loginClients: [...new Set(loginClients)], // 去重,避免重复添加同一系统
        createdAt: Date.now(),
      }),
      'EX',
      8 * 60 * 60, // 8小时(与JWT有效期同步)
    );

    // ⑤ 返回SSO Token与用户基础信息(不含敏感字段)
    const { password: _, ...userInfo } = user;
    return {
      ssoToken,
      userInfo,
      expiresIn: 8 * 60 * 60, // 有效期(秒)
    };
  }

  /**
   * 2. 验证SSO Token:供业务系统调用,确认Token有效性
   * @param ssoToken 业务系统传递的SSO Token
   * @param clientId 业务系统标识(验证Token是否属于该系统)
   */
  async validateSsoToken(ssoToken: string, clientId: string) {
    // ① 验证业务系统白名单
    if (!this.clientWhitelist.includes(clientId)) {
      throw new ForbiddenException('非法业务系统,禁止验证SSO Token');
    }

    // ② 检查Token是否在黑名单(已登出/失效)
    const isBlacklisted = await this.redis.get(`${JWT_BLACKLIST_PREFIX}${ssoToken}`);
    if (isBlacklisted) {
      throw new UnauthorizedException('SSO Token已失效,请重新登录');
    }

    // ③ 验证JWT签名与过期时间
    let payload: any;
    try {
      payload = this.jwtService.verify(ssoToken);
    } catch (error) {
      throw new UnauthorizedException('SSO Token无效或已过期');
    }

    // ④ 验证Token中的业务系统标识与当前请求系统一致
    if (payload.clientId !== clientId) {
      throw new UnauthorizedException('SSO Token不属于当前业务系统');
    }

    // ⑤ 验证Redis中的SSO会话是否存在(防止Token未过期但会话被主动删除)
    const ssoSessionKey = `${SSO_SESSION_PREFIX}${ssoToken}`;
    const ssoSession = await this.redis.get(ssoSessionKey);
    if (!ssoSession) {
      throw new UnauthorizedException('SSO会话已失效,请重新登录');
    }

    // ⑥ 返回用户信息(供业务系统使用)
    const { userId } = JSON.parse(ssoSession);
    const user = await this.userRepo.findOne({ where: { id: userId } });
    if (!user) {
      throw new UnauthorizedException('用户不存在');
    }

    const { password: _, ...userInfo } = user;
    return { userInfo, ssoToken };
  }

  /**
   * 3. SSO登出:删除Redis中的SSO会话与Token黑名单,实现全系统同步登出
   * @param ssoToken 待登出的SSO Token
   */
  async ssoLogout(ssoToken: string) {
    // ① 查询SSO会话(确认会话存在)
    const ssoSessionKey = `${SSO_SESSION_PREFIX}${ssoToken}`;
    const ssoSession = await this.redis.get(ssoSessionKey);
    if (!ssoSession) {
      throw new UnauthorizedException('SSO会话不存在');
    }

    // ② 将Token加入黑名单(防止登出后继续使用)
    const payload = this.jwtService.decode(ssoToken) as any;
    await this.redis.set(
      `${JWT_BLACKLIST_PREFIX}${ssoToken}`,
      'invalid',
      'EX',
      payload.exp - Math.floor(Date.now() / 1000), // 黑名单有效期=Token剩余有效期
    );

    // ③ 删除SSO会话(全系统同步登出)
    await this.redis.del(ssoSessionKey);

    return { message: 'SSO登出成功,所有系统已同步登出' };
  }
}

3. 实现 SSO 接口(AuthController)

提供"登录""Token验证""登出"三个对外接口,供前端和业务系统调用:

typescript 复制代码
// src/auth/auth.controller.ts(Auth Server)
import { Controller, Post, Get, Query, Body, HttpCode } from '@nestjs/common';
import { AuthService } from './auth.service';

@Controller('sso')
export class AuthController {
  constructor(private authService: AuthService) {}

  /**
   * 接口1:用户登录(前端调用,如业务系统的登录页重定向到Auth Server的登录接口)
   * @param body { username, password, clientId }
   */
  @Post('login')
  @HttpCode(200)
  async ssoLogin(
    @Body() body: { username: string; password: string; clientId: string },
  ) {
    return this.authService.ssoLogin(body.username, body.password, body.clientId);
  }

  /**
   * 接口2:验证SSO Token(业务系统后端调用,确认用户身份)
   * @param ssoToken 业务系统传递的SSO Token
   * @param clientId 业务系统标识
   */
  @Get('validate-token')
  async validateSsoToken(
    @Query('ssoToken') ssoToken: string,
    @Query('clientId') clientId: string,
  ) {
    return this.authService.validateSsoToken(ssoToken, clientId);
  }

  /**
   * 接口3:SSO登出(任意系统的前端调用,实现全系统同步登出)
   * @param ssoToken 待登出的SSO Token
   */
  @Post('logout')
  @HttpCode(200)
  async ssoLogout(@Body('ssoToken') ssoToken: string) {
    return this.authService.ssoLogout(ssoToken);
  }
}

四、业务系统(Client Server)代码实现

业务系统(如订单系统、会员系统)自身不处理用户登录,仅需实现"重定向到 Auth Server 登录""验证 SSO Token""生成业务令牌"三个核心逻辑,确保用户身份合法后允许访问。

1. 配置业务系统与 Auth Server 通信

首先在业务系统中配置 Auth Server 的地址、自身标识(clientId)等信息:

typescript 复制代码
// src/app.module.ts(Client Server,以订单系统为例)
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { HttpModule } from '@nestjs/axios'; // 用于调用Auth Server的HTTP接口
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    // 配置HTTP模块(调用Auth Server的接口)
    HttpModule.registerAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        baseURL: config.get('SSO_AUTH_SERVER_URL', 'https://sso-auth-server.com'), // Auth Server基础地址
        timeout: 5000, // 超时时间
      }),
    }),
    // 配置业务系统自身的JWT(可选,用于生成业务令牌,减少调用Auth Server的频率)
    JwtModule.registerAsync({
      global: true,
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        secret: config.get('CLIENT_JWT_SECRET', 'order-system-secret'), // 业务系统专用密钥
        signOptions: { expiresIn: '2h' }, // 业务令牌有效期(短于SSO Token)
      }),
    }),
    UserModule,
    AuthModule,
  ],
})
export class AppModule {}

2. 实现业务系统的 SSO 适配服务(ClientAuthService)

封装"重定向到 Auth Server 登录""验证 SSO Token""生成业务令牌"等逻辑:

typescript 复制代码
// src/auth/client-auth.service.ts(Client Server)
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class ClientAuthService {
  // 业务系统自身标识(需在Auth Server的白名单中)
  private readonly clientId: string;
  // 业务系统的回调地址(Auth Server登录成功后重定向回此地址)
  private readonly redirectUri: string;

  constructor(
    private httpService: HttpService,
    private jwtService: JwtService,
    private configService: ConfigService,
  ) {
    this.clientId = this.configService.get('SSO_CLIENT_ID', 'order-system'); // 如"order-system"
    this.redirectUri = this.configService.get(
      'SSO_REDIRECT_URI',
      'https://order-system.com/sso/callback',
    );
  }

  /**
   * 1. 生成重定向到Auth Server的登录地址
   * @param currentPath 当前访问的业务系统路径(登录成功后跳转回此路径)
   */
  getSsoLoginUrl(currentPath: string) {
    // 构造Auth Server的登录地址,携带业务系统标识和回调地址
    const authServerLoginUrl = new URL(
      '/sso/login',
      this.configService.get('SSO_AUTH_SERVER_URL'),
    );
    authServerLoginUrl.searchParams.append('clientId', this.clientId);
    authServerLoginUrl.searchParams.append('redirectUri', this.redirectUri);
    authServerLoginUrl.searchParams.append('state', currentPath); // 记录当前路径,用于登录后跳转

    return authServerLoginUrl.toString();
  }

  /**
   * 2. 验证SSO Token(调用Auth Server的验证接口)
   * @param ssoToken 从Auth Server获取的SSO Token
   */
  async validateSsoToken(ssoToken: string) {
    try {
      // 调用Auth Server的验证接口
      const response = await firstValueFrom(
        this.httpService.get('/sso/validate-token', {
          params: {
            ssoToken,
            clientId: this.clientId,
          },
        }),
      );
      return response.data; // { userInfo, ssoToken }
    } catch (error) {
      // 验证失败(如Token无效/过期),抛出未授权异常
      throw new UnauthorizedException(
        error.response?.data?.message || 'SSO身份验证失败,请重新登录',
      );
    }
  }

  /**
   * 3. 生成业务系统自身的令牌(可选,减少重复调用Auth Server)
   * @param userInfo 从Auth Server获取的用户信息
   */
  generateClientToken(userInfo: any) {
    return this.jwtService.sign({
      sub: userInfo.id,
      username: userInfo.username,
      role: userInfo.role,
      ssoToken: userInfo.ssoToken, // 携带SSO Token,用于后续刷新或登出
    });
  }

  /**
   * 4. 业务系统登出(调用Auth Server的登出接口,实现全系统同步登出)
   * @param ssoToken SSO Token
   */
  async logout(ssoToken: string) {
    try {
      await firstValueFrom(
        this.httpService.post('/sso/logout', { ssoToken }),
      );
      return { message: '登出成功,已同步退出所有系统' };
    } catch (error) {
      throw new UnauthorizedException('登出失败,请重试');
    }
  }
}

3. 实现业务系统的 SSO 回调与接口保护

业务系统需提供"SSO 回调接口"(接收 Auth Server 重定向的 SSO Token)和"受保护接口"(验证用户身份):

typescript 复制代码
// src/auth/client-auth.controller.ts(Client Server)
import { Controller, Get, Query, Res, UseGuards, Req, Post } from '@nestjs/common';
import { Response, Request } from 'express';
import { ClientAuthService } from './client-auth.service';
import { JwtAuthGuard } from './jwt-auth.guard'; // 业务系统自身的JWT守卫

@Controller('sso')
export class ClientAuthController {
  constructor(private clientAuthService: ClientAuthService) {}

  /**
   * 接口1:SSO回调接口(Auth Server登录成功后重定向到这里)
   * 接收SSO Token,生成业务令牌,存储到Cookie或返回给前端
   */
  @Get('callback')
  async ssoCallback(
    @Query('ssoToken') ssoToken: string,
    @Query('state') state: string, // 登录前的访问路径
    @Res() res: Response,
  ) {
    if (!ssoToken) {
      return res.redirect('/login?error=缺少SSO令牌');
    }

    try {
      // ① 验证SSO Token有效性
      const { userInfo } = await this.clientAuthService.validateSsoToken(ssoToken);

      // ② 生成业务系统自身的令牌(可选)
      const clientToken = this.clientAuthService.generateClientToken({
        ...userInfo,
        ssoToken,
      });

      // ③ 将业务令牌存储到httpOnly Cookie(推荐,防XSS)
      res.cookie('client_token', clientToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production', // 生产环境启用HTTPS
        maxAge: 2 * 60 * 60 * 1000, // 2小时(与业务令牌有效期一致)
        path: '/',
      });

      // ④ 重定向回登录前的路径(如用户原本访问的订单详情页)
      return res.redirect(state || '/');
    } catch (error) {
      return res.redirect(`/login?error=${error.message}`);
    }
  }

  /**
   * 接口2:业务系统登出接口
   */
  @Post('logout')
  @UseGuards(JwtAuthGuard) // 验证业务令牌
  async logout(@Req() req: Request, @Res() res: Response) {
    // 从请求中获取SSO Token(业务令牌的Payload中携带)
    const ssoToken = req.user.ssoToken;
    await this.clientAuthService.logout(ssoToken);

    // 清除业务令牌Cookie
    res.clearCookie('client_token');
    return res.send({ message: '登出成功' });
  }
}

4. 业务系统的接口保护(JWT守卫)

使用业务系统自身的 JWT 守卫保护接口,验证用户身份:

typescript 复制代码
// src/auth/jwt-auth.guard.ts(Client Server)
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest<Request>();
    
    // 从Cookie或请求头获取业务令牌
    const token = 
      request.cookies.client_token || 
      (request.headers.authorization?.split(' ')[1] || '');

    if (!token) {
      throw new UnauthorizedException('未登录,请先通过SSO登录');
    }

    try {
      // 验证业务令牌
      const payload = this.jwtService.verify(token);
      // 将用户信息附加到req.user
      request.user = payload;
    } catch (error) {
      throw new UnauthorizedException('登录已过期,请重新登录');
    }

    return true;
  }
}

5. 业务系统的受保护接口示例

typescript 复制代码
// src/user/user.controller.ts(Client Server,订单系统的用户接口)
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';

@Controller('user')
export class UserController {
  // 需SSO登录才能访问的接口:获取当前用户的订单列表
  @UseGuards(JwtAuthGuard)
  @Get('orders')
  getUserOrders(@Req() req) {
    // req.user包含从SSO获取的用户信息
    return {
      userId: req.user.sub,
      username: req.user.username,
      orders: [], // 业务逻辑:查询该用户的订单
      message: '订单列表查询成功',
    };
  }
}

五、SSO 前端适配逻辑(通用方案)

前端需实现"未登录时重定向到 Auth Server""接收 SSO Token 并存储""请求时携带业务令牌"三大逻辑,以下是简化示例(基于 Vue/React 通用逻辑):

javascript 复制代码
// sso-utils.js(前端工具函数)
import { getToken, setToken, removeToken } from './storage'; // 封装localStorage/Cookie操作

// 1. 初始化:检查是否已登录,未登录则重定向到Auth Server
export function initSso() {
  const currentPath = window.location.pathname;
  const isLoggedIn = !!getToken('client_token');

  if (!isLoggedIn && !currentPath.includes('/sso/callback')) {
    // 未登录且不在回调页,重定向到Auth Server登录
    const clientAuthService = new ClientAuthService(); // 封装的API服务
    const loginUrl = clientAuthService.getSsoLoginUrl(currentPath);
    window.location.href = loginUrl;
  }
}

// 2. 处理SSO回调(在/sso/callback页面调用)
export async function handleSsoCallback() {
  const urlParams = new URLSearchParams(window.location.search);
  const ssoToken = urlParams.get('ssoToken');
  const state = urlParams.get('state');

  if (ssoToken) {
    try {
      // 调用业务系统的回调接口,验证Token并获取业务令牌
      await api.post('/sso/callback', { ssoToken });
      // 跳转回登录前的路径
      window.location.href = state || '/';
    } catch (error) {
      // 处理错误(如Token无效)
      window.location.href = `/login?error=${error.message}`;
    }
  }
}

// 3. 请求拦截器:为所有API请求附加业务令牌
export function setupRequestInterceptor(axiosInstance) {
  axiosInstance.interceptors.request.use((config) => {
    const token = getToken('client_token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  });

  // 响应拦截器:处理401(登录过期)
  axiosInstance.interceptors.response.use(
    (response) => response,
    (error) => {
      if (error.response?.status === 401) {
        // 登录过期,清除令牌并重定向到登录页
        removeToken('client_token');
        window.location.href = '/login';
      }
      return Promise.reject(error);
    },
  );
}

六、SSO 关键安全与扩展说明

1. 安全加固

  • 白名单机制 :Auth Server 严格校验业务系统的 clientId,仅允许白名单内的系统访问,防止恶意系统滥用 SSO;
  • Token 传输安全:SSO Token 和业务令牌必须通过 HTTPS 传输,避免中间人攻击;
  • 令牌存储安全 :前端优先使用 httpOnly Cookie 存储令牌(防 XSS 攻击),配合 SameSite=Strict 防 CSRF;
  • 会话有效期:SSO Token 有效期不宜过长(如 8 小时),业务令牌可更短(如 2 小时),降低令牌泄露风险。

2. 扩展场景

  • 多端适配:移动端(App)可通过 WebView 集成 SSO,或使用 OAuth 2.0 扩展(如授权码模式);
  • 刷新令牌机制:为 SSO Token 添加刷新令牌(存储在 Redis),避免用户频繁登录;
  • 权限同步:当用户权限在 Auth Server 变更时,通过 Redis 发布订阅机制通知所有业务系统刷新权限缓存。

通过以上实现,多个业务系统可共享统一的用户身份,用户只需一次登录即可访问所有关联系统,大幅提升用户体验,同时通过 Redis 实现会话集中管理,确保登出时全系统同步失效,兼顾安全性与易用性。

相关推荐
Python代狂魔18 分钟前
Redis
数据库·redis·python·缓存
柠檬茶AL29 分钟前
36 NoSQL 注入
数据库·nosql·postman
-XWB-30 分钟前
PostgreSQL诊断系列(2/6):锁问题排查全攻略——揪出“阻塞元凶”
数据库·postgresql
XiaoMu_0011 小时前
【MongoDB与MySQL对比】
数据库
做科研的周师兄2 小时前
【机器学习入门】1.2 初识机器学习:从数据到智能的认知之旅
大数据·数据库·人工智能·python·机器学习·数据分析·机器人
技术与健康3 小时前
LLM实践系列:利用LLM重构数据科学流程04 - 智能特征工程
数据库·人工智能·重构
007php0073 小时前
Jenkins+docker 微服务实现自动化部署安装和部署过程
运维·数据库·git·docker·微服务·自动化·jenkins
北极糊的狐3 小时前
MySQL常见报错分析及解决方案总结(1)---Can‘t connect to MySQL server on ‘localhost‘(10061)
数据库·mysql
SelectDB4 小时前
2-5 倍性能提升,30% 成本降低,阿里云 SelectDB 存算分离架构助力波司登集团实现降本增效
大数据·数据库·数据分析