keycloak~keycloak14.0源代码二次开发
<h1 id="本地调试入口">本地调试入口</h1><p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031130102590-141265487.png" alt="图片" loading="lazy"></p>
<h1 id="编译keycloak源代码某个包">编译keycloak源代码某个包</h1>
<pre><code>mvn package -Denforcer.skip=true -Dmaven.test.skip=true
mvn clean install -Dskip=true
</code></pre>
<ul>
<li>部署到私服,建议源码修改后,应该部署到私服,这样其它应用在部署时,也有可以使用修改后的代码了</li>
</ul>
<pre><code>$ mvn deploy -Denforcer.skip=true -Dmaven.test.skip=true
</code></pre>
<h1 id="当用户已经在浏览器登录在使用自动登录接口或者之前同时打开两个登录页这时为了保证用户的登录状态后面的登录请求会被拦截跳转到首页">当用户已经在浏览器登录,在使用自动登录接口(或者之前同时打开两个登录页)这时为了保证用户的登录状态,后面的登录请求会被拦截,跳转到“首页”</h1>
<ul>
<li>如果非iframe的情况,使用下面的代码可以实现问题,在<code>org.keycloak.services.resources.LoginActionsServiceChecks</code><br>
.checkNotLoggedInYet()和SessionCodeChecks.initialVerifyAuthSession()方法添加302跳转</li>
<li>对于iframe里面的登录页,需要考虑如何在顶级窗口实现重定向,目前添加js重定向解决了这个iframe问题</li>
</ul>
<pre><code>// TODO: 当用户已经登录了,直接跳到首页
Response.status(302).header(HttpHeaders.LOCATION, "https://www.xxx.com").build();
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031130137437-928403871.png" alt="图片" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031132218294-2122274439.png" alt="图片" loading="lazy"></p>
<h2 id="js重定向解决了同时多个iframe登录框在其中一个登录另一个在本iframe重定向问题">js重定向解决了同时多个iframe登录框,在其中一个登录,另一个在本iframe重定向问题</h2>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031130215204-1342721606.png" alt="图片" loading="lazy"></p>
<h1 id="修改url中有特殊符号的问题">修改url中有特殊符号的问题</h1>
<p>修改org.keycloak.protocol.oidc.utils.RedirectUtils.removeUrlSpaceParams方法,将特殊符号进行编码</p>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031130236092-848500007.png" alt="图片" loading="lazy"></p>
<h1 id="生成token时添加日志">生成token时添加日志</h1>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031130253489-1794584238.png" alt="图片" loading="lazy"></p>
<h1 id="修改code-to-token的缓存类型">修改code to token的缓存类型</h1>
<p>默认使用BasicCache,应该是本地缓存,查通过查看TokenEndpoint,发现是分布式缓存</p>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031130312867-216073201.png" alt="图片" loading="lazy"></p>
<h1 id="login事件的个性化配置">LOGIN事件的个性化配置</h1>
<p>org.keycloak.services.managers.AuthenticationManager的方法nextRequiredAction和actionRequired,添加了LOGIN事件的个性化字段</p>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031130402528-1358259724.png" alt="图片" loading="lazy"></p>
<h1 id="code_to_token时去掉了clientid限制">code_to_token时,去掉了clientId限制</h1>
<ul>
<li>code_to_token时,去掉了clientId必须一致的条件的检验,这样不同客户端在通过code换token时,可以减少与kc交互交次数</li>
<li>方法变更:<code>TokenEndpoint.codeToToken()</code><br>
<img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031130456821-614161782.png" alt="图片" loading="lazy"></li>
</ul>
<h1 id="code_to_token时对浏览器sessionid操作">code_to_token时对浏览器sessionId操作</h1>
<ul>
<li>
<p>当code to token出现错误时,添加了清空浏览器里sessionId在kc的会话信息,但如果是httpclient的调用,咱们是拿不到客户端浏览器的cookie的</p>
</li>
<li>
<p>Code '%s' already used for userSession<br>
org.keycloak.protocol.oidc.util.OAuth2CodeParser.parseCode这块添加了clientId的日志描述</p>
</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031130821102-50637644.png" alt="图片" loading="lazy"></p>
<h1 id="修改kc管理后台session列表由于信息不全报错的问题">修改kc管理后台session列表由于信息不全报错的问题</h1>
<ul>
<li>主要是ModelToRepresentation报错了,应该是client_id为空引起的,像refresh_token达到次数会引起会话的client_id为空,但sessionId还是在线的。</li>
<li><strong>这块将异常报错复原了,如果不报错,将会出现大量session从数据库加载,导致数据库崩盘</strong></li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031130845764-422674113.png" alt="图片" loading="lazy"></p>
<h1 id="验证token去掉协议名的限制">验证token去掉协议名的限制</h1>
<ul>
<li>对在线token的校验,去掉了https和http的校验,只要后面域名相同就是ISS(Issuer)相同就行,这块在验证token时会用到,另外在适配器集成中,每人请求加载前,也会用到它</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031130906213-686546926.png" alt="图片" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031130917192-1031714476.png" alt="图片" loading="lazy"></p>
<h1 id="调查code码被占用问题生产了重复的code码">调查code码被占用问题(生产了重复的code码)</h1>
<p>1 code的生产</p>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131100141-412120810.png" alt="图片" loading="lazy"></p>
<p>2 code的校验</p>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131116667-1006640798.png" alt="图片" loading="lazy"></p>
<ul>
<li>解决同时打开两个登录窗口,在第一个窗口登录后,在第二个窗口再登录一次,会出现“您已经登录”的页面</li>
<li>解决:发生上面的情况后,直接跳到v6首页
<ul>
<li>LoginActionsServiceChecks.checkNotLoggedInYet()方法</li>
<li>SessionCodeChecks.initialVerifyAuthSession()方法</li>
</ul>
</li>
</ul>
<h1 id="让identityprovidermapper实现的类型自动为社区登录执行delegateupdatebrokereduser">让IdentityProviderMapper实现的类型,自动为社区登录执行delegateUpdateBrokeredUser</h1>
<ul>
<li>修改源代码:org.keycloak.services.resources.updateFederatedIdentity()</li>
<li>添加了代码逻辑,实现了按需自动执行</li>
</ul>
<pre><code> //对已有用户进行更新,注意,可能会覆盖用户的其它属性
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) {
}
});
</code></pre>
<ul>
<li>添加一个例子,实现社区登录的类型自动存储到用户属性loginType中,getCompatibleProviders()方法中绑定了<br>
<code>IdentityProviderMapper.ANY_PROVIDER</code>,所以在每个社区登录后,它都会被执行</li>
<li><strong>新用户不会绑定这个,已绑定的用户才执行这个方法,原因是syncModel为Force,表示当有用户后,会强制更新它</strong></li>
</ul>
<pre><code>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};
@Override
public String[] getCompatibleProviders() {
return cp;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user,
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
logger.info("updateBrokeredUser user info...");
user.setSingleAttribute("loginType", mapperModel.getIdentityProviderAlias());
}
}
</code></pre>
<ul>
<li>登录后更新用户属性loginType</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131146507-447426014.png" alt="图片" loading="lazy"></p>
<h1 id="登录后将logintype添加到refresh_token中">登录后,将loginType添加到refresh_token中</h1>
<blockquote>
<p>解决由于kc的refresh_token不支持自定义属性,所以在登录后,将loginType添加到refresh_token中,这样在refresh_token时,就可以获取到loginType了</p>
</blockquote>
<ul>
<li>实现逻辑:将当前loginType添加到当前refresh_token,在下次刷新token时,将refresh_token里的loginType取出来,覆盖到新的access_token里.</li>
<li>org.keycloak.protocol.oidc.TokenManager.validateToken()</li>
<li>org.keycloak.protocol.oidc.TokenManager.build()</li>
</ul>
<h1 id="用户session_state生成方式">用户session_state生成方式</h1>
<ul>
<li>org.keycloak.models.sessions.infinispan.createUserSession()</li>
<li></li>
</ul>
<h1 id="解决用户浏览器因为丢失keycloak_identity而keycloak_session_id有并且是在线的导致无法登录的问题">解决用户浏览器因为丢失keycloak_identity而keycloak_session_id有并且是在线的,导致无法登录的问题</h1>
<ul>
<li>在方法<code>AuthorizationEndpointBase.createAuthenticationSession()</code>添加了判断逻辑,没有keycloak_identity就重新根据session_id再生成一个到cookie里</li>
</ul>
<pre><code>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());
}
}
</code></pre>
<ul>
<li>关于对loginType和登录事件的修改<br>
请查看TODO: 20230406的注释代码</li>
<li>涉及到以下动作会触发的事件,会添加我们扩展的属性
<ul>
<li>共享登录</li>
<li>code换token</li>
<li>刷新token</li>
</ul>
</li>
</ul>
<h1 id="关于otp提供商的调研">关于OTP提供商的调研</h1>
<ul>
<li>OTP提供商的策略:org.keycloak.models.OTPPolicy,目前支持FreeOTP和GoogleAuthenticator</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131206585-1922725889.png" alt="图片" loading="lazy"></p>
<h1 id="关于keycloak-services项目添加第三方jar包的问题">关于keycloak-services项目添加第三方jar包的问题</h1>
<blockquote>
<p>我们例如将org.infinispan这个包,在kc里也是一个module,引用到keycloak-services项目,它在启动时会报错,告诉找不到这个org.infinispan.Cache类,类似这种类无法找到的错误。</p>
</blockquote>
<pre><code>Uncaught server error: java.lang.NoClassDefFoundError: org/infinispan/Cache
at org.keycloak.keycloak-services@14.0.0//org.keycloak.protocol.oidc.TokenManager.checkTokenValidForIntrospection(TokenManager.java:494)
</code></pre>
<p><strong>解决思路,在module.xml中,添加对应的模块即可</strong></p>
<ol>
<li></li>
</ol>
<p>从keycloak容器里将/opt/jboss/keycloak/modules/system/layers/keycloak/org/keycloak/keycloak-core/main/module.xml复制出来,在文件的dependencies节点下添加依赖,如<br>
<code><module name="org.infinispan"/></code></p>
<ol start="2">
<li>修改Dockerfile文件,将这个module.xml文件也复制到上面的容器目录,覆盖原来的文件</li>
<li>重新构建镜像,启动容器,问题解决</li>
</ol>
<h1 id="自动登录接口同一浏览器添加踢出之前登录的逻辑">自动登录接口同一浏览器添加踢出之前登录的逻辑</h1>
<ul>
<li>org.keycloak.protocol.AuthorizationEndpointBase.handleBrowserAuthenticationRequest()</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131221137-1663678532.png" alt="图片" loading="lazy"></p>
<h1 id="从carsi网站过来的用户会带有carsi-auto这个关键字也应该要踢出之前的登录">从carsi网站过来的用户,会带有carsi-auto这个关键字,也应该要踢出之前的登录</h1>
<ul>
<li>org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider.Endpoint.authResponse()</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131234451-1015202555.png" alt="图片" loading="lazy"></p>
<h1 id="刷新token和通过code换token逻辑中添加infinispan缓存的逻辑键key是sessionidclientid作用是当用户的角色变更后用户校验token直接返回false让迫使用户重新去刷新token">刷新token和通过code换token逻辑中,添加infinispan缓存的逻辑,键key是sessionId+clientId,作用是当用户的角色变更后,用户校验token直接返回false,让迫使用户重新去刷新token</h1>
<blockquote>
<p>// TODO: xxx_user_modify_role 需要添加逻辑,去检索事件中是否包括了权限变更的用户</p>
</blockquote>
<ul>
<li>验证token: org.keycloak.protocol.oidc.TokenManager.checkTokenValidForIntrospection()</li>
<li><s>刷新token: org.keycloak.protocol.oidc.endpoints.TokenEndpoint.refreshTokenGrant</s><br>
,这块因为相同的event对象,所以代码迁移到keycloak-services-event-kafka</li>
<li><s>code换token: org.keycloak.protocol.oidc.endpoints.TokenEndpoint.codeToToken()</s><br>
,这块因为相同的event对象,所以代码迁移到keycloak-services-event-kafka</li>
</ul>
<p><strong>用户权限更新后,通和逻辑整理</strong></p>
<ol>
<li>kc服务端收到<code>REALM_ROLE_MAPPING</code>或者<code>USER_ROLE_CHANGE</code>事件后,向infinispan缓存里添加一个key,key是<br>
<code>xxx_user_modify_role_{userId}</code>,value是空</li>
<li>它有缓存有效期与access_token的相同,目前为30分钟</li>
<li>当用户进行code换token或者刷新token时,根据当前用户id,去上面缓存中找,如果查找到,说明这个用户的权限发生了变更</li>
<li>找到后,向这个缓存<code>xxx_user_modify_role_{userId}</code>添加value,value格式是<code>{sessionId}_{clientId}</code>,就是用户在哪个<code>浏览器</code><br>
哪个<code>客户端</code>访问</li>
<li>当用户调用<code>验证token</code>接口时,如果在<code>xxx_user_modify_role_{userId}</code>中没有找到这个value<code>{sessionId}_{clientId}</code>,就验证失败</li>
<li>当验证失败后,返回401,用户再去刷新token,向<code>xxx_user_modify_role_{userId}</code>中添加对应的value, 保持下次验证会成功</li>
</ol>
<h1 id="获取ip地址的方法修改">获取IP地址的方法修改</h1>
<p>// TODO: 优化登录事件中,获取ipAddress的逻辑,改为real-ip有限</p>
<ul>
<li>org.keycloak.events.EventBuilder.ipAddress()进行了重新赋值</li>
<li>org.keycloak.services.resources.admin.AdminEventBuilder.AdminEventBuilder()初始化时,使用real-ip</li>
<li></li>
</ul>
<h1 id="验证token逻辑的抛下解析session-idle和session-max的逻辑">验证token逻辑的抛下,解析session idle和session max的逻辑</h1>
<ul>
<li>org.keycloak.services.managers.AuthenticationManager.isSessionValid</li>
<li>SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS是个时间戳口,它是120秒</li>
</ul>
<pre><code class="language-java">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 postponed
int 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;
}
</code></pre>
<h1 id="session-idle和session-max的逻辑生效如果修改refresh_token生成时的校验逻辑">session idle和session max的逻辑生效,如果修改refresh_token生成时的校验逻辑</h1>
<pre><code>org.keycloak.protocol.oidc
.TokenManager.refreshAccessToken()方法中的代码,将verifyRefreshToken方法参数中的checkExpiration改成false
// TODO: 完善实现了在线校验时,session idle和session max的功能
</code></pre>
<h1 id="session-idle空闲过期时间和session-max最大过期时间不相等时产生的问题">session idle(空闲过期时间)和session max(最大过期时间)不相等时,产生的问题</h1>
<h2 id="描述与解决思路">描述与解决思路</h2>
<ol>
<li>session idle会作为刷新token的过期时间</li>
<li>当这个时间到达后,不能再刷新token了,但是,session还是在线的</li>
<li>是否需要在到达这个时间后,将会话删除?</li>
<li>如果真要删除的话,可能产生的问题就是session max的时间还没到,但是session已经被删除了,这样就会导致session max的时间不准确了</li>
<li>但如果session idle到达,并且token没有成功刷新,这说明用户空闲了,这时session是可以删除的,与4不矛盾</li>
<li>解决方法<br>
* 在session idle到达后,将session删除,应该就解决问题了<br>
* 或者在生成code之前,判断它的session idle是否到期,如果到期,将会话删除,不能生成code</li>
</ol>
<h1 id="用户会话过期清理用户会话的逻辑调整"><s>用户会话过期,清理用户会话的逻辑调整</s></h1>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131305129-927960828.png" alt="图片" loading="lazy"></p>
<ul>
<li>org.keycloak.services.scheduled.ScheduledTaskRunner # 默认900秒(15分钟)执行一次
<ul>
<li>org.keycloak.services.scheduled.ClearExpiredUserSessionsTask
<ul>
<li>org.keycloak.models.map.authSession.removeExpired</li>
<li>需要添加空闲过期时间的判断,如果到期,就删除会话</li>
<li>这块需要看如何手动清除,因为默认的,infinispan中的session,有自己的过期时间,按着过期时间自动清除的</li>
<li>咱们相当于,如何让它按着咱们的时间(这个时间经过了session idle的时间)来清除的</li>
</ul>
</li>
</ul>
</li>
<li><s>通过上面的分析,直接从infinispan中获取过期的session,并删除不太可能,人家通知初始化的过期时间自行维护的,而且这种过期时间,是session<br>
max,而咱们的过期时间是可变的,它可能是一个session idle,也可能是session max,这与用户是否在idle时间内是否有操作有关</s></li>
</ul>
<ul class="contains-task-list">
<li class="task-list-item"><input class="task-list-item-checkbox" checked="" disabled="" type="checkbox"><label> 这种定时器,可能是为了mysql中存储的离线token用的,可查看offline_user_session相关的内容</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" checked="" disabled="" type="checkbox"><label> 我们如果在生成code时,添加一个判断,判断这个session idle是否到期,如果到期,就删除会话,不能生成code</label></li>
</ul>
<h2 id="生成code时添加session-idle的判断">生成code时,添加session idle的判断</h2>
<ul>
<li><strong>如果不添加这个判断</strong>,将会出现的问题是,当session idle和session max设置不同时,当session<br>
idle到期后,用户的会话不会被删除,导致刷新token和申请code码换token,两块产生的token逻辑不一样,最终效果就是,code可以换回来token,但token在校验时是<br>
<code>session not active</code>这样的错误。</li>
<li>org.keycloak.protocol.AuthorizationEndpointBase.createAuthenticationSession()方法</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131320310-211156356.png" alt="图片" loading="lazy"></p>
<h1 id="修改orgkeycloakthemedefaultthemeselectorprovider文件getthemename方法添加了url中皮肤参数theme">修改org.keycloak.theme.DefaultThemeSelectorProvider文件getThemeName()方法,添加了url中皮肤参数theme</h1>
<pre><code class="language-java">MultivaluedMap<String, String> query = session.getContext().getUri().getQueryParameters();
if(query.
containsKey("theme")){
name=query.
getFirst("theme");
}else{
}
</code></pre>
<h1 id="登录跨域支持">登录跨域支持</h1>
<p>org.keycloak.protocol.oidc.endpoints.TokenEndpoint.resourceOwnerPasswordCredentialsGrant(),返回值添加跨域代码</p>
<pre><code class="language-java">return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).
allowAllOrigins().
build();
</code></pre>
<h1 id="登录回调地址中添加logintype这个参数">登录回调地址中添加loginType这个参数</h1>
<ul>
<li>org.keycloak.services.resources.IdentityBrokerService.finishBrokerAuthentication()方法添加对loginType的操作</li>
<li>org.keycloak.protocol.oidc.OIDCLoginProtocol.authenticated()方法中,获取loginType,并添加到回调路径的URL参数中</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131338010-1112209879.png" alt="图片" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131347584-1061649223.png" alt="图片" loading="lazy"></p>
<h1 id="社区登录成功后绑定用户信息修改federated_identity_link的内容">社区登录成功后,绑定用户信息,修改FEDERATED_IDENTITY_LINK的内容</h1>
<ul>
<li>org.keycloak.services.resources.IdentityBrokerService.afterFirstBrokerLogin方法</li>
<li>添加自定义事件元素:event.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getBrokerUserId());</li>
</ul>
<h1 id="刷新token时如果用户有required-action抛出异常">刷新token时,如果用户有required action,抛出异常</h1>
<ul>
<li>org.keycloak.protocol.oidc.TokenManager.validateToken()方法</li>
</ul>
<pre><code> //TODO:刷新token时,如果用户有required action,抛出异常
if (user.getRequiredActionsStream().findAny().isPresent()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User has required action",
"User has required action");
}
</code></pre>
<h1 id="社区登录state的自定义">社区登录state的自定义</h1>
<h2 id="社区登录回调state参数支持4个参数">社区登录回调state参数,支持4个参数</h2>
<ul>
<li>org.keycloak.broker.provider.util.IdentityBrokerState类中encoded方法,将<br>
<code>String[] decoded = DOT.split(encodedState, 4);</code>从3改成4</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131406307-694209605.png" alt="图片" loading="lazy"></p>
<h2 id="建立社区登录地址时添加自定义state参数">建立社区登录地址时添加自定义state参数</h2>
<ul>
<li>AbstractOAuth2IdentityProvider类中createAuthorizationUrl方法,修改state参数的拼接</li>
</ul>
<pre><code class="language-java"> String state = request.getState().getEncoded();
if(request.
getAuthenticationSession().
getAuthNote("g") !=null&&
request.
getAuthenticationSession().
getAuthNote("g").
trim() !=""){
state =state +"."+request.
getAuthenticationSession().
getAuthNote("g");
}
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131421572-1156419805.png" alt="图片" loading="lazy"></p>
<h2 id="在认证成功后federatedidentitycontext上下文添加参数">在认证成功后federatedIdentityContext上下文添加参数</h2>
<ul>
<li>AbstractOAuth2IdentityProvider类中Endpoint.authResponse方法,再返回之前为federatedIdentity添加groupId参数</li>
</ul>
<pre><code class="language-java">// 添加集团代码
String[] decoded = DOT.split(state, 4);
if(decoded.length ==4){
federatedIdentity.
setUserAttribute("groupId",decoded);
}
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131436295-803991133.png" alt="图片" loading="lazy"></p>
<h1 id="登录超时的提示语调整">登录超时的提示语调整</h1>
<ul>
<li>keycloak-themes/themes/base/login/messages/messages_en.properties文件</li>
<li>修改loginTimeout的值即可</li>
</ul>
<h1 id="社区登录页providerlogin页添加idp和login_type参数">社区登录页{provider}/login页添加idp和login_type参数</h1>
<ul>
<li>org.keycloak.services.Urls类identityProviderAuthnRequest()方法,添加idp参数的追加</li>
<li>org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator类中redirect()<br>
方法构建登录页重定向参数,添加loginType和idp两个参数</li>
<li></li>
</ul>
<h1 id="authrealmsxxxprotocolopenid-connectuserinfo接口添加session_state属性返回值">/auth/realms/xxx/protocol/openid-connect/userinfo接口添加session_state属性返回值</h1>
<ul>
<li>org.keycloak.protocol.oidc.OIDCLoginProtocolService类中的issueUserInfo()方法</li>
<li>添加跨域支持<code>allowedOrigins("*")</code></li>
<li>解析当前token,并添加session_state</li>
</ul>
<pre><code>claims.put("session_state", userSession.getId());// 添加当前的session信息
</code></pre>
<h1 id="刷新token时出现session-not-active或者invalid-refresh-token">刷新token时,出现<code>Session not active</code>或者<code>Invalid refresh token</code></h1>
<ul>
<li>Session not active 表示用户的session已经过期了,需要重新登录,返回400</li>
<li>Invalid refresh token 表示refresh token不正确,可能token被篡改了,需要重新登录,返回400</li>
<li>刷新token时,只有一种情况会返回401,就是client_secret错误时,<code>Client secret not provided in request</code></li>
<li>org.keycloak.protocol.oidc.TokenEndpoint.refreshTokenGrant()方法,添加refresh_token验证不通过,会走这个catch逻辑,大多数情况httpcode都是400</li>
</ul>
<pre><code>catch (OAuthErrorException e) {
logger.trace(e.getMessage(), e);
// KEYCLOAK-6771 Certificate Bound Token
if (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);
}
}
</code></pre>
<h1 id="社区登录添加logintype为社区的idp">社区登录添加loginType为社区的idp</h1>
<ul>
<li>情况一,未绑定用户,走first flow</li>
<li>情况二,绑定了用户,下次再登录,会走after flow</li>
</ul>
<h2 id="first-flow">first flow</h2>
<p>org.keycloak.services.resources.IdentityBrokerService.authenticated()方法,添加代码</p>
<pre><code>authenticationSession.setUserSessionNote("loginType",context.getIdpConfig().getProviderId());
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131456327-841310785.png" alt="图片" loading="lazy"></p>
<h2 id="after-flow">after flow</h2>
<p>org.keycloak.services.resources.IdentityBrokerService.authenticated()方法,添加代码</p>
<pre><code>authenticationSession.setUserSessionNote("loginType",context.getIdpConfig().getProviderId());
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131713648-1891194583.png" alt="图片" loading="lazy"></p>
<h1 id="社区登录microsoft修改federated_identity_link的bug">社区登录microsoft修改FEDERATED_IDENTITY_LINK的BUG</h1>
<ul>
<li>用户第一次使用社区来绑定本地KC用户时,需要为社区用户的unionId赋值到BrokeredIdentityContext对象</li>
<li>在MicrosoftIdentityProvider.extractIdentityFromProfile()方法,添加了<code> user.setBrokerUserId(id);</code></li>
<li>如果其它社区登录需要集成,也需要手动添加上面的代码</li>
<li>IdentityBrokerService.afterFirstBrokerLogin()方法,添加用户第一次绑定社区时FEDERATED_IDENTITY_LINK的扩展信息</li>
</ul>
<h1 id="管理后台-用户检索-改为用户名精确或者邮箱精确">管理后台-用户检索-改为用户名精确或者邮箱精确</h1>
<ul>
<li>org.keycloak.models.jpa.JapUserProvider类</li>
<li>searchForUserStream(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults)方法</li>
</ul>
<h1 id="个人中心绑定社区用户">个人中心绑定社区用户</h1>
<p>代码注释,去掉权限的控制:IdentityBrokerService.clientInitiatedAccountLinking()方法,注册下面代码</p>
<ul>
<li>出错信息:not_allowed</li>
</ul>
<pre><code class="language-java">// 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();
// }
// }
</code></pre>
<ul>
<li>出错信息:insufficientPermissionMessage</li>
</ul>
<pre><code class="language-java">// if (!authenticatedUser.hasRole(this.realmModel.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)
// .getRole(AccountRoles.MANAGE_ACCOUNT))) {
// return redirectToErrorPage(authSession, Response.Status.FORBIDDEN, Messages.INSUFFICIENT_PERMISSION);
// }
</code></pre>
<ul>
<li>修改非account客户端的错误页逻辑,直接将错误编码带着来源页</li>
<li>IdentityBrokerService.redirectToErrorWhenLinkingFailed()</li>
</ul>
<pre><code class="language-java"> 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();
}
}
</code></pre>
<h1 id="首次登录社区并完成老用户的绑定向federated_identity_link事件添加corpid">首次登录社区,并完成老用户的绑定,向FEDERATED_IDENTITY_LINK事件添加corpId</h1>
<ul>
<li>org.keycloak.services.resources.IdentityBrokerService.afterFirstBrokerLogin(AuthenticationSessionModel authSession)<br>
方法中添加代码</li>
</ul>
<pre><code>event.detail(CORP_ID,context.getUserAttribute(CORP_ID)); // 这块与认证页有跨页,所以authSession.getAuthNote(CORP_ID)无法获取到corpId,所以临时存在userAttribute对应的内存中,并存持久化到数据库
</code></pre>
<ul>
<li>具体的AbstractOAuth2IdentityProvider子类中Endpoint.authResponse()方法中添加代码</li>
</ul>
<pre><code>//11.3.0之后改成这样了,去掉了code字段
federatedIdentity.setUserAttribute("corpId",federatedIdentity.getUserAttribute(DINGTALK_CORP_ID));
// authSession.setAuthNote("corpId",federatedIdentity.getUserAttribute(DINGTALK_CORP_ID));
</code></pre>
<h1 id="已登录的用户去绑定社区用户时向federated_identity_link事件添加corpid">已登录的用户去绑定社区用户时,向FEDERATED_IDENTITY_LINK事件添加corpId</h1>
<ul>
<li>org.keycloak.services.resources.IdentityBrokerService.performAccountLinking()方法中添加代码</li>
<li>这块为了统一,也使用getUserAttribute即可</li>
</ul>
<pre><code>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();
</code></pre>
<h1 id="parsesessioncode报错">parseSessionCode报错</h1>
<p><code>AuthenticationSessionManager.getCurrentAuthenticationSession authSessionCookies</code>这块添加日志,看是否kc可以获取到浏览器cookie中的auth_session_id,如果获取不到会出现下面错误</p>
<pre><code>ERROR (default task-1709) unexpectedErrorHandlingRequestMessage: javax.ws.rs.WebApplicationException: HTTP 400 Bad Request
at 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
</code></pre>
<h3 id="auth_session_id解析过程">auth_session_id解析过程</h3>
<ul>
<li>auth_session_id它是由session_state.nodeId组成的,session_state是用户会话的id,nodeId是kc集群节点的标识符,它们之间用点号分隔开,比如<br>
<code>5e161e00-d426-4ea6-98e9-52eb9844e2d7.node1</code>,这样在集群环境下,可以将请求路由到对应的节点上去。</li>
<li>KEYCLOAK_IDENTITY它是用户登录之后产生的,它是一个jwt的token,包含最基础的会话信息</li>
</ul>
<pre><code>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"
}
</code></pre>
<ul>
<li>生成一个auth_session_id到浏览器cookie中</li>
<li>IdentityBrokerService.clientInitiatedAccountLinking()方法中,调用<br>
<code>AuthenticationSessionManager.getCurrentAuthenticationSession()</code>方法,解析浏览器cookie中的auth_session_id</li>
</ul>
<pre><code>AuthenticationManager.AuthResult cookieResult =
AuthenticationManager.authenticateIdentityCookie(session, realmModel, true);
//...
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client);
// Refresh the cookie
new AuthenticationSessionManager(session).setAuthSessionCookie(userSession.getId(), realmModel);
</code></pre>
<p><img src="img_2.png" alt="img_2.png" loading="lazy"></p>
<ul>
<li>AuthenticationManager类中的authenticateIdentityCookie用来生成一个AuthResult对象,如果用户已经登录(有KEYCLOAK_IDENTITY_COOKIE)这个auth_session_id就会被使用</li>
</ul>
<pre><code>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;
}
</code></pre>
<ul>
<li>AuthenticationSessionManager文件</li>
</ul>
<pre><code>
/**
* @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);
}
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131741725-1102827248.png" alt="图片" loading="lazy"></p>
<ul>
<li>我在获取auth_session_id的代码段添加日志后,发现在跨域iframe对接kc时,kc服务端无法获取到auth_session_id,所以最后导致出现parseSessionCode</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/118538/202510/118538-20251031131752239-1156847526.png" alt="图片" loading="lazy"></p>
</div>
<div id="MySignature" role="contentinfo">
<p></p>
<div class="navgood">
<p>作者:仓储大叔,张占岭,<br>
荣誉:微软MVP<br>QQ:853066980</p>
<p><strong>支付宝扫一扫,为大叔打赏!</strong>
<br><img src="https://images.cnblogs.com/cnblogs_com/lori/237884/o_IMG_7144.JPG"></p>
</div><br><br>
来源:https://www.cnblogs.com/lori/p/19179518
頁:
[1]