pac4j 5.3.1 isAuthorized方法中SessionStore作用及403报错咨询
问题背景
处理外部请求时触发403 forbidden错误,经排查确认鉴权失败点为pac4j框架CsrfAuthorizer.class中的isAuthorized方法。对比pac4j 4.3.1与5.3.1版本实现,两个版本的isAuthorized逻辑差异明显,核心代码如下:
VERSION 4.3.1 核心逻辑
boolean checkRequest = this.checkAllRequests || ContextHelper.isPost(context) || ContextHelper.isPut(context) || ContextHelper.isPatch(context) || ContextHelper.isDelete(context); if (!checkRequest) { return true; } else { String parameterToken = (String)context.getRequestParameter(this.parameterName).orElse((Object)null); String headerToken = (String)context.getRequestHeader(this.headerName).orElse((Object)null); Optional<String> sessionToken = context.getSessionStore().get(context, "pac4jCsrfToken"); return sessionToken.isPresent() && (((String)sessionToken.get()).equals(parameterToken) || ((String)sessionToken.get()).equals(headerToken)); }
VERSION 5.3.1 核心逻辑
public boolean isAuthorized(final WebContext context, final SessionStore sessionStore, final List<UserProfile> profiles) { final var checkRequest = checkAllRequests || isPost(context) || isPut(context) || isPatch(context) || isDelete(context); if (checkRequest) { final var parameterToken = context.getRequestParameter(parameterName).orElse(null); final var headerToken = context.getRequestHeader(headerName).orElse(null); LOGGER.debug("parameterToken: {}", parameterToken); LOGGER.debug("headerToken: {}", headerToken); final var sessionPreviousToken = sessionStore.get(context, Pac4jConstants.PREVIOUS_CSRF_TOKEN); final var sessionToken = sessionStore.get(context, Pac4jConstants.CSRF_TOKEN); final var sessionDate = sessionStore.get(context, Pac4jConstants.CSRF_TOKEN_EXPIRATION_DATE); if (sessionStore.getSessionId(context, false).isPresent()) { sessionStore.set(context, Pac4jConstants.PREVIOUS_CSRF_TOKEN, null); } // 所有校验全程执行,条件判断改为逻辑运算,字符串比较替换为哈希比较以防御时序攻击 final var hasSessionData = sessionToken.isPresent() & sessionDate.isPresent(); final var previousToken = (String) sessionPreviousToken.orElse(""); LOGGER.debug("previous token: {}", previousToken); final var token = (String) sessionToken.orElse(""); LOGGER.debug("token: {}", token); final var isGoodCurrentToken = hashEquals(token, parameterToken) | hashEquals(token, headerToken); final var isGoodPreviousToken = hashEquals(previousToken, parameterToken) | hashEquals(previousToken, headerToken); final var isGoodToken = isGoodCurrentToken | isGoodPreviousToken; final var expirationDate = (Long) sessionDate.orElse(0L); final var now = new Date().getTime(); final var isDateExpired = expirationDate < now; if (!hasSessionData | !isGoodToken | isDateExpired) { return false; } } return true; }
当前5.3.1版本中,上述校验逻辑的判断条件始终不满足,导致isAuthorized方法一直返回false,鉴权不通过。
SessionStore在CsrfAuthorizer中的作用
SessionStore是pac4j抽象的会话存储层,负责跨请求保存、读取CSRF校验所需的核心状态,在5.3.1版本的CSRF校验流程中,它承担三个核心职责:
- 存储当前生效的CSRF令牌(
Pac4jConstants.CSRF_TOKEN),作为服务端留存的校验基准值 - 存储上一个周期的CSRF令牌(
Pac4jConstants.PREVIOUS_CSRF_TOKEN),兼容令牌轮换场景下的并发请求校验,避免合法请求因令牌刷新被误拦截 - 存储CSRF令牌的过期时间戳(
Pac4jConstants.CSRF_TOKEN_EXPIRATION_DATE),实现令牌有效期校验,降低令牌泄露后的风险
和4.3.1版本只存单个CSRF令牌的逻辑相比,5.x版本的SessionStore存储内容更多,校验维度也更全。
排查解决思路
按照5.3.1版本返回false的三个触发条件,按优先级逐项排查:
- 先排查会话数据是否存在
打开DEBUG日志,查看日志中打印的token、previous token值,如果两个值都为空,说明CSRF令牌根本没有写入SessionStore:- 检查是否配置了
CsrfTokenGenerator,且该生成器在请求进入CsrfAuthorizer之前已经执行,完成令牌写入 - 检查SessionStore的实现是否正确:如果是分布式部署场景,确认会话共享配置正常,请求携带的会话ID能正确关联到存储的CSRF数据;如果是无状态API场景,确认使用的是适配无状态场景的SessionStore实现,不会出现跨请求数据丢失
- 检查请求是否正常携带了会话标识(Cookie/Header中的会话ID),如果会话标识缺失,每次请求都会生成新会话,自然读不到之前存入的CSRF令牌
- 检查是否配置了
- 再排查令牌匹配是否正常
如果日志中能看到Session里存的token值,对比请求携带的parameterToken、headerToken值:- 确认前端/调用方是否正确从Cookie或者响应头中拿到CSRF令牌,在POST/PUT/PATCH/DELETE请求中通过请求参数或者约定的请求头携带
- 如果是令牌轮换场景,确认上一个令牌的清空逻辑没有异常:当前逻辑会在会话存在时把
PREVIOUS_CSRF_TOKEN置空,要确保令牌生成器在写入新令牌时,会把旧令牌正确赋值到PREVIOUS_CSRF_TOKEN字段,避免并发请求校验失败 - 确认没有网关、反向代理在转发过程中修改/丢弃了携带CSRF令牌的请求头或请求参数
- 最后排查令牌过期问题
如果令牌值匹配正常,查看日志中打印的expirationDate时间戳,和当前服务器时间对比:- 确认CSRF令牌的有效期配置合理,不会出现令牌刚生成就过期的情况
- 检查多节点部署时各服务器的系统时间是否同步,时间偏移会导致未过期的令牌被误判为过期
内容的提问来源于stack exchange,提问作者Linidu Praneeth Gunathilaka
相关产品推荐
相关产品推荐

