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_token
和code
请求用户身份信息,还可进一步获取用户详细信息,用这些信息查询或建立新用户,完成登录。最后重定向回到业务服务器,业务服务器即可向用户提供服务。
回到代码,我们现在的任务就是仿照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()
其实就重定向、访问了企业微信授权接口,此接口带着code
和state
参数回调你指定的redirect_uri
。
redirect_uri
按照是自动配置的,我们现在无需过多关注,需要关注的是其处理逻辑,是在内部类Endpoint
里的authResponse
方法,重点关注authorizationCode
即code
这个参数,还有真正获取、设置用户信息的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>
这样集成 keycloak配置完企业微信的信息,为啥getConfig().getAgentId()获取不到值