Keycloak自定义实现第三方登录

第三方Oauth登录

  • 由于对接的第三方IDP不一定都是标准的openid connect实现,所以都需要根据第三方的Oauth文档进行定制;
    Keycloak对于新增Social IDP的实现,都是标准,以及灵活的;
    我们完全可以参照 Keycloak 本身已实现的Github LinkedIn等,快速实现我们的需求;

我们这里以酷家乐的Oauth2 接口进行说明

酷家乐 Oauth2接口分析

  • 请求code的参数及返回都是标准的,这里无需进行修改在这里插入图片描述
  • 换取token接口,由于返回参数进行data的wrap,以及字段名为驼峰的,所以需要对这部分进行修改定制

在这里插入图片描述

  • 获取用户信息

由于获取用户信息,还需要额外请求用户的OpenId,所以需要对该部分进行修改

在这里插入图片描述

在这里插入图片描述

定制Provider

提供工程类: KujialeIdentityProviderFactory

工厂类完全可以从GitHubIdentityProviderFactory 拷贝过来,修改成自己 PROVIDER_ID, NAME 以及create自己的Provider

public class GitHubIdentityProviderFactory extends AbstractIdentityProviderFactory<GitHubIdentityProvider> implements SocialIdentityProviderFactory<GitHubIdentityProvider> {

    public static final String PROVIDER_ID = "github";

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

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

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

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

解析accessToken 以及获取用户信息

在这里插入图片描述

解析用户信息

在这里插入图片描述

KujialeIdentityProvider 完整代码

public class KujialeIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider {

    public static final String AUTH_URL = "https://oauth.kujiale.com/oauth2/show";
    public static final String TOKEN_URL = "https://oauth.kujiale.com/oauth2/auth/token";
    public static final String OPENID_URL = "https://oauth.kujiale.com/oauth2/auth/user";
    public static final String PROFILE_URL = "https://oauth.kujiale.com/oauth2/openapi/user";
    public static final String DEFAULT_SCOPE = "get_user_info";

    public static final String OAUTH2_PARAMETER_ACCESS_TOKEN = "accessToken";

    public KujialeIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
        super(session, config);
        config.setAuthorizationUrl(AUTH_URL);
        config.setTokenUrl(TOKEN_URL);
        config.setUserInfoUrl(PROFILE_URL);
    }

    @Override
    protected boolean supportsExternalExchange() {
        return true;
    }

    @Override
    protected String getProfileEndpointForValidation(EventBuilder event) {
        return PROFILE_URL;
    }

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

    /**
     * 提取酷家乐用户信息,转换为keycloak用户实体
     *
     * @param event
     * @param profile
     * @return
     */
    @Override
    protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {

        if (!profile.has("d") || profile.get("d").isEmpty()) {
            throw new NullPointerException("kujiale idp user info response is null " + profile.toString());
        }

        JsonNode userNode = profile.get("d");

        String openId = getJsonProperty(userNode, "openId");
        if (openId == null || openId.isEmpty()) {
            throw new NullPointerException("kujiale idp user info is null " + userNode.asText());
        }

        BrokeredIdentityContext user = new BrokeredIdentityContext(openId);
        String userName = userNode.get("userName").asText();

        user.setUsername(userName);
        user.setFirstName(userName);
        user.setIdpConfig(getConfig());
        user.setIdp(this);
        AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, userNode, getConfig().getAlias());
        return user;
    }

    /**
     * 构建获取酷家乐用户信息的请求 暂不支持
     *
     * @param subjectToken
     * @param userInfoUrl
     * @return
     */
    @Override
    protected SimpleHttp buildUserInfoRequest(String subjectToken, String userInfoUrl) {
        return SimpleHttp.doGet(userInfoUrl, session)
                .header("Authorization", "Bearer " + subjectToken);
    }

    /**
     * 获取酷家乐用户信息
     *
     * @param response
     * @return
     */
    @Override
    public BrokeredIdentityContext getFederatedIdentity(String response) {

        String accessToken;
        try {
            JsonNode resNode = asJsonNode(response);
            accessToken = extractTokenFromResponse(resNode.get("d").toString(), OAUTH2_PARAMETER_ACCESS_TOKEN);
        } catch (Exception ex) {
            throw new IdentityBrokerException("No access token available in OAuth server response: " + response);
        }

        if (accessToken == null) {
            throw new IdentityBrokerException("No access token available in OAuth server response: " + response);
        }

        BrokeredIdentityContext context = doGetFederatedIdentity(accessToken);
        context.getContextData().put(FEDERATED_ACCESS_TOKEN, accessToken);
        return context;
    }

    protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
        BrokeredIdentityContext context = null;

        try {
            JsonNode openidResponse = generateOpenIdRequest(accessToken).asJson();
            String openId = openidResponse.get("d").asText();
            if (openId == null) {
                throw new Exception("Can not get openId from kujiale IDP");
            }
            JsonNode profile = generateUserInfoRequest(accessToken, openId).asJson();
            context = extractIdentityFromProfile(null, profile);
        } catch (Exception ex) {
            logger.warn("Cannot GetFederatedIdentity from kujiale IDP, error " + ex.getMessage());
        }

        return context;
    }

    public SimpleHttp generateOpenIdRequest(String accessToken) {
        SimpleHttp openIdRequest = SimpleHttp.doGet(OPENID_URL, session)
                .param("access_token", accessToken);
        return openIdRequest;
    }

    public SimpleHttp generateUserInfoRequest(String accessToken, String openId) {
        SimpleHttp openIdRequest = SimpleHttp.doGet(PROFILE_URL, session)
                .param("open_id", openId)
                .param("access_token", accessToken);
        return openIdRequest;
    }

}

定制属性映射: IdentityProviderMapper

其实属性映射,本质上只是新增个说明,我们的Provider支持 Import Attribute,
所以代码都是模板代码

public class KujialeUserAttributeMapper extends AbstractJsonUserAttributeMapper {

    private static final String[] cp = new String[]{KujialeIdentityProviderFactory.PROVIDER_ID};

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

    @Override
    public String getId() {
        return "kujiale-user-attribute-mapper";
    }

}

添加META-INF

添加
org.keycloak.broker.social.SocialIdentityProviderFactory

内容为:自己的ProviderFactory全类名

添加
org.keycloak.broker.provider.IdentityProviderMapper

内容为 自己Mapper的全类名

NoClassDefFoundError报错

在这里插入图片描述

相关问题链接
https://stackoverflow.com/questions/57778240/noclassdeffounderror-in-a-provider-jar-when-using-a-class-from-org-keycloak-auth

解决方法

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <configuration>
        <archive>
            <manifestEntries>
                <Dependencies>org.keycloak.keycloak-services</Dependencies>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

部署Provider及配置使用

部署

拷贝到 /standalone/deployments中,重启服务

重启服务后,到 Admin ==> server info 确认新增的Provider生效

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

配置

新增IDP在这里插入图片描述

设置属性mapper

在这里插入图片描述

可以设置默认使用的IDP

在这里插入图片描述

第一次登录,会自动创建用户已经,导入对应的属性信息

在这里插入图片描述

属性:

在这里插入图片描述

关联的IDP信息
在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐