一、先聊聊我踩过的坑

前阵子帮一家客户排查Blue Prism登录失效的问题,现象很诡异:机器人跑得好好的,突然某一天全部任务报“认证失败”,手动登录控制台却一切正常。当时第一反应是密码过期,可IT说密码有效期还有三个月。后来翻日志才发现,问题出在单点登录的令牌生命周期上——不是太长就是太短,导致Blue Prism的机器人会话在后台静默失效。

这个事儿让我意识到,很多团队在搭企业级单点登录时,只关注了“能不能登进去”,却忽略了“登录状态能维持多久”。今天就把这次排查鉴权链路和缓存策略的经历总结一下,用大白话讲清楚,希望能帮大家少走弯路。

二、应用场景:为什么Blue Prism会和SSO“打架”

2.1 Blue Prism是什么角色

蓝棱镜(Blue Prism)是个RPA机器人流程自动化工具,它在企业里通常作为“数字员工”存在。它的控制台和运行机器人都有独立的登录机制。如果企业用了统一的单点登录(SSO),那Blue Prism就会把认证委托给身份提供方(比如Okta、Azure AD、Keycloak)。

也就是说,用户打开Blue Prism时,会跳转到SSO页面输一次账号密码,SSO签发一个令牌(Token),Blue Prism拿着这个令牌去访问受保护的资源。

2.2 令牌生命周期为什么是关键

令牌不是永久的。它有“有效期”。过了有效期,再拿它去访问API或者执行任务,服务器就会返回401未授权。Blue Prism的机器人有时候是7x24小时挂着的,如果令牌只有30分钟,那机器人每半小时就得重新认证一次。但机器人是无人值守的,没法弹个窗口让你输密码。所以就需要有一套刷新机制或缓存策略。

反过来,如果令牌有效期设成24小时甚至7天,看似省事,但安全风险极大。一旦令牌泄露,攻击者能长时间冒用身份。而且企业安全策略通常要求短令牌加频繁刷新。

2.3 典型故障场景

我这里画个场景:企业用Keycloak做SSO,Blue Prism通过OAuth2的授权码模式获取访问令牌。Keycloak里访问令牌有效期设为5分钟,刷新令牌有效期设为30分钟。Blue Prism在启动时拿到令牌,执行一个需要20分钟的长流程。结果5分钟后令牌过期,机器人还在跑,后续与数据库、文件服务器的交互全部401。管理员一看,说是“登录失效”。

还有另一种极端:把访问令牌有效期设为8小时,但刷新令牌设为永久。结果某个机器人账号密码改了,SSO侧已经吊销令牌,Blue Prism缓存里还保留着旧的刷新令牌,导致一直无法获取新令牌,直到缓存过期才恢复。

这两个案例说明:令牌生命周期太短,会打断正在执行的任务;太长,会带来安全漏洞和吊销延迟。我们需要找到平衡点,同时要理解Blue Prism的缓存机制如何影响这个平衡。

三、技术优缺点:长短令牌的博弈

3.1 短令牌(比如5分钟)

优点:

  • 泄露后影响面小。
  • 符合严格安全合规要求。
  • 可以快速响应权限变更。

缺点:

  • 高频刷新带来网络和计算开销。
  • 长任务会中途失效,需要重试机制。
  • 机器人无人值守时,刷新失败会导致任务中断。

3.2 长令牌(比如8小时)

优点:

  • 减少刷新次数,适合长时间批处理。
  • 系统负载低。
  • 用户体验好(不用老重新登录)。

缺点:

  • 泄露后长时间有效,风险高。
  • 权限变更无法及时生效。
  • 缓存策略不当会导致“僵尸会话”。

3.3 折中方案:访问令牌短,刷新令牌长

这是业界标准做法。访问令牌负责实际访问资源,寿命短;刷新令牌负责换取新访问令牌,寿命长(几天到几周)。Blue Prism支持配置这些参数,但要注意它的缓存路径是写入磁盘的还是内存的。

如果Blue Prism的机器人服务以Windows服务方式运行,它可能把令牌缓存在注册表或某个配置文件夹里。如果服务重启,内存缓存消失,但磁盘缓存还在。这就会造成一种情况:SSO认为刷新令牌已吊销,但Blue Prism磁盘缓存里还有旧的刷新令牌,导致认证一直失败。

四、注意事项:缓存策略是隐形地雷

4.1 缓存位置影响刷新行为

举个例子:Blue Prism的“System Manager”里可以设置凭据和会话选项。但很多团队不知道,它的令牌缓存默认存在 C:\ProgramData\Blue Prism Limited\Automate 下的某个配置文件里。如果你在配置里启用“使用单点登录”,Blue Prism会保存加密的令牌到该文件。

当令牌过期时,Blue Prism会尝试用刷新令牌换取新令牌。但如果这个缓存文件损坏、被安全软件隔离、或者权限不对,可能导致刷新失败。我曾遇到过,杀毒软件把那个配置文件当成威胁隔离了,然后机器人全部掉线。

4.2 时钟同步问题

令牌校验通常依赖时间戳。如果Blue Prism服务器和SSO服务器的时间偏差超过几十秒,令牌就会被认为是无效的。企业环境里虚拟机时钟漂移很常见,尤其是休眠恢复后的机器。排查时,我先检查的就是所有机器上的 w32tm /query /status。如果偏差大,需要配置NTP同步。

4.3 多环境下的缓存隔离

测试环境和生产环境如果共用同一套Blue Prism环境变量,缓存可能串味。比如你在测试环境登录了,然后切到生产环境,旧的令牌缓存还在,可能被误用。这时候需要清理Blue Prism本地缓存文件夹。

4.4 令牌大小对请求性能的影响

令牌里如果塞了很多自定义声明(Claims),比如部门、角色、项目列表,令牌会变得很大(几十KB)。Blue Prism每次调用都会在HTTP头带上这个令牌,导致请求变慢。而且某些网关对请求头大小有限制(比如8KB),超出就报错。建议精简令牌声明,只保留必要信息。

五、详细示例:用Node.js模拟Blue Prism的SSO鉴权链路

下面我用Node.js写一个模拟程序,演示令牌生命周期和缓存策略如何导致“登录失效”。技术栈是Node.js + Express + Axios。为了完整性,我会模拟三个角色:身份提供方(SSO)、受保护API、Blue Prism机器人客户端。

5.1 模拟SSO服务端

// sso-server.js
const express = require('express');
const app = express();
app.use(express.json());

// 模拟令牌存储,实际生产环境应该在数据库或缓存中
const tokenStore = new Map();

// 模拟用户数据库
const users = {
  admin: { password: '123456', roles: ['rpa-admin'] },
  robot: { password: 'rpapass', roles: ['rpa-executor'] }
};

// 简单的base64编码,仅演示用,生产环境必须用正式签名算法
function createToken(user, type, ttlSeconds) {
  const payload = {
    sub: user,
    type: type,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + ttlSeconds,
    roles: users[user].roles
  };
  // 这里用一个随机字符串模拟签名
  const token = Buffer.from(JSON.stringify(payload)).toString('base64') + '.fake-signature';
  tokenStore.set(token, payload);
  return token;
}

// 校验令牌是否有效
function verifyToken(token) {
  const parts = token.split('.');
  if (parts.length !== 2) return { valid: false, reason: '格式错误' };
  const payload = JSON.parse(Buffer.from(parts[0], 'base64').toString());
  const stored = tokenStore.get(token);
  if (!stored) return { valid: false, reason: '令牌在存储中不存在' };
  const now = Math.floor(Date.now() / 1000);
  if (payload.exp < now) {
    // 过期了,删除令牌
    tokenStore.delete(token);
    return { valid: false, reason: '令牌过期' };
  }
  return { valid: true, payload };
}

// 登录接口,获得访问令牌和刷新令牌
app.post('/oauth/token', (req, res) => {
  const { username, password, grant_type } = req.body;
  
  if (grant_type === 'password') {
    // 校验用户名密码
    if (!users[username] || users[username].password !== password) {
      return res.status(401).json({ error: '用户名或密码错误' });
    }
    // 访问令牌5分钟,刷新令牌60分钟
    const accessToken = createToken(username, 'access', 300);
    const refreshToken = createToken(username, 'refresh', 3600);
    return res.json({
      access_token: accessToken,
      refresh_token: refreshToken,
      expires_in: 300
    });
  }
  
  if (grant_type === 'refresh_token') {
    // 校验刷新令牌
    const { refresh_token } = req.body;
    const result = verifyToken(refresh_token);
    if (!result.valid) {
      return res.status(401).json({ error: '刷新令牌无效或过期' });
    }
    // 重新生成访问令牌
    const accessToken = createToken(result.payload.sub, 'access', 300);
    return res.json({
      access_token: accessToken,
      refresh_token: refresh_token, // 简化处理,不轮换刷新令牌
      expires_in: 300
    });
  }
  
  res.status(400).json({ error: '不支持的grant_type' });
});

// 受保护资源示例
app.get('/api/tasks', (req, res) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: '缺少令牌' });
  }
  const token = authHeader.slice(7);
  const result = verifyToken(token);
  if (!result.valid) {
    return res.status(401).json({ error: result.reason });
  }
  res.json({ tasks: ['任务A', '任务B'], user: result.payload.sub });
});

app.listen(3000, () => console.log('SSO服务运行在3000端口'));

5.2 模拟Blue Prism机器人客户端

// blueprism-robot.js
const axios = require('axios');

// 模拟磁盘缓存文件(实际是Blue Prism的加密配置文件)
const fs = require('fs');
const CACHE_FILE = './token-cache.json';

// 读取缓存的令牌
function readCache() {
  try {
    const data = fs.readFileSync(CACHE_FILE, 'utf-8');
    return JSON.parse(data);
  } catch (err) {
    // 没有缓存则返回空对象
    return {};
  }
}

// 写入缓存
function writeCache(cache) {
  fs.writeFileSync(CACHE_FILE, JSON.stringify(cache), 'utf-8');
}

// 使用密码获取令牌(手动登录场景)
async function loginWithPassword() {
  console.log('使用密码登录...');
  const response = await axios.post('http://localhost:3000/oauth/token', {
    username: 'robot',
    password: 'rpapass',
    grant_type: 'password'
  });
  const { access_token, refresh_token } = response.data;
  // 缓存到本地
  writeCache({ access_token, refresh_token });
  console.log('登录成功,令牌已缓存');
  return access_token;
}

// 获取有效令牌(优先使用缓存,过期则刷新)
async function getValidToken() {
  const cache = readCache();
  if (!cache.access_token) {
    return await loginWithPassword();
  }

  // 先尝试用访问令牌请求一次(这里省略了解析token内容的操作,直接调用API验证)
  try {
    const response = await axios.get('http://localhost:3000/api/tasks', {
      headers: { Authorization: `Bearer ${cache.access_token}` }
    });
    console.log('访问令牌有效,直接使用');
    return cache.access_token;
  } catch (err) {
    if (err.response && err.response.status === 401) {
      console.log('访问令牌过期,尝试刷新...');
      // 使用刷新令牌换新
      try {
        const refreshResponse = await axios.post('http://localhost:3000/oauth/token', {
          grant_type: 'refresh_token',
          refresh_token: cache.refresh_token
        });
        const { access_token, refresh_token } = refreshResponse.data;
        // 更新缓存,注意这里没有处理刷新令牌轮换,但实际令牌服务器会轮换
        writeCache({ access_token, refresh_token });
        console.log('刷新成功');
        return access_token;
      } catch (refreshErr) {
        console.error('刷新失败,需要重新登录');
        // 清除缓存
        fs.unlinkSync(CACHE_FILE);
        return await loginWithPassword();
      }
    }
  }
}

// 模拟执行长时间任务
async function runLongTask() {
  console.log('任务开始,先获取令牌...');
  const token = await getValidToken();
  console.log('开始执行任务...');
  
  // 模拟一个需要8分钟的任务
  for (let i = 0; i < 8; i++) {
    await sleep(1000); // 实际是每分钟输出一次,这里为了演示快进到每秒
    // 假设任务里每步都要调用API
    try {
      await axios.get('http://localhost:3000/api/tasks', {
        headers: { Authorization: `Bearer ${token}` }
      });
      console.log(`任务第${i + 1}分钟:API调用成功`);
    } catch (err) {
      if (err.response && err.response.status === 401) {
        console.log(`第${i + 1}分钟:令牌过期,需要刷新`);
        // 注意这里token是const,需要重新获取
        // 实际使用中,我们会把token变量改为let
        return;
      }
    }
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 主流程
(async () => {
  await runLongTask();
})();

5.3 演示问题发生

我连续运行两次这个脚本。第一次,令牌新鲜,任务能跑完。第二次,我手动把缓存文件里的 exp 字段改小,模拟令牌提前过期,然后脚本就会在任务中途报401。但实际操作中,我们只需要等待5分钟(访问令牌有效期),任务就会失败。这就是“令牌生命周期太短导致登录失效”的直观体现。

5.4 改进后的智能刷新版

// blueprism-robot-enhanced.js
// 这次我们加入主动刷新机制,不再等到请求失败才刷新

const axios = require('axios');
const fs = require('fs');
const CACHE_FILE = './token-cache-enhanced.json';

// 解析JWT(这里只是模拟解析,实际JWT需要base64解码和验签)
function parseToken(token) {
  const payloadPart = token.split('.')[0];
  return JSON.parse(Buffer.from(payloadPart, 'base64').toString());
}

// 判断令牌是否将在指定时间后过期(比如提前30秒刷新)
function shouldRefresh(token, bufferSeconds = 30) {
  const payload = parseToken(token);
  const now = Math.floor(Date.now() / 1000);
  // 如果令牌剩余寿命小于buffer秒,就需要刷新
  return (payload.exp - now) < bufferSeconds;
}

async function getTokenWithProactiveRefresh() {
  let cache = readCache();
  if (!cache.access_token) {
    cache = await passwordLogin();
  }

  // 检查是否接近过期
  if (shouldRefresh(cache.access_token, 30)) {
    console.log('令牌将在30秒内过期,主动刷新...');
    try {
      const refreshResult = await refreshToken(cache.refresh_token);
      cache = {
        access_token: refreshResult.access_token,
        refresh_token: refreshResult.refresh_token
      };
      writeCache(cache);
      console.log('主动刷新完成');
    } catch (err) {
      console.error('刷新失败,需要重新登录');
      cache = await passwordLogin();
    }
  }

  return cache.access_token;
}

async function passwordLogin() {
  // 和前面类似,这里省略具体实现
  // 实际会调用/oauth/token接口
}

async function refreshToken(refreshToken) {
  const response = await axios.post('http://localhost:3000/oauth/token', {
    grant_type: 'refresh_token',
    refresh_token: refreshToken
  });
  return response.data;
}

// 这个版本的好处是:在长任务执行过程中,每个步骤开始前都检查令牌剩余寿命,提前刷新,
// 避免任务执行到一半因为令牌过期而中断。

这个改进版模拟了在生产环境里推荐的“主动刷新”策略。Blue Prism虽然没有内置这种主动刷新,但我们可以通过外部调度,定期调用它的自动化接口来刷新会话。

六、缓存策略深入分析

6.1 缓存分层模型

企业级SSO通常有三层缓存:

  • 身份提供方(SSO服务器)的会话缓存
  • 应用侧(Blue Prism)的令牌缓存
  • 网络中间层(反向代理/API网关)的令牌缓存

排查时要逐层检查。Blue Prism的问题多数出现在第二层。我曾见过某个企业统一配置了网关层面的令牌缓存(比如Nginx用proxy_cache_key包含Authorization头),结果缓存了过期令牌,导致用户明明刷新了新令牌,网关还是拿旧缓存去鉴权。

6.2 Blue Prism的实际缓存机制

Blue Prism的认证模块是闭源的,但通过行为分析可以推断出以下几种情况:

  • 它会把访问令牌存在内存中,供当前服务进程使用。
  • 刷新令牌可能存在加密配置文件中。
  • 当服务重启后,内存令牌丢失,但刷新令牌还在,所以能自动恢复会话。

但问题在于:Blue Prism不会主动去检查刷新令牌是否被吊销。如果你在SSO管理端强制下线某个用户,Blue Prism的下一个刷新操作才会失败。所以管理员经常发现“明明在SSO里踢掉用户了,机器人还能正常跑一段时间”。

6.3 合适的缓存策略建议

我建议使用“双缓存+短访问令牌”策略:

  • 访问令牌有效期设为2分钟。
  • 刷新令牌有效期设为12小时。
  • Blue Prism执行长任务时,外部调度器(比如Windows任务计划程序)每1分钟调用一次Blue Prism的“恢复会话”命令。
  • 缓存本地会话状态,若发现刷新失败,立即发送邮件告警。

6.4 别再硬编码过期时间

很多团队喜欢在代码里写死expires_in,这是坏习惯。应该从SSO服务器的元数据(/.well-known/openid-configuration)动态读取有效期,然后根据任务最长耗时设定一个安全余量。比如最长任务是30分钟,那么访问令牌有效期至少应该大于30分钟,同时配合刷新机制。

七、完整排查步骤(附命令和配置)

假设你接手一个Blue Prism登录失效的问题,建议按顺序排查:

7.1 检查SSO侧日志

在Keycloak里,进入 Realm Settings -> Events 查看认证事件。搜索Blue Prism机器人的客户端ID。如果看到 REFRESH_TOKEN_INVALID,说明刷新令牌失效。如果看到 CODE_TO_TOKEN_ERROR,说明授权码换取令牌失败。

7.2 检查Blue Prism日志

Blue Prism安装目录下Logs文件夹里,有SchedulerRuntime日志。搜索关键词401Authentication failed。一般会直接告诉你哪个URL返回401。

7.3 验证令牌有效性

用curl模拟Blue Prism的请求。拿一个缓存中的访问令牌去调用API:

# 先获取一个访问令牌(假设已经写好了一个脚本获取)
ACCESS_TOKEN=$(curl -X POST http://sso.example.com/oauth/token \
  -H "Content-Type: application/json" \
  -d '{"username":"robot","password":"yourpass","grant_type":"password"}' | jq -r .access_token)

# 调用业务API
curl -i http://bp-api.example.com/api/tasks \
  -H "Authorization: Bearer $ACCESS_TOKEN"

如果返回401,再解析令牌内容看看过期时间:

# 把令牌第一部分base64解码
echo "$ACCESS_TOKEN" | cut -d '.' -f 1 | base64 -d 2>/dev/null

7.4 检查时钟偏差

# 在Blue Prism服务器上执行
w32tm /stripchart /computer:ntp.example.com /samples:3

# 对比SSO服务器时间
date && ssh sso-server date

如果偏差超过5秒,配置NTP同步。Windows下:

# 配置NTP服务器
w32tm /config /manualpeerlist:"ntp.example.com" /syncfromflags:manual /reliable:yes /update

# 强制同步
w32tm /resync

7.5 清理本地缓存

找到Blue Prism程序数据目录,备份后删除缓存文件。注意路径根据版本不同:

# 旧版本
rm -rf "/c/ProgramData/Blue Prism Limited/Automate/*.cache"

# 新版本(v6+)
rm -rf "/c/ProgramData/Blue Prism Limited/Blue Prism/Automate/*.token"

删除后重启Blue Prism服务,它会强制重新登录。

7.6 调整SSO令牌策略

在Keycloak管理界面配置客户端Client details -> Advanced Settings -> Access Token LifespanRefresh Token Lifespan。推荐设置:

{
  "accessTokenLifespan": "300",
  "refreshTokenLifespan": "43200",
  "refreshTokenExpiration": "900",
  "revokeRefreshToken": true
}

这里的 refreshTokenExpiration 是刷新令牌的绝对过期时间,900秒表示15分钟没有使用就过期,这样既安全又灵活。

7.7 添加监控告警

写一个脚本定期检测Blue Prism会话状态:

#!/bin/bash
# 每5分钟检查一次Blue Prism机器人是否在线
# 依赖curl和jq

BASE_URL="http://bp-control.example.com/api"
API_KEY="your_api_key"

for robot in "robot1" "robot2"; do
  status=$(curl -s -X GET "$BASE_URL/robots/$robot/status" \
    -H "X-API-Key: $API_KEY" | jq -r .status)
  echo "$(date) $robot status=$status"
  if [ "$status" != "Ready" ]; then
    echo "告警:$robot 状态异常" | mail -s "Blue Prism机器人离线" ops@example.com
  fi
done

这个脚本可以放到cron里跑。实际生产环境,建议直接接入Prometheus等监控,让告警更灵活。

八、关联技术:会话固定保护与刷新令牌轮换

在排障过程中,我还发现团队对刷新令牌轮换不了解。标准OAuth2.0推荐每次使用刷新令牌后,就颁发一个新的刷新令牌,旧的立即失效。这样即使某个刷新令牌泄露,攻击者只能用一次。

Blue Prism如果配置不当,可能是用一个固定的刷新令牌反复使用。这时候需要在SSO侧开启轮换。以Keycloak为例,在客户端高级设置里有个 Revoke refresh token 开关,开启后,旧刷新令牌立刻失效。

示例:把一个固定刷新令牌连续用两次,第二次会报错。这有助于检测令牌泄露。

# 第一次刷新
curl -X POST http://sso.example.com/oauth/token \
  -H "Content-Type: application/json" \
  -d '{"grant_type":"refresh_token","refresh_token":"old-refresh-token"}'
# 返回200,同时返回新的refresh_token

# 再次用旧refresh_token刷新
curl -X POST http://sso.example.com/oauth/token \
  -H "Content-Type: application/json" \
  -d '{"grant_type":"refresh_token","refresh_token":"old-refresh-token"}'
# 返回400,因为旧令牌已被吊销

如果Blue Prism没有正确处理“刷新令牌被轮换”的响应,它可能还保存着旧值,导致下一次刷新失败。这时需要在Blue Prism的配置里找到“启用刷新令牌轮换”的选项。遗憾的是,Blue Prism的文档并没有明确提到这个,所以我们在实现时需要让Blue Prism每次刷新后,从响应体中提取新的刷新令牌并更新存储,这是我们的代码逻辑层面能做好的。

九、文章总结

排查Blue Prism的SSO登录失效问题,核心就是追踪令牌整个生命周期:签发、使用、刷新、吊销。令牌太短,任务会被打断;令牌太长,安全风险高;缓存策略不对,刷新会失败。我们既要理解SSO服务端的配置,也要理解Blue Prism本地的缓存行为,还要注意时钟同步和网络中间层。

我的经验是:先从日志入手,分清是访问令牌过期还是刷新令牌失效。然后检查两家系统的时间是否同步,接着看缓存文件是否被清理或锁定。最后才是调整有效期参数。调整时不要拍脑袋,要根据实际最长任务耗时、任务频率、安全要求来算。

如果你也遇到了类似问题,希望这篇文章能帮你理清思路。别慌,SSO登录失效不是一个玄学问题,它背后一定有明确的原因。按照链路一步步排查,肯定能解决。

最后补充一句:企业里任何认证配置变更,都要先在测试环境模拟无人值守场景跑满一个周期(至少覆盖最长任务时间+刷新时间),再上生产。