Keycloak 配置自定义社交帐号登录组件

由于 Keycloak 由红帽开发维护,所以其自带的 Social Identity Providers 都是国外的平台。现有需求对接公司 SSO,且由于国内 OAuth2 协议实现均不标准(公司的也是),所以无法使用 Keycloak 的 OpenID Connect 自定义接入,需要自己扩展 Identity Provider 实现。和国内的微信、钉钉、飞书等系统接入代码类似,但发现现有的接入组件没有实现 UserAttributeMapper,且版本变动,代码并不兼容,所以参考 Github 的实现进行开发。

Keycloak 使用当前最新版:16.1.0

一、自定义 IdentityProvider

0. 引入依赖

引入 Keycloak 相关依赖,依赖版本要与使用的 Keycloak 版本保持一致,scope 使用 provided 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>

0.5. 自定义 IdentityProvider

由于后续需要,我们先新建一个 IdentityProvider 放在这里。

1
2
3
4
5
6
7
8
9
10
11
12
public class MyIdentityProvider extends AbstractOAuth2IdentityProvider<OAuth2IdentityProviderConfig>
implements SocialIdentityProvider<OAuth2IdentityProviderConfig> {

public MyIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(session, config);
}

@Override
protected String getDefaultScopes() {
return null;
}
}

1. 自定义 IdentityProviderFactory

我们先来看一下 GitHub 的 GitHubIdentityProviderFactory,很简单,有一个唯一的 PROVIDER_ID,然后 getName 方法返回了一个名称用于展示。

我们照着改一下即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyIdentityProviderFactoryFactory extends AbstractIdentityProviderFactory<MyIdentityProvider> implements SocialIdentityProviderFactory<MyIdentityProvider> {

public static final String PROVIDER_ID = "my-idp";

@Override
public String getName() {
return "My IDP";
}

@Override
public MyIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new MyIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
}

@Override
public IdentityProviderModel createConfig() {
return new OAuth2IdentityProviderConfig();
}

@Override
public String getId() {
return PROVIDER_ID;
}
}

2. 自定义 IdentityProvider

先定义并在构造时传入 Authorize、Token 及 Profile 的 Url。

重写必须要求实现的抽象方法

重写 getDefaultScopes 的抽象方法,返回自定义 SCOPE。

按需重写方法

首先重写 supportsExternalExchange方法,写死返回 true。这个官方文档也没有找到注释,但是各实现类均这么重写的,所以这里我们也一起重写。

由于我们继承了 AbstractOAuth2IdentityProvider,剩下的可以直接看抽象类的实现,按需重写非标准定义下无法使用默认实现调用的方法。

比如我需要对应自己系统返回的 User 和 Keycloak 的 User 对象的字段映射关系,所以重写 extractIdentityFromProfile 方法。这个方法也是默认空实现,一定要重写的。我们可以直接拷贝 GitHub 的实现再进行修改,同时,如果我们需要额外存储用户信息,可以使用 setUserAttribute 方法进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {

BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));

String username = getJsonProperty(profile, "login");
user.setUsername(username);
user.setName(getJsonProperty(profile, "name"));
user.setEmail(getJsonProperty(profile, "email"));
user.setIdpConfig(getConfig());
user.setIdp(this);

user.setUserAttribute(USER_ATTRIBUTE_EMPLOY_ID, getJsonProperty(profile, USER_ATTRIBUTE_EMPLOY_ID));

AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());

return user;
}

3. 重写回调方法

可以发现,我们之前重写的方法,都是接受到回调之后获取 token、获取用户信息的逻辑,如果我们的回调接口传参也不是标准实现的话,那么我们可以重写回调方法。

自定义 Endpoint

编写自定义 Endpoint 类,继承 AbstractOAuth2IdentityProvider 内部 Endpoint 类。然后重写 authResponse 方法,自定义接收参数,然后仿照默认实现,通过 authorizationCode 换取用户信息并通过 callback.authenticated(federatedIdentity) 返回。

注册自定义 Endpoint

重写 AbstractOAuth2IdentityProvidercallback 方法,返回自定义 Endpoint。

1
2
3
4
@Override
public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
return new MyEndpoint(callback, realm, event);
}

4. 注册 IdentityProviderFactory

resources 文件夹下,新建 MEAT-INF 文件夹,并在其下面建立 services 文件。新增 org.keycloak.broker.social.SocialIdentityProviderFactory 文件,在里面直接填写 MyIdentityProviderFactory 的全限定类名即可。


大体流程就是这样,OAuth2 认证后回调默认的 EndPoint 类的 authResponse 方法。如果大体流程标准,且回调参数标准,可以直接参照默认实现,按需重写所调用的 IdentityProvider 方法;如果回调参数会流程不标准,则需要自定义 EndPoint 并进行注册,再重写 authResponse 方法。

二、自定义 IdentityProvider 配置页

在新建/编辑 IdentityProvider 时,页面元素是可以配置的,我们可以按需增减页面需要填写的字段。

首先打开 Keycloak 路径下的 themes\base\admin\resources\partials\ 文件夹,我们还是将 realm-identity-provider-github.htmlrealm-identity-provider-github-ext.html 复制并重命名为 realm-identity-provider-my-idp.htmlrealm-identity-provider-my-idp-ext.html。如果不需要自定义,那么保持原样即可。如果需要修改页面字段展示,打开 realm-identity-provider-my-idp.html 文件,将文件内容替换为 realm-identity-provider-social.html 文件的内容,然后再对其进行修改即可。我们可以看到,默认页面就是引用了这个文件的内容。

三、部署 jar 文件

以 standalone 模式为例:

将打包后的 jar 文件复制到 standalone\deployments\ 文件夹,不需要重启服务,Keycloak 会自动热部署,并在同级目录生成 文件名.deployed 文件。

如果后续调用时出现 问题,可以参考 NoClassDefFoundError in a provider jarmaven-jar-plugin 插件配置依赖即可。

四、使用自定义 IDP

回到 Keycloak 后台,右上角点击 Server Info,切到 Providers Tab,如果在 social 栏看到了 my-idp(即上面定义的 provider id),说明部署成功,在 Identity Providers 后台正常新增即可。

五、自定义 UserAttributeMapper

由于之前我们需要额外储存用户信息,所以在重写 extractIdentityFromProfile 方法时,使用了 setUserAttribute 方法设置自定义属性。但由于 Keycloak 是冗余的用户数据,我们希望用户每次登录后,都可以自动更新用户信息。我们当然可以在 IDP 的配置中设置 Sync Modeforce 来每次更新用户信息,但此时只会自动同步更新 User 中 Details Tab 下的基本信息,不会再次同步 Attributes。
正常我们就会想到,可以通过设置 IDP 的 Mappers 来进行属性的放置,结果就会发现,Mapper Type 只有三个 Hardcoded 开头的硬编码的 Mapper。所以为了能够重新使用 Attribute Importer,需要我们自定义一个 UserAttributeMapper

1. 自定义 UserAttributeMapper

仍然参考 GitHub 的 GitHubUserAttributeMapper,直接拷贝过来,将 PROVIDER_ID 改成自己的即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyUserAttributeMapper extends AbstractJsonUserAttributeMapper {

public static final String PROVIDER_ID = "my-idp-user-attribute-mapper";
private static final String[] cp = new String[] { MyIdentityProviderFactory.PROVIDER_ID };

@Override
public String[] getCompatibleProviders() {
return cp;
}

@Override
public String getId() {
return PROVIDER_ID;
}
}

在这我们也可以看到,Keycloak 是通过 AbstractJsonUserAttributeMappergetCompatibleProviders 方法来控制 IDP 可选哪些 Mapper 的。

2. 注册自定义 UserAttributeMapper

同之前注册 IdentityProvider,在 META-INF/service/ 下新建 org.keycloak.broker.provider.IdentityProviderMapper 文件,并在其中填写我们自定义的 UserAttributeMapper 的全限定类名。
最后,别忘了重新打包部署,我们就可以继续在自定义的 IDP 中使用 Attribute Importer 了。