本地调试入口

编译keycloak源代码某个包
mvn package -Denforcer.skip=true -Dmaven.test.skip=true
mvn clean install -Dskip=true
- 部署到私服,建议源码修改后,应该部署到私服,这样其它应用在部署时,也有可以使用修改后的代码了
$ mvn deploy -Denforcer.skip=true -Dmaven.test.skip=true
当用户已经在浏览器登录,在使用自动登录接口(或者之前同时打开两个登录页)这时为了保证用户的登录状态,后面的登录请求会被拦截,跳转到“首页”
- 如果非iframe的情况,使用下面的代码可以实现问题,在org.keycloak.services.resources.LoginActionsServiceChecks
 .checkNotLoggedInYet()和SessionCodeChecks.initialVerifyAuthSession()方法添加302跳转
- 对于iframe里面的登录页,需要考虑如何在顶级窗口实现重定向,目前添加js重定向解决了这个iframe问题
// TODO: 当用户已经登录了,直接跳到首页
Response.status(302).header(HttpHeaders.LOCATION, "https://www.xxx.com").build();


js重定向解决了同时多个iframe登录框,在其中一个登录,另一个在本iframe重定向问题

修改url中有特殊符号的问题
修改org.keycloak.protocol.oidc.utils.RedirectUtils.removeUrlSpaceParams方法,将特殊符号进行编码

生成token时添加日志

修改code to token的缓存类型
默认使用BasicCache,应该是本地缓存,查通过查看TokenEndpoint,发现是分布式缓存

LOGIN事件的个性化配置
org.keycloak.services.managers.AuthenticationManager的方法nextRequiredAction和actionRequired,添加了LOGIN事件的个性化字段

code_to_token时,去掉了clientId限制
- code_to_token时,去掉了clientId必须一致的条件的检验,这样不同客户端在通过code换token时,可以减少与kc交互交次数
- 方法变更:TokenEndpoint.codeToToken()
  
code_to_token时对浏览器sessionId操作
- 
当code to token出现错误时,添加了清空浏览器里sessionId在kc的会话信息,但如果是httpclient的调用,咱们是拿不到客户端浏览器的cookie的 
- 
Code '%s' already used for userSession 
 org.keycloak.protocol.oidc.util.OAuth2CodeParser.parseCode这块添加了clientId的日志描述

修改kc管理后台session列表由于信息不全报错的问题
- 主要是ModelToRepresentation报错了,应该是client_id为空引起的,像refresh_token达到次数会引起会话的client_id为空,但sessionId还是在线的。
- 这块将异常报错复原了,如果不报错,将会出现大量session从数据库加载,导致数据库崩盘

验证token去掉协议名的限制
- 对在线token的校验,去掉了https和http的校验,只要后面域名相同就是ISS(Issuer)相同就行,这块在验证token时会用到,另外在适配器集成中,每人请求加载前,也会用到它


调查code码被占用问题(生产了重复的code码)
1 code的生产

2 code的校验

- 解决同时打开两个登录窗口,在第一个窗口登录后,在第二个窗口再登录一次,会出现“您已经登录”的页面
- 解决:发生上面的情况后,直接跳到v6首页
- LoginActionsServiceChecks.checkNotLoggedInYet()方法
- SessionCodeChecks.initialVerifyAuthSession()方法
 
让IdentityProviderMapper实现的类型,自动为社区登录执行delegateUpdateBrokeredUser
- 修改源代码:org.keycloak.services.resources.updateFederatedIdentity()
- 添加了代码逻辑,实现了按需自动执行
     //对已有用户进行更新,注意,可能会覆盖用户的其它属性FederatedIdentityModel finalFederatedIdentityModel = federatedIdentityModel;sessionFactory.getProviderFactoriesStream(IdentityProviderMapper.class).map(IdentityProviderMapper.class::cast).map(mapper -> Arrays.stream(mapper.getCompatibleProviders()).filter(type -> Objects.equals(finalFederatedIdentityModel.getIdentityProvider(), type)).map(type -> mapper).findFirst().orElse(null)).filter(Objects::nonNull).collect(Collectors.toMap(IdentityProviderMapper::getId, Function.identity())).forEach((a, b) -> {IdentityProviderMapper target = (IdentityProviderMapper) sessionFactory.getProviderFactory(IdentityProviderMapper.class, a);IdentityProviderMapperModel identityProviderMapperModel = new IdentityProviderMapperModel();identityProviderMapperModel.setConfig(new HashMap<>());identityProviderMapperModel.setSyncMode(IdentityProviderMapperSyncMode.FORCE);identityProviderMapperModel.setId(a);identityProviderMapperModel.setIdentityProviderMapper(finalFederatedIdentityModel.getIdentityProvider());identityProviderMapperModel.setIdentityProviderAlias(finalFederatedIdentityModel.getIdentityProvider());try {if (!Objects.equals(target.getId(), UsernameTemplateMapper.PROVIDER_ID)) {IdentityProviderMapperSyncModeDelegate.delegateUpdateBrokeredUser(session, realmModel, federatedUser,identityProviderMapperModel,context, target);}} catch (RuntimeException ex) {}});
- 添加一个例子,实现社区登录的类型自动存储到用户属性loginType中,getCompatibleProviders()方法中绑定了
 IdentityProviderMapper.ANY_PROVIDER,所以在每个社区登录后,它都会被执行
- 新用户不会绑定这个,已绑定的用户才执行这个方法,原因是syncModel为Force,表示当有用户后,会强制更新它
public class V6UserAttributeMapper extends AbstractJsonUserAttributeMapper {public static final String PROVIDER_ID = "v6-user-attribute-mapper";private static final String[] cp = new String[] {IdentityProviderMapper.ANY_PROVIDER};@Overridepublic String[] getCompatibleProviders() {return cp;}@Overridepublic String getId() {return PROVIDER_ID;}@Overridepublic void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user,IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {logger.info("updateBrokeredUser user info...");user.setSingleAttribute("loginType", mapperModel.getIdentityProviderAlias());}
}
- 登录后更新用户属性loginType

登录后,将loginType添加到refresh_token中
解决由于kc的refresh_token不支持自定义属性,所以在登录后,将loginType添加到refresh_token中,这样在refresh_token时,就可以获取到loginType了
- 实现逻辑:将当前loginType添加到当前refresh_token,在下次刷新token时,将refresh_token里的loginType取出来,覆盖到新的access_token里.
- org.keycloak.protocol.oidc.TokenManager.validateToken()
- org.keycloak.protocol.oidc.TokenManager.build()
用户session_state生成方式
- org.keycloak.models.sessions.infinispan.createUserSession()
解决用户浏览器因为丢失keycloak_identity而keycloak_session_id有并且是在线的,导致无法登录的问题
- 在方法AuthorizationEndpointBase.createAuthenticationSession()添加了判断逻辑,没有keycloak_identity就重新根据session_id再生成一个到cookie里
Cookie cookie = CookieHelper.getCookie(headers.getCookies(), KEYCLOAK_IDENTITY_COOKIE);
if (cookie == null) {cookie =CookieHelper.getCookie(headers.getCookies(), KEYCLOAK_IDENTITY_COOKIE + CookieHelper.LEGACY_COOKIE);if (cookie == null) {AuthenticationManager.createLoginCookie(session, realm, user, userSession, session.getContext().getUri(),session.getContext().getConnection());}
}
- 关于对loginType和登录事件的修改
 请查看TODO: 20230406的注释代码
- 涉及到以下动作会触发的事件,会添加我们扩展的属性
- 共享登录
- code换token
- 刷新token
 
关于OTP提供商的调研
- OTP提供商的策略:org.keycloak.models.OTPPolicy,目前支持FreeOTP和GoogleAuthenticator

关于keycloak-services项目添加第三方jar包的问题
我们例如将org.infinispan这个包,在kc里也是一个module,引用到keycloak-services项目,它在启动时会报错,告诉找不到这个org.infinispan.Cache类,类似这种类无法找到的错误。
Uncaught server error: java.lang.NoClassDefFoundError: org/infinispan/Cacheat org.keycloak.keycloak-services@14.0.0//org.keycloak.protocol.oidc.TokenManager.checkTokenValidForIntrospection(TokenManager.java:494)解决思路,在module.xml中,添加对应的模块即可
从keycloak容器里将/opt/jboss/keycloak/modules/system/layers/keycloak/org/keycloak/keycloak-core/main/module.xml复制出来,在文件的dependencies节点下添加依赖,如
<module name="org.infinispan"/>
- 修改Dockerfile文件,将这个module.xml文件也复制到上面的容器目录,覆盖原来的文件
- 重新构建镜像,启动容器,问题解决
自动登录接口同一浏览器添加踢出之前登录的逻辑
- org.keycloak.protocol.AuthorizationEndpointBase.handleBrowserAuthenticationRequest()

从carsi网站过来的用户,会带有carsi-auto这个关键字,也应该要踢出之前的登录
- org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider.Endpoint.authResponse()

刷新token和通过code换token逻辑中,添加infinispan缓存的逻辑,键key是sessionId+clientId,作用是当用户的角色变更后,用户校验token直接返回false,让迫使用户重新去刷新token
// TODO: xxx_user_modify_role 需要添加逻辑,去检索事件中是否包括了权限变更的用户
- 验证token: org.keycloak.protocol.oidc.TokenManager.checkTokenValidForIntrospection()
- 刷新token: org.keycloak.protocol.oidc.endpoints.TokenEndpoint.refreshTokenGrant
 ,这块因为相同的event对象,所以代码迁移到keycloak-services-event-kafka
- code换token: org.keycloak.protocol.oidc.endpoints.TokenEndpoint.codeToToken()
 ,这块因为相同的event对象,所以代码迁移到keycloak-services-event-kafka
用户权限更新后,通和逻辑整理
- kc服务端收到REALM_ROLE_MAPPING或者USER_ROLE_CHANGE事件后,向infinispan缓存里添加一个key,key是
 xxx_user_modify_role_{userId},value是空
- 它有缓存有效期与access_token的相同,目前为30分钟
- 当用户进行code换token或者刷新token时,根据当前用户id,去上面缓存中找,如果查找到,说明这个用户的权限发生了变更
- 找到后,向这个缓存xxx_user_modify_role_{userId}添加value,value格式是{sessionId}_{clientId},就是用户在哪个浏览器
 哪个客户端访问
- 当用户调用验证token接口时,如果在xxx_user_modify_role_{userId}中没有找到这个value{sessionId}_{clientId},就验证失败
- 当验证失败后,返回401,用户再去刷新token,向xxx_user_modify_role_{userId}中添加对应的value, 保持下次验证会成功
获取IP地址的方法修改
// TODO: 优化登录事件中,获取ipAddress的逻辑,改为real-ip有限
- org.keycloak.events.EventBuilder.ipAddress()进行了重新赋值
- org.keycloak.services.resources.admin.AdminEventBuilder.AdminEventBuilder()初始化时,使用real-ip
验证token逻辑的抛下,解析session idle和session max的逻辑
- org.keycloak.services.managers.AuthenticationManager.isSessionValid
- SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS是个时间戳口,它是120秒
  public static boolean isSessionValid(RealmModel realm, UserSessionModel userSession) {if (userSession == null) {logger.debug("No user session");return false;}int currentTime = Time.currentTime();// Additional time window is added for the case when session was updated in different DC and the update to current DC was postponedint maxIdle = userSession.isRememberMe() && realm.getSsoSessionIdleTimeoutRememberMe() > 0 ?realm.getSsoSessionIdleTimeoutRememberMe() : realm.getSsoSessionIdleTimeout();int maxLifespan = userSession.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 ?realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan();boolean sessionIdleOk =maxIdle > currentTime - userSession.getLastSessionRefresh() - SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS;boolean sessionMaxOk = maxLifespan > currentTime - userSession.getStarted();return sessionIdleOk && sessionMaxOk;
}
session idle和session max的逻辑生效,如果修改refresh_token生成时的校验逻辑
org.keycloak.protocol.oidc
.TokenManager.refreshAccessToken()方法中的代码,将verifyRefreshToken方法参数中的checkExpiration改成false
// TODO: 完善实现了在线校验时,session idle和session max的功能
session idle(空闲过期时间)和session max(最大过期时间)不相等时,产生的问题
描述与解决思路
- session idle会作为刷新token的过期时间
- 当这个时间到达后,不能再刷新token了,但是,session还是在线的
- 是否需要在到达这个时间后,将会话删除?
- 如果真要删除的话,可能产生的问题就是session max的时间还没到,但是session已经被删除了,这样就会导致session max的时间不准确了
- 但如果session idle到达,并且token没有成功刷新,这说明用户空闲了,这时session是可以删除的,与4不矛盾
- 解决方法
 *[x] 在session idle到达后,将session删除,应该就解决问题了
 *[x] 或者在生成code之前,判断它的session idle是否到期,如果到期,将会话删除,不能生成code
用户会话过期,清理用户会话的逻辑调整

- org.keycloak.services.scheduled.ScheduledTaskRunner # 默认900秒(15分钟)执行一次
- org.keycloak.services.scheduled.ClearExpiredUserSessionsTask
- org.keycloak.models.map.authSession.removeExpired
- 需要添加空闲过期时间的判断,如果到期,就删除会话
- 这块需要看如何手动清除,因为默认的,infinispan中的session,有自己的过期时间,按着过期时间自动清除的
- 咱们相当于,如何让它按着咱们的时间(这个时间经过了session idle的时间)来清除的
 
 
- org.keycloak.services.scheduled.ClearExpiredUserSessionsTask
- 通过上面的分析,直接从infinispan中获取过期的session,并删除不太可能,人家通知初始化的过期时间自行维护的,而且这种过期时间,是session
 max,而咱们的过期时间是可变的,它可能是一个session idle,也可能是session max,这与用户是否在idle时间内是否有操作有关
生成code时,添加session idle的判断
- 如果不添加这个判断,将会出现的问题是,当session idle和session max设置不同时,当session
 idle到期后,用户的会话不会被删除,导致刷新token和申请code码换token,两块产生的token逻辑不一样,最终效果就是,code可以换回来token,但token在校验时是
 session not active这样的错误。
- org.keycloak.protocol.AuthorizationEndpointBase.createAuthenticationSession()方法

修改org.keycloak.theme.DefaultThemeSelectorProvider文件getThemeName()方法,添加了url中皮肤参数theme
MultivaluedMap<String, String> query = session.getContext().getUri().getQueryParameters();
if(query.containsKey("theme")){
name=query.getFirst("theme");
}else{}
登录跨域支持
org.keycloak.protocol.oidc.endpoints.TokenEndpoint.resourceOwnerPasswordCredentialsGrant(),返回值添加跨域代码
return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).allowAllOrigins().build();
登录回调地址中添加loginType这个参数
- org.keycloak.services.resources.IdentityBrokerService.finishBrokerAuthentication()方法添加对loginType的操作
- org.keycloak.protocol.oidc.OIDCLoginProtocol.authenticated()方法中,获取loginType,并添加到回调路径的URL参数中


社区登录成功后,绑定用户信息,修改FEDERATED_IDENTITY_LINK的内容
- org.keycloak.services.resources.IdentityBrokerService.afterFirstBrokerLogin方法
- 添加自定义事件元素:event.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getBrokerUserId());
刷新token时,如果用户有required action,抛出异常
- org.keycloak.protocol.oidc.TokenManager.validateToken()方法
    //TODO:刷新token时,如果用户有required action,抛出异常if (user.getRequiredActionsStream().findAny().isPresent()) {throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User has required action","User has required action");}
社区登录state的自定义
社区登录回调state参数,支持4个参数
- org.keycloak.broker.provider.util.IdentityBrokerState类中encoded方法,将
 String[] decoded = DOT.split(encodedState, 4);从3改成4

建立社区登录地址时添加自定义state参数
- AbstractOAuth2IdentityProvider类中createAuthorizationUrl方法,修改state参数的拼接
 String state = request.getState().getEncoded();if(request.getAuthenticationSession().getAuthNote("g") !=null&&request.getAuthenticationSession().getAuthNote("g").trim() !=""){
state =state +"."+request.getAuthenticationSession().getAuthNote("g");}

在认证成功后federatedIdentityContext上下文添加参数
- AbstractOAuth2IdentityProvider类中Endpoint.authResponse方法,再返回之前为federatedIdentity添加groupId参数
// 添加集团代码
String[] decoded = DOT.split(state, 4);
if(decoded.length ==4){federatedIdentity.setUserAttribute("groupId",decoded[3]);
}

登录超时的提示语调整
- keycloak-themes/themes/base/login/messages/messages_en.properties文件
- 修改loginTimeout的值即可
社区登录页{provider}/login页添加idp和login_type参数
- org.keycloak.services.Urls类identityProviderAuthnRequest()方法,添加idp参数的追加
- org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator类中redirect()
 方法构建登录页重定向参数,添加loginType和idp两个参数
/auth/realms/xxx/protocol/openid-connect/userinfo接口添加session_state属性返回值
- org.keycloak.protocol.oidc.OIDCLoginProtocolService类中的issueUserInfo()方法
- 添加跨域支持allowedOrigins("*")
- 解析当前token,并添加session_state
claims.put("session_state", userSession.getId());// 添加当前的session信息
刷新token时,出现Session not active或者Invalid refresh token
- Session not active 表示用户的session已经过期了,需要重新登录,返回400
- Invalid refresh token 表示refresh token不正确,可能token被篡改了,需要重新登录,返回400
- 刷新token时,只有一种情况会返回401,就是client_secret错误时,Client secret not provided in request
- org.keycloak.protocol.oidc.TokenEndpoint.refreshTokenGrant()方法,添加refresh_token验证不通过,会走这个catch逻辑,大多数情况httpcode都是400
catch (OAuthErrorException e) {logger.trace(e.getMessage(), e);// KEYCLOAK-6771 Certificate Bound Tokenif (MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC.equals(e.getDescription())) {event.error(Errors.NOT_ALLOWED);throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.UNAUTHORIZED);} else {event.error(Errors.INVALID_TOKEN);throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);}}
社区登录添加loginType为社区的idp
- 情况一,未绑定用户,走first flow
- 情况二,绑定了用户,下次再登录,会走after flow
first flow
org.keycloak.services.resources.IdentityBrokerService.authenticated()方法,添加代码
authenticationSession.setUserSessionNote("loginType",context.getIdpConfig().getProviderId());

after flow
org.keycloak.services.resources.IdentityBrokerService.authenticated()方法,添加代码
authenticationSession.setUserSessionNote("loginType",context.getIdpConfig().getProviderId());

社区登录microsoft修改FEDERATED_IDENTITY_LINK的BUG
- 用户第一次使用社区来绑定本地KC用户时,需要为社区用户的unionId赋值到BrokeredIdentityContext对象
- 在MicrosoftIdentityProvider.extractIdentityFromProfile()方法,添加了user.setBrokerUserId(id);
- 如果其它社区登录需要集成,也需要手动添加上面的代码
- IdentityBrokerService.afterFirstBrokerLogin()方法,添加用户第一次绑定社区时FEDERATED_IDENTITY_LINK的扩展信息
管理后台-用户检索-改为用户名精确或者邮箱精确
- org.keycloak.models.jpa.JapUserProvider类
- searchForUserStream(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults)方法
个人中心绑定社区用户
代码注释,去掉权限的控制:IdentityBrokerService.clientInitiatedAccountLinking()方法,注册下面代码
- 出错信息:not_allowed
//      if (!userAccountRoles.contains(manageAccountRole)) {
//        RoleModel linkRole = accountService.getRole(AccountRoles.MANAGE_ACCOUNT_LINKS);
//        if (!userAccountRoles.contains(linkRole)) {
//          event.error(Errors.NOT_ALLOWED);
//          UriBuilder builder = UriBuilder.fromUri(redirectUri)
//              .queryParam(errorParam, Errors.NOT_ALLOWED)
//              .queryParam("nonce", nonce);
//          return Response.status(302).location(builder.build()).build();
//        }
//      }
- 出错信息:insufficientPermissionMessage
//    if (!authenticatedUser.hasRole(this.realmModel.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)
//        .getRole(AccountRoles.MANAGE_ACCOUNT))) {
//      return redirectToErrorPage(authSession, Response.Status.FORBIDDEN, Messages.INSUFFICIENT_PERMISSION);
//    }
- 修改非account客户端的错误页逻辑,直接将错误编码带着来源页
- IdentityBrokerService.redirectToErrorWhenLinkingFailed()
 private Response redirectToErrorWhenLinkingFailed(AuthenticationSessionModel authSession, String message,Object... parameters) {if (authSession.getClient() != null &&authSession.getClient().getClientId().equals(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)) {return redirectToAccountErrorPage(authSession, message, parameters);} else {//  return redirectToErrorPage(authSession, Response.Status.BAD_REQUEST, message, parameters); // Should rather redirect to app instead and display error here?// 当出现错误,将错误消息直接带到来源页URI errUrl =UriBuilder.fromUri(authSession.getRedirectUri()).queryParam("error", message).build();return Response.status(302).location(errUrl).build();}
}
首次登录社区,并完成老用户的绑定,向FEDERATED_IDENTITY_LINK事件添加corpId
- org.keycloak.services.resources.IdentityBrokerService.afterFirstBrokerLogin(AuthenticationSessionModel authSession)
 方法中添加代码
event.detail(CORP_ID,context.getUserAttribute(CORP_ID)); // 这块与认证页有跨页,所以authSession.getAuthNote(CORP_ID)无法获取到corpId,所以临时存在userAttribute对应的内存中,并存持久化到数据库
- 具体的AbstractOAuth2IdentityProvider子类中Endpoint.authResponse()方法中添加代码
//11.3.0之后改成这样了,去掉了code字段
federatedIdentity.setUserAttribute("corpId",federatedIdentity.getUserAttribute(DINGTALK_CORP_ID));
// authSession.setAuthNote("corpId",federatedIdentity.getUserAttribute(DINGTALK_CORP_ID));
已登录的用户去绑定社区用户时,向FEDERATED_IDENTITY_LINK事件添加corpId
- org.keycloak.services.resources.IdentityBrokerService.performAccountLinking()方法中添加代码
- 这块为了统一,也使用getUserAttribute即可
this.event.user(authenticatedUser).detail(Details.USERNAME, authenticatedUser.getUsername()).detail(Details.IDENTITY_PROVIDER, newModel.getIdentityProvider()).detail(Details.IDENTITY_PROVIDER_USERNAME, newModel.getUserName()).detail(CORP_ID,federatedIdentity.getUserAttribute(CORP_ID))// 从已经登录的用户点社区登录,绑定事件中添加corpId.success();
parseSessionCode报错
AuthenticationSessionManager.getCurrentAuthenticationSession authSessionCookies这块添加日志,看是否kc可以获取到浏览器cookie中的auth_session_id,如果获取不到会出现下面错误
ERROR [org.keycloak.services.resources.IdentityBrokerService] (default task-1709) unexpectedErrorHandlingRequestMessage: javax.ws.rs.WebApplicationException: HTTP 400 Bad Requestat org.keycloak.keycloak-services@14.0.0//org.keycloak.services.resources.IdentityBrokerService.parseSessionCode(IdentityBrokerService.java:1225)at org.keycloak.keycloak-services@14.0.0//org.keycloak.services.resources.IdentityBrokerService.performLogin(IdentityBrokerService.java:419)at jdk.internal.reflect.GeneratedMethodAccessor673.invoke(Unknown Source)at java.base/jdk.internal.reflect.DelegatingMethodAccessorImp
auth_session_id解析过程
- auth_session_id它是由session_state.nodeId组成的,session_state是用户会话的id,nodeId是kc集群节点的标识符,它们之间用点号分隔开,比如
 5e161e00-d426-4ea6-98e9-52eb9844e2d7.node1,这样在集群环境下,可以将请求路由到对应的节点上去。
- KEYCLOAK_IDENTITY它是用户登录之后产生的,它是一个jwt的token,包含最基础的会话信息
eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyMTFiNDI0OC02OGZjLTQwNDQtYjM4Ny1kMGNjOTI3ZWI1MmIifQ.eyJleHAiOjE3NjE5MTY2OTgsImlhdCI6MTc2MTg4MDY5OCwianRpIjoiYTIwNDFjNTgtZmE5NC00MDA4LTg3YzEtZTI1MWEwMmZmNjk2IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguNC4yNjo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsInN1YiI6IjZlMjZjNGQwLTZiMzktNDllYy1hNWE0LWI3MzBkOTA3ZjM3ZiIsInR5cCI6IlNlcmlhbGl6ZWQtSUQiLCJzZXNzaW9uX3N0YXRlIjoiOGI5YjgyMDUtMTcyYi00YzFiLWFmNzYtNGI1Yjk4ZTE4YzY4Iiwic3RhdGVfY2hlY2tlciI6InNGdDkxOFBWcnFDaGxWNV8wYm5RY0pxZVJ2dlYyS3hQbU9lRTBfV3dPRjQifQ.KRViHyjY54UhswmXnCCMpSRY9SoV2k3yANXfUtQpLvc{"exp": 1761916698,"iat": 1761880698,"jti": "a2041c58-fa94-4008-87c1-e251a02ff696","iss": "http://192.168.4.26:8080/auth/realms/master","sub": "6e26c4d0-6b39-49ec-a5a4-b730d907f37f","typ": "Serialized-ID","session_state": "8b9b8205-172b-4c1b-af76-4b5b98e18c68","state_checker": "sFt918PVrqChlV5_0bnQcJqeRvvV2KxPmOeE0_WwOF4"
}
- 生成一个auth_session_id到浏览器cookie中
- IdentityBrokerService.clientInitiatedAccountLinking()方法中,调用
 AuthenticationSessionManager.getCurrentAuthenticationSession()方法,解析浏览器cookie中的auth_session_id
AuthenticationManager.AuthResult cookieResult =AuthenticationManager.authenticateIdentityCookie(session, realmModel, true);
//...
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client);// Refresh the cookie
new AuthenticationSessionManager(session).setAuthSessionCookie(userSession.getId(), realmModel);
- AuthenticationManager类中的authenticateIdentityCookie用来生成一个AuthResult对象,如果用户已经登录(有KEYCLOAK_IDENTITY_COOKIE)这个auth_session_id就会被使用
public static AuthResult authenticateIdentityCookie(KeycloakSession session, RealmModel realm, boolean checkActive) {Cookie cookie =CookieHelper.getCookie(session.getContext().getRequestHeaders().getCookies(), KEYCLOAK_IDENTITY_COOKIE);if (cookie == null || "".equals(cookie.getValue())) {logger.debugv("Could not find cookie: {0}", KEYCLOAK_IDENTITY_COOKIE);return null;}String tokenString = cookie.getValue();AuthResult authResult =verifyIdentityToken(session, realm, session.getContext().getUri(), session.getContext().getConnection(),checkActive, false, null, true, tokenString, session.getContext().getRequestHeaders(),VALIDATE_IDENTITY_COOKIE);if (authResult == null) {expireIdentityCookie(realm, session.getContext().getUri(), session.getContext().getConnection());expireOldIdentityCookie(realm, session.getContext().getUri(), session.getContext().getConnection());return null;}authResult.getSession().setLastSessionRefresh(Time.currentTime());return authResult;}
- AuthenticationSessionManager文件
/*** @param authSessionId decoded authSessionId (without route info attached)* @param realm*/public void setAuthSessionCookie(String authSessionId, RealmModel realm) {UriInfo uriInfo = session.getContext().getUri();String cookiePath = AuthenticationManager.getRealmCookiePath(realm, uriInfo);boolean sslRequired = realm.getSslRequired().isRequired(session.getContext().getConnection());StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class);String encodedAuthSessionId = encoder.encodeSessionId(authSessionId);CookieHelper.addCookie(AUTH_SESSION_ID, encodedAuthSessionId, cookiePath, null, null, -1, sslRequired, true, SameSiteAttributeValue.NONE);log.debugf("Set AUTH_SESSION_ID cookie with value %s", encodedAuthSessionId);}/**** @param encodedAuthSessionId encoded ID with attached route in cluster environment (EG. "5e161e00-d426-4ea6-98e9-52eb9844e2d7.node1" )* @return object with decoded and actually encoded authSessionId*/AuthSessionId decodeAuthSessionId(String encodedAuthSessionId) {log.debugf("Found AUTH_SESSION_ID cookie with value %s", encodedAuthSessionId);StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class);String decodedAuthSessionId = encoder.decodeSessionId(encodedAuthSessionId);String reencoded = encoder.encodeSessionId(decodedAuthSessionId);return new AuthSessionId(decodedAuthSessionId, reencoded);}

- 我在获取auth_session_id的代码段添加日志后,发现在跨域iframe对接kc时,kc服务端无法获取到auth_session_id,所以最后导致出现parseSessionCode
