最近这些年,我分别在不同的系统中对接过不同的社交身份源,它们一般都是遵循 OIDC 标准,或者是不太标准的 OIDC 标准,但是看着又有点像。今天写一个总结吧!

开发一个社交账号登录功能,只需要三步。

不要自行实现!

三步虽好,0 步最佳!

三思而后行,千万不要一上来就想自己写代码来实现,或者引入一个额外的第三方库。如果是一个 OIDC 标准的产品,很可能不用写一行代码,就能像拼搭积木一样集成完毕。
举个例子,曾经接到一个咨询,希望在 Keycloak 中集成 Twitch 登录,于是找到了 GitHub 上的一个 Keycloak-Twitch 插件: https://github.com/intricate/keycloak-twitch,但是碰到了问题。
image.png
我去看了一下这个开源库,发现是针对一个很老的 Keycloak 版本,如果要升级它以适配新的 Keycloak,代价蛮大的。于是我去看一下 Twitch,发现它是一个标准的 OIDC,通过 https://id.twitch.tv/oauth2/.well-known/openid-configuration 可以查看它的 OpenId 配置信息。于是建议提问者,扔掉那个开源库,直接通过 OIDC 的方式来完成:
image.png

还有一个例子,就是在 Keycloak 中对接阿里云登录,也是不需要写一行代码的:详见《【多图超详细】用 oidc 方式在 keycloak 中集成阿里云登录方式 - Jeff Tian的文章 - 知乎 》。
有一些产品,尽管看上去不是一个标准的 OIDC,但也可能通过配置完成,只是没有那个发现端点,从而需要单独配置一些接口,它们分别是:

  • 授权接口地址,通过它,可以得到一个授权码
  • 换取令牌接口地址,通过它,可以通过授权码换取到令牌
  • 用户信息接口地址,通过它,可以使用令牌获取到用户个人资料

如果换取令牌时,在身份令牌中就包含了用户的唯一身份标识,甚至 Email 等信息,那么以上第三个接口都是可以省略的。

自行实现的一般套路

尽管建议不要自行实现,但总有需要自行实现的时候,比如对接一些非 OIDC 标准的产品时。这时候,不要慌,只要有套路,实现起来非常简单。这个套路,其实在上一节已经透露出来了,它是在配置 OIDC 提供者时需要填写的一些关键信息,其实也是自行实现社交登录插件时的关键步骤,分别是:

  • 构建授权链接,将用户重定向至身份源的授权页面,只要用户授权,你的应用就会得到一个授权码
  • 使用授权码换取令牌,做得好一点可以为其实现一个缓存,以在一定时间窗口内,减少用户授权的麻烦
  • 使用令牌换取用户个人资料,以用户的身份信息,为其在你的应用中创建持久化的会话

以钉钉登录举例

钉钉登录似乎有一些特别,一是我没有从官方文档中找到其 OIDC 发现端点,很可能是没有。另外,它有上面说的那三个关键接口,但返回的字段名称,却和标准有差异,比如命令风格不是 snake_case,而是 camelCase。总之,没能成功地在 Keycloak 中通过配置的方式实现钉钉登录,不得不写了一个自定义插件。这里就以它举例,来说明一下在 Keycloak 里实现自定义的社交登录插件的方法。

源代码见:https://github.com/Jeff-Tian/keycloak-services-social-dingding

在介绍开发套路前,先看一下如何在 Keycloak 中引入它:

在 Keycloak 中引入的两种方式

通过手动拷贝到 /providers 目录

这需要你从源码执行 mvn clean install,在 target目录找到相应的 jar 包,然后拷贝到 Keycloak 的 providers 目录下。
当然也可以不用从源码自行编译,而是直接下载我编译好的,地址是: https://github.com/Jeff-Tian/keycloak-services-social-dingding/packages/1982789

通过 pom 方式引入

在 Keycloak 项目中的 pom 中引入,然后在 Dockerfile 里编译整个项目,顺带会将该插件编译出来,并统一拷贝。可以参考 https://github.com/Jeff-Tian/keycloak-heroku/blob/master/pom.xml 以及 https://github.com/Jeff-Tian/keycloak-heroku/blob/master/Dockerfile 。然而,由于以上 package 发布到了 GitHub 的仓库,尽管可以手动下载,但是通过 pom 方式引入时却需要一个 token。不过我有计划将它发布到 Maven 中央仓库,这样就不需要 token 了,敬请期待。

通过 pom 方式引入后,还需要在 META-INF/services 里列出对它的引用,参考 https://github.com/Jeff-Tian/keycloak-heroku/blob/master/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory

org.keycloak.social.dingding.DingDingIdentityProviderFactory

开发步骤

先创建仓库,并建立 pom 文件如 https://github.com/Jeff-Tian/keycloak-services-social-dingding/blob/main/pom.xml

(1)构建授权链接

这是将用户从你的应用,重定向到钉钉的授权页面,即 https://login.dingtalk.com/oauth2/auth 。这个方法,要做的不多,就是拼接一些参数: java @Override protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) { final UriBuilder uriBuilder;

String ua = request.getSession().getContext().getRequestHeaders().getHeaderString(user-agent);

uriBuilder = UriBuilder.fromUri(getConfig().getAuthorizationUrl());

uriBuilder
	.queryParam(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
	.queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri())
	.queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, DEFAULT_RESPONSE_TYPE)
	.queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope())
	.queryParam(OAUTH2_PARAMETER_STATE, request.getState().getEncoded());

return uriBuilder;

}

详见 https://github.com/Jeff-Tian/keycloak-services-social-dingding/blob/main/src/main/java/org/keycloak/social/dingding/DingDingIdentityProvider.java#L224

(2)使用授权码换取令牌

其实就是对钉钉的令牌接口进行一个调用,该接口地址是 https://api.dingtalk.com/v1.0/oauth2/userAccessToken 。之所以不能通过配置方式进行对接,最大的原因之一是这个接口返回的字段名称,相比 OIDC 标准来说有些奇怪,所以通过自行写代码就能解决。
由于令牌返回后,可以在 2 小时内重复使用,所以可以实现一个缓存机制,于是代码看上去有些多,但本质上就是一个 HTTP call。
image.png

(3)获取用户资料

由于钉钉在令牌阶段,没有给出用户的识别信息,只能再次调用 https://api.dingtalk.com/v1.0/contact/users/me 接口拿到用户的 openId、unionId 以及手机号、邮箱等信息。这本质上也是一个 HTTP call,但是令牌需要通过 x-acs-dingtalk-access-token头部携带,非常特别,这也是不能通过配置来实现对接的原因之一。

java

@Override protected BrokeredIdentityContext extractIdentityFromProfile( EventBuilder event, JsonNode profile) { logger.info(profile.toString()); BrokeredIdentityContext identity = new BrokeredIdentityContext((getJsonProperty(profile, unionId)));

identity.setUsername(getJsonProperty(profile, nick).toLowerCase());
identity.setBrokerUserId(getJsonProperty(profile, unionId).toLowerCase());
identity.setModelUsername(getJsonProperty(profile, nick).toLowerCase());
String email = getJsonProperty(profile, email);
if (email != null) {
    identity.setFirstName(email.split(@)[0].toLowerCase());
    identity.setEmail(email);
}
identity.setLastName(getJsonProperty(profile, nick));
// 手机号码,第三方仅通讯录应用可获取
identity.setUserAttribute(PROFILE_MOBILE, getJsonProperty(profile, mobile));

identity.setIdpConfig(getConfig());
identity.setIdp(this);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(
        identity, profile, getConfig().getAlias());
return identity;

}

public BrokeredIdentityContext getFederatedIdentity(String authorizationCode) { logger.info(getting federated identity);

String accessToken = getAccessToken(authorizationCode);
if (accessToken == null) {
    throw new IdentityBrokerException(No access token available);
}
BrokeredIdentityContext context = null;
try {
    JsonNode profile;
    profile =
            SimpleHttp.doGet(PROFILE_URL, session)
                    .header(x-acs-dingtalk-access-token, accessToken)
                    .asJson();
    logger.info(profile in federation  + profile.toString());

    context = extractIdentityFromProfile(null, profile);
    context.getContextData().put(FEDERATED_ACCESS_TOKEN, accessToken);
} catch (Exception e) {
    logger.error(e);
    e.printStackTrace(System.out);
}
return context;

}

完成这三步,一个社交账号登录功能就开发成功了。虽然是以在 Keycloak 中开发钉钉登录举例,但其实所有系统中的社交账号登录功能都是这样的套路。