蓝色狐狸 發表於 2025-11-5 14:01:00

keycloak~登录时将请求头里某个属性放入UserSessionModel

<p>UserSessionModel做为用户登录过程中的一个会话,可以用来跨flow使用数据,这些数据被保存到内存里,在认证过程中可以被使用,今天的一个需求要求在登录时从请求头获取IP所在地并写到kafka里,要想实现这个需求,你可以在现有认证流程中修改代码,但不建议这样做,因为这种修改对原始逻辑会有破坏,keycloak提供了自定义认证流,并在后台可以灵活的配置。</p>
<p><img src="https://img2024.cnblogs.com/blog/118538/202511/118538-20251105135309869-1354673946.png" alt="图片" loading="lazy"></p>
<h1 id="相关keycloak中的知识">相关keycloak中的知识</h1>
<h2 id="认证流程的执行动作">认证流程的执行动作</h2>
<p>从上面图中可以看到,这个登录的过程会经历多个认证流,在所有被开启的认证流执行完成后才算登录成功,而这些流程我们是可以进行按需开发并配置的,下面说一下keycloak认证过程的几大事件,以表单登录为例(社区三方认证流程更复杂一些:</p>
<ul>
<li>表单提交</li>
<li>标准用户密码认证流执行</li>
<li>扩展认证流执行
<ul>
<li>会话限制 User Session Count Limiter</li>
<li>请求头到session的转换 Header-session-authenticator</li>
<li>黑名单控制 BlackListFilterAuthenticator</li>
<li>用户有效性控制 User Validate</li>
<li>弱密码提醒 Config Simple Password Alert Form</li>
<li>MFA多因子认证 OTP Form</li>
</ul>
</li>
<li>执行jwt token构建流程,包含自定义的<code>AbstractOIDCProtocolMapper</code>等</li>
<li>发布Login登录成功事件</li>
<li>订阅了Login事件的监听器可以写入kafka消息</li>
</ul>
<h2 id="keycloak认证流程相关元素">keycloak认证流程相关元素</h2>
<ol>
<li>浏览器认证流Browser Flow 继承AbstractUsernameFormAuthenticator类</li>
<li>直接认证流Direct Grant Flow 继承BaseDirectGrantAuthenticator类</li>
<li>用户所需要动作Require Action 实现RequiredActionProvider接口</li>
<li>表单页面Form Action,实现了FormAction接口</li>
</ol>
<h2 id="关于登录事件login扩展的分析">关于登录事件Login扩展的分析</h2>
<ol>
<li>在AbstractUsernameFormAuthenticator实现类中,添加登录扩展参数</li>
<li>定义一个认证流,专业处理这块请求头参数,并把它放入登录事件</li>
<li>定义一个Login事件监听器,在收到后信息后,可进行中间件转发</li>
<li>如果需要扩展token中的属性,也可以通过扩展认证流来实现,将数据存入userSession,并且在AbstractOIDCProtocolMapper阶段读取userSession的内容写入到jwtToken中</li>
</ol>
<h1 id="实现步骤">实现步骤</h1>
<p>下面自定义一个从请求头获取属性写入userSessionModel的例子</p>
<pre><code>@JBossLog
public class RequestHeaderToSessionNoteAuthenticator implements Authenticator {

    private final KeycloakSession session;

    public RequestHeaderToSessionNoteAuthenticator(KeycloakSession session) {
      this.session = session;
    }


    @Override
    public void authenticate(AuthenticationFlowContext context) {
      HttpHeaders httpHeaders = context.getHttpRequest().getHttpHeaders();
      if (httpHeaders.getRequestHeaders().containsKey(UserUtils.EO_CLIENT_REGIONNAME)) {
            context.getAuthenticationSession().setUserSessionNote("lastLoginProvince",
                  URLDecoder.decode(httpHeaders.getHeaderString(UserUtils.EO_CLIENT_REGIONNAME)));
            context.getEvent().detail("lastLoginProvince",
                  URLDecoder.decode(httpHeaders.getHeaderString(UserUtils.EO_CLIENT_REGIONNAME)));
      }
      if (httpHeaders.getRequestHeaders().containsKey(UserUtils.EO_CLIENT_CITYNAME)) {
            context.getAuthenticationSession().setUserSessionNote("lastLoginCity",
                  URLDecoder.decode(httpHeaders.getHeaderString(UserUtils.EO_CLIENT_CITYNAME)));
            context.getEvent().detail("lastLoginCity",
                  URLDecoder.decode(httpHeaders.getHeaderString(UserUtils.EO_CLIENT_CITYNAME)));
      }
      context.success();
    }

    private EntityManager getEntityManager() {
      return this.session.getProvider(JpaConnectionProvider.class).getEntityManager();
    }

    @Override
    public void action(AuthenticationFlowContext context) {
    }

    @Override
    public boolean requiresUser() {
      return false;
    }

    @Override
    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
      return false;
    }

    @Override
    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {

    }
}

public class RequestHeaderToSessionNoteAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {

    public final static String PROVIDER_ID = "header-session-authenticator";


    @Override
    public String getDisplayType() {
      return "header-session-authenticator";
    }

    @Override
    public String getReferenceCategory() {
      return null;
    }

    @Override
    public boolean isConfigurable() {
      return false;
    }

    @Override
    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
      return REQUIREMENT_CHOICES;
    }

    // 是否针对用户有require action动作,如果没有,requiresUser()返回也为false
    @Override
    public boolean isUserSetupAllowed() {
      return false;
    }

    @Override
    public String getHelpText() {
      return "header-session-authenticator";
    }

    @Override
    public List&lt;ProviderConfigProperty&gt; getConfigProperties() {
      return null;
    }

    @Override
    public Authenticator create(KeycloakSession keycloakSession) {
      return new RequestHeaderToSessionNoteAuthenticator(keycloakSession);
    }

    @Override
    public void init(Scope scope) {
    }

    @Override
    public void postInit(KeycloakSessionFactory keycloakSessionFactory) {
    }

    @Override
    public void close() {
    }

    @Override
    public String getId() {
      return PROVIDER_ID;
    }
}
</code></pre>
<p>最后在resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory中添加你的这个Factory即可。</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/19193473
頁: [1]
查看完整版本: keycloak~登录时将请求头里某个属性放入UserSessionModel