UserSessionModel做為用户登錄過程中的一個會話,可以用來跨flow使用數據,這些數據被保存到內存裏,在認證過程中可以被使用,今天的一個需求要求在登錄時從請求頭獲取IP所在地並寫到kafka裏,要想實現這個需求,你可以在現有認證流程中修改代碼,但不建議這樣做,因為這種修改對原始邏輯會有破壞,keycloak提供了自定義認證流,並在後台可以靈活的配置。
相關keycloak中的知識
認證流程的執行動作
從上面圖中可以看到,這個登錄的過程會經歷多個認證流,在所有被開啓的認證流執行完成後才算登錄成功,而這些流程我們是可以進行按需開發並配置的,下面説一下keycloak認證過程的幾大事件,以表單登錄為例(社區三方認證流程更復雜一些:
- 表單提交
- 標準用户密碼認證流執行
- 擴展認證流執行
- 會話限制 User Session Count Limiter
- 請求頭到session的轉換 Header-session-authenticator
- 黑名單控制 BlackListFilterAuthenticator
- 用户有效性控制 User Validate
- 弱密碼提醒 Config Simple Password Alert Form
- MFA多因子認證 OTP Form
- 執行jwt token構建流程,包含自定義的
AbstractOIDCProtocolMapper等 - 發佈Login登錄成功事件
- 訂閲了Login事件的監聽器可以寫入kafka消息
keycloak認證流程相關元素
- 瀏覽器認證流Browser Flow 繼承AbstractUsernameFormAuthenticator類
- 直接認證流Direct Grant Flow 繼承BaseDirectGrantAuthenticator類
- 用户所需要動作Require Action 實現RequiredActionProvider接口
- 表單頁面Form Action,實現了FormAction接口
實現步驟
下面自定義一個從請求頭獲取屬性寫入userSessionModel的例子
@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<ProviderConfigProperty> 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;
}
}
最後在resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory中添加你的這個Factory即可。