使用企业微信 (Identity Provider) 登录Keycloak (Identity Broker)

Keycloak是一套不错的IAM解决方案,它能够实现SSO,还可以作为Identity Broker集成多种第三方登录方式。Keycloak自带常见的Social Login,包括Google、GitHub、Twitter等等,但是没有微信和企业微信。鉴于我司企业微信的广泛使用的事实, 在实现企业SSO服务过程中我们决定将其集成为主要第三方登录方式,基于Keycloak 6.0.1进行企业微信Identity Provider研发,本文记录了个人在此中积累的一点经验。本人主力语言并不是Java,文章有错误或您有更佳方案,请指正。

首先感谢https://gitee.com/jyqq163/keycloak-services-social-weixin 这个项目,指路明灯!企业微信与微信差异不大,我们做些改造即可。

查看项目里的WeixinIdentityProvider类,我们发现它extends AbstractOAuth2IdentityProvider;,这个org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider就是我们要实现的目标。

其实这个类不完全是问题的关键,因为你上来直接去看,不一定看得懂,所以问题的关键是要熟悉OAuth2的基本概念和常用认证流程。比如,Authentication和Authorization的区别是什么?OAuth2和OpenID Connect(OIDC)是什么关系?OAuth2里面Resource Owner是什么,Client是什么,Identity Provider是什么,User Federation又是怎么回事?常见的Authorization Grant有几种?分别是什么?哪两种比较常见?

超过一半答不上来的话,没有捷径,请老老实实先翻资料:https://tools.ietf.org/html/rfc6749 。不过说实话RFC文档并不适合阅读,你应该找其他面向人类的资料,比如http://gen.lib.rus.ec/book/index.php?md5=3707B342B22E5C059B2F17FEF8AB7D2F 。But when in doubt, read RFC。

另外你还要熟悉企业微信并不是100%符合标准的”OAuth2″认证流程,比如单独获取的、7200秒有效的、需要你缓存的access_token并不是 OAuth2 标准里的用户级access_token,而是全局的,它不是在认证第二步用code换回的,而是用corpId(clientId)和corpSecret(clientSecret)直接获取,是相对独立的流程。更多内容可参考https://zhuanlan.zhihu.com/p/36320213

有了清晰的概念之后就好办了。SSO一般使用流程是:用户想使用一个业务,使用客户端(一般是浏览器)访问业务服务器,业务服务器检查用户当前session状态,如果没有或过期,则向SSO服务检查用户是否已统一登录。如果SSO已登录则直接刷新自己的session,为用户提供服务;如果SSO未登录,则将用户重定向到SSO登录页。

此时用户有多种登录方式可选,我们这里是:直接登录Keycloak,或企业微信登录。用户选择企业微信登录后,Keycloak作为broker向向企业微信请求access_token,再向authorize endpoint发送指定appid、redirect_uri、response_type(grant type)、scope等参数的认证请求(链接/扫码),企业微信返回code后,再次利用access_tokencode请求用户身份信息,还可进一步获取用户详细信息,用这些信息查询或建立新用户,完成登录。最后重定向回到业务服务器,业务服务器即可向用户提供服务。

回到代码,我们现在的任务就是仿照WeixinIdentityProvider类,实现一个WechatWorkIdentityProvider类,让Keycloak认得企业微信。

从认证请求开始,方法名叫performLogin,看它基类的介绍Initiates the authentication process,这就是social login的起点。参考https://work.weixin.qq.com/api/doc#90000/90135/91022网页授权登录(这种方式在企业微信内打开有效,此外还有另一种二维码方式,可在企业微信外部打开。因此你可以根据user agent是否包含wxwork字样,生成不同的授权链接,余下的步骤是一样的),我们不难将链接的构造方法createAuthorizationUrl改造为适合企业微信的。然后Response.seeOther(authorizationUrl).build()其实就重定向、访问了企业微信授权接口,此接口带着codestate参数回调你指定的redirect_uri

redirect_uri按照是自动配置的,我们现在无需过多关注,需要关注的是其处理逻辑,是在内部类Endpoint里的authResponse方法,重点关注authorizationCodecode这个参数,还有真正获取、设置用户信息的getFederatedIdentity方法。根据企业微信文档,你需要拿code再加上单独获取的access_token来获取访问用户身份https://work.weixin.qq.com/api/doc#90000/90135/91023 ,取得UserId。随后还能进一步获取用户在企业微信通讯录里的详细信息:https://work.weixin.qq.com/api/doc#10019 。在extractIdentityFromProfile方法中,你可以将信息填入BrokeredIdentityContext,这样我们的扩展插件经手的工作就结束了,剩余的交给Keycloak处理。

注意企业微信比微信多了一个AgentId属性,Keycloak修改前端页面就能直接添加字段, 使用的是angular框架。我们可以在/keycloak-services-social-weixin/templates/realm-identity-provider-wechat-work.html里找到clientId属性,具体是

            <div class="form-group clearfix">
                <label class="col-md-2 control-label" for="clientId"><span class="required">*</span> {{:: 'pc-weixin-appid' | translate}}</label>
                <div class="col-md-6">
                    <input class="form-control" id="clientId" type="text" ng-model="identityProvider.config.clientId" required>
                </div>
                <kc-tooltip>{{:: 'social.client-id.tooltip' | translate}}</kc-tooltip>
            </div>

其实对应到企业微信就是CorpID。复制一段,把信息全改为AgentID

            <div class="form-group clearfix">
                <label class="col-md-2 control-label" for="agentId"><span class="required">*</span> {{:: 'Agentid' | translate}}</label>
                <div class="col-md-6">
                    <input class="form-control" id="agentId" type="text" ng-model="identityProvider.config.agentId" required>
                </div>
                <kc-tooltip>{{:: 'social.agent-id.tooltip' | translate}}</kc-tooltip>
            </div>

这就为AgentID找到了保存的地方。可以在Provider里调用。内部怎样调取AgentId不具体细说了,看看源码,扩展OAuth2IdentityProviderConfig增加getAgentId方法,很容易的。

最后修改pom.xml文件,把项目名等信息改为 wechat-work ,调用mvn clean package把项目编译为独立Jar包,放入 KEYCLOAK_HOME/providers/(不存在就创建),把templates/*放入 KEYCLOAK_HOME/themes/base/admin/resources/partials/,重启Keycloak服务就可以在Identity Providers里找到wechat-work,新建,填写CorpID, AgentID, CorpSecret,随后在首页就会出现WechatWork登录渠道。

虽然文档异常缺乏,mailing list提问也没人理,Wildfly依赖关系花了一个星期才弄明白,但为Keycloak做功能扩展确实算很容易了,插件式的集成方式完全不用侵入其源码,这一点让人觉得选Keycloak还是正确的。

源码在此: https://github.com/kkzxak47/keycloak-services-social-wechatwork

PS: 为了缓存企业微信的access_token,我直接使用了org.infinispan.Cache,因为Keycloak自己就使用了这个缓存组件,我就不用再增加依赖了。但最后还是遇到了依赖问题Uncaught server error: java.lang.NoClassDefFoundError: org/infinispan/configuration/cache/ConfigurationBuilder,原本在项目pom.xml里添加了org.infinispan只在编译时有效,最后部署时,还是要在KEYCLOAK_HOME/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml里的dependencies里添加依赖,module.xml文件最后长这样:

<module name="org.keycloak.keycloak-services" xmlns="urn:jboss:module:1.3">
    <properties>
        <property name="jboss.api" value="private"/>
    </properties>

    <resources>
        <resource-root path="keycloak-services-6.0.1.jar"/>
    </resources>

    <dependencies>
        <module name="org.infinispan" services="import"/>
        <module name="org.keycloak.keycloak-common" services="import"/>
    ...
    </dependencies>
</module>