Force.com微信企业号开发系列(一) - 启用二次验证
微信于9月份推出企业号后引起了业界不小的反响,许多企业都在思索企业号将如何影响企业的运营,从本文开始,我将详细阐述微信企业号开发的相关知识,而本文将着重介绍如何实现更高安全机制的二次验证。
申请企业体验号:
企业号顾名思义就是企业来申请的号,申请时就像申请服务号一样,需要提供各种组织证明文件,对广大开发者来说很难操作,好在腾讯公司也像服务号一样开通了体验号申请,留意企业体验号的有效期间非常短,只有90天(服务号测试账号有1年有效期),且如果企业体验号长期不使用还会收到腾讯公司的提前失效提醒邮件。企业体验号的申请链接如下,开发者只需要按照腾讯公司的引导完成注册步骤,立刻就能获得体验号:
http://qydev.weixin.qq.com/try?t=experience
通讯录添加成员:
与公众号不同的是,因为是面向企业内部,所以腾讯允许企业主动添加粉丝,具体操作是进入到通讯录后点击+按钮添加新成员,留意作为唯一识别个人信息,微信号、手机号或者邮箱必须至少有一个,直接搜集微信号通常比较困难,一般可以使用企业HR数据库里的手机号和邮箱等信息,具体操作上除了手工添加还可以通过Excel模板导入以及通过腾讯企业号微信API来添加,关于API添加用户稍后章节介绍。
输入完成以后,可以将企业微信号的二维码发送给员工,员工扫描后会自动出现系统默认的企业号小助手,小助手会自动引导员工通过邮箱或手机验证码来完成员工身份绑定的过程,此为一次验证,企业自行确保通讯录员工数据的正确性,后续依赖于腾讯公司来进行员工验证,验证通过后通讯录状态列的问号会消失,表明一次验证通过:
启用二次验证:
一次验证通常能够满足大多数企业的要求,但对于员工信息以及权限管理比较严格的公司来说,一次验证还不足够放心,希望能够员工通过输入公司内部的用户名和密码再进行一次验证,此为二次验证,二次验证的启用位于企业号首页设置处,到设置画面后滚动画面找到二次验证,点击右侧的选择钮启用二次验证:
此时会弹出如下窗口需要输入企业的二次验证页面地址:
为此我们可以参考企业号官方接口文档http://qydev.weixin.qq.com/wiki/index.php?title=%E5%85%B3%E6%B3%A8%E4%B8%8E%E5%8F%96%E6%B6%88%E5%85%B3%E6%B3%A8在Force.com平台开发相应页面。
开发二次验证用页面:
同样,页面分成两个部分,一部分是显示部分,用来输入用户名和密码,页面示意图如下,用户输入用户名user以及密码123点击绑定按钮既可以完成绑定:
页面名称是EmployeeAuth,页面代码如下,有些属于apex代码特有的标签,无需做深入理解,重要是在第13行按钮的action属性指定了bind方法,当点击按钮的时候将调用控制器类EmployeeAuthController的bind方法:
1 <apex:page standardstylesheets="false" showHeader="false" sidebar="false" controller="EmployeeAuthController"> 2 <font size="50"> 3 <h1>Please input your user name and password</h1> 4 </font> 5 <font size="30"> 6 UserName: user<br /> 7 Password: 123<br /><br /> 8 <hr/> 9 <apex:form > 10 UserName: <apex:inputText size="100" style="height:100px" value="{!strUsername}" id="strUsername"/><br /><br /> 11 Password: <apex:inputText size="100" style="height:100px" value="{!strPassword}" id="strPassword"/><br /><br /> 12 <center> 13 <apex:commandButton value="Bind" style="width:600px; height:100px;font-size:50px" action="{!bind}" id="bind" /> 14 </center> 15 </apex:form> 16 {!msg} 17 </font> 18 </apex:page>
在解读EmployeeAuthController控制器类的代码前我们首先看看微信二次认证的步骤。
二次验证的步骤与机理:
1. 首先,当微信一次验证(或邮箱或手机号码等认证)完成后,微信会发送如下图所示的消息给到用户:
2. 页面跳转:
当用户点击这个图文的时候实际上打开了一个位于open.weixin.qq.com网站下面的网页,这个页面会做一些处理后跳转到前面在二次验证里设置的URL也就是我们正在开发的这个页面,在跳转的时候还会再我们设置的URL后面加上参数code=CODE&state=STATE,例如在本例里二次验证配置的URL是http://johnson0001-developer-edition.ap1.force.com/EmployeeAuth,那么从腾讯openweixin.qq.com跳转后实际打开的URL是http://johnson0001-developer-edition.ap1.force.com/EmployeeAuth?code=CODE&state=STATE 。这里的state参数是干嘛的腾讯公司并没有说明目前看也并不重要。重要的是code参数,利用这个参数可以调用腾讯的oauth2接口换取员工的userid,留意userid是一个很重要的概念,在企业号里没有微信OpenId一说,只有userid用来唯一标识用户,这个userid实际上就是我们在维护通讯录时的账号字段值:
3. 通过code调用腾讯oauth2接口换取员工userid
关于这个接口的说明参见腾讯文档http://qydev.weixin.qq.com/wiki/index.php?title=%E6%A0%B9%E6%8D%AEcode%E8%8E%B7%E5%8F%96%E6%88%90%E5%91%98%E4%BF%A1%E6%81%AF,也可以参加下方说明,这里需要特别说明的是access token和agentid:
做过微信公共号开发或者看过前面介绍相关开发文章的读者应该不会陌生,当主动调用腾讯的api时都需要access token已确保访问的正当性,获得access token相应的也有一个专门的接口,具体的介绍可以参见腾讯公司文档http://qydev.weixin.qq.com/wiki/index.php?title=%E4%B8%BB%E5%8A%A8%E8%B0%83%E7%94%A8,简单点说获得access token实际就是通过以下接口:
https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=id&corpsecret=secrect
这个接口里Corpid好找,打开设置就能找到,如下图:
不过corpsecret就没那么好找,实际是需要系统管理员在后台创建管理组,创建管理组后就可以拥有相应的Secret,而这个Secret所拥有的访问权限就是系统管理员创建的管理组所拥有的权限,腾讯文章http://qydev.weixin.qq.com/wiki/index.php?title=Secret也有提到:
再回过头来说agentid腾讯文档里提到指的是“跳转链接时所在的企业应用ID”,在本例里其实指的就是发送“身份验证”图文消息的那个应用也就是“企业小助手”的应用ID,当然在不同的用户场景里可能会是不同的应用在调用换取userid接口,如何查看“企业小助手”的应用ID呢?进到应用中心,第一个就是企业小助手,点击进入就可以看到如下图所示的企业应用ID了:
4. 二次验证
拿到userid后实际就可以进行二次验证了,二次验证的方式有很多种,例如如果公司已经建立起良好的通讯录管理机制(userid等和企业人力资源数据库同步,入职离职员工均能和企业号通讯录同步),拿到userid后只要判断这个userid是一位在职员工就可以自动判断为二次验证通过,或者再保险点如本例演示的,要求员工输入公司的员工用户名和密码进行验证。留意,输入用户名和密码验证的页面也就是我们前面提到的二次验证页面是属于企业拥有也是企业开发的,这样就确保了企业对安全的控制,具体操作上,用户输入用户名和密码后企业可以调用已有的接口进行验证,如果验证成功则将员工的userid等信息保存在业务系统数据库中一遍后续操作。
5.通知腾讯关注成功
现在最后一步等企业在自己的网页里完成了用户验证后只剩下通知腾讯该用户已经验证成功让相应员工关注成功,此时应该调用如下接口,可以看到接口需要的第二个参数即是我们前面换回来的userid:
https://qyapi.weixin.qq.com/cgi-bin/user/authsucc?access_token=ACCESS_TOKEN&userid=USERID
此接口的详细说明如下:
二次验证的代码实现:
按照前面的思路,我们首先获取从腾讯跳转过来的code,并通过code换取用户的userid,换取的这个过程在页面加载中完成,为此主要代码应放在类构造器里。下面的代码里设置了五个变量,其中strPassword和strUsername和用户在页面里输入的用户名和密码相对应,userID用来存储换回来的userid信息,msg用来调试帮助在页面里显示中间信息,accessToken则用来存储access token:
1 public class EmployeeAuthController { 2 3 public String strPassword { get; set; } 4 public String strUsername { get; set; } 5 public String msg { get; set; } 6 public String userID { get; set; } 7 public String accessToken { get; set; } 8 9 public EmployeeAuthController (){ 10 accessToken = obtainAccessToken(); 11 String code = ApexPages.currentPage().getParameters().get('code'); 12 //Obtain user ID 13 Http h = new Http(); 14 HttpRequest req = new HttpRequest(); 15 req.setMethod('GET'); 16 req.setHeader('Accept-Encoding','gzip,deflate'); 17 req.setHeader('Content-Type','text/xml;charset=UTF-8'); 18 req.setHeader('User-Agent','Jakarta Commons-HttpClient/3.1'); 19 req.setEndpoint('https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=' + accessToken + '&code=' + code + '&agentid=0'); 20 String bodyRes = ''; 21 try{ 22 HttpResponse res = h.send(req); 23 bodyRes = res.getBody(); 24 } 25 catch(System.CalloutException e) { 26 System.debug('Callout error: '+ e); 27 ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL, e.getMessage())); 28 } 29 msg = bodyRes ; 30 //String operation to obtain userID: 31 JSONParser parser = JSON.createParser(bodyRes); 32 while(parser.nextToken() != null){ 33 if((parser.getCurrentToken() == JSONToken.FIELD_NAME)){ 34 String fieldName = parser.getText(); 35 parser.nextToken(); 36 if(fieldName == 'UserId'){ 37 userID = parser.getText(); 38 } 39 } 40 } 41 msg = userID; 42 } 43 44 }
上述代码第9行调用obtainAccessToken方法获取accessToken,后续会介绍该方法的详情,accessToken两个小时内会失效,所以这里采取实时获取的方式,当然可以设计的再巧妙些以省却每次实时获取accessToken的网络开销。第10行获得了从腾讯跳转过来时带的code参数,从第11行通过HttpRequest方法来调用换取接口获得userid,留意第18行指定了agentid为0,这是因为验证消息是从企业小助手应用发起的,而企业小助手应用id是0。第29行开始解析返回来的JSON数据获取userid。
下面是obtainAccessToken方法,方法内容也比较直接,主要通过调用gettoken接口来获取accessToken,并通过JSONParser类来解析返回的JSON数据以获得accessToken:
1 private String obtainAccessToken(){ 2 String token; 3 Http h = new Http(); 4 HttpRequest req = new HttpRequest(); 5 req.setMethod('GET'); 6 req.setHeader('Accept-Encoding','gzip,deflate'); 7 req.setHeader('Content-Type','text/xml;charset=UTF-8'); 8 req.setHeader('User-Agent','Jakarta Commons-HttpClient/3.1'); 9 req.setEndpoint('https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=wx548178d7f347f582&corpsecret=9pwWy0AVoT6V65hnwZLYdi4jnLLx65ofBRb_Ds0mAozysQoywDaqbqYCqglm2vhr'); 10 String bodyRes = ''; 11 try{ 12 HttpResponse res = h.send(req); 13 bodyRes = res.getBody(); 14 } 15 catch(System.CalloutException e) { 16 System.debug('Callout error: '+ e); 17 ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL, e.getMessage())); 18 } 19 msg = bodyRes; 20 JSONParser parser = JSON.createParser(bodyRes); 21 while(parser.nextToken() != null){ 22 if((parser.getCurrentToken() == JSONToken.FIELD_NAME)){ 23 String fieldName = parser.getText(); 24 parser.nextToken(); 25 if(fieldName == 'access_token'){ 26 token= parser.getText(); 27 } 28 } 29 } 30 msg = token; 31 return token; 32 }
接下来最重要的方法是bind方法,该方法将负责用户身份验证以及通知腾讯用户关注成功,可以看到下面代码里第2行到第6行只做了很简单的用户名密码校验,真实场景里可以根据企业的具体认证机制进行替换,从第9行开始也即企业内部用户认证通过后开始调用authsucc接口通知腾讯用户关注成功。
1 public PageReference bind() { 2 if(!strUsername.equals('user')){ 3 msg = 'Please input correct user name'; 4 } 5 else if(!strPassword.equals('123')){ 6 msg = 'Please input correct password'; 7 } 8 else{ 9 msg = 'Bind successfully!'; 10 //Notify tencent to add user 11 Http h = new Http(); 12 HttpRequest req = new HttpRequest(); 13 req.setMethod('GET'); 14 req.setHeader('Accept-Encoding','gzip,deflate'); 15 req.setHeader('Content-Type','text/xml;charset=UTF-8'); 16 req.setHeader('User-Agent','Jakarta Commons-HttpClient/3.1'); 17 req.setEndpoint('https://qyapi.weixin.qq.com/cgi-bin/user/authsucc?access_token=' + accessToken + '&userid=' + userID); 18 String bodyRes = ''; 19 try{ 20 HttpResponse res = h.send(req); 21 bodyRes = res.getBody(); 22 } 23 catch(System.CalloutException e) { 24 System.debug('Callout error: '+ e); 25 ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL, e.getMessage())); 26 } 27 msg = bodyRes ; 28 } 29 } 30