分享

钉钉实现企业级微应用免登陆详解

 AS400r 2017-12-18

(一)基本概述:

钉钉中实现免登陆的核心思想就是通过corpId和corpSecret这两个参数来获得免登陆码Code,继而通过Code来获取用户信息,并在后台数据库中比对该用户信息是否存在,如果比对成功就免登陆成功。具体实现的流程图如下:



(二)过程详解:

1.注册企业用户和创建微应用:

这个过程比较简单,略过。

2.获取corpId,corpSecret,agentId:

可以登录钉钉企业用户账号直接获得,可以存储在本地文件中,便于后面存取,本人存储在本地的properties文件中。略过。

3.获取access_token:

钉钉官方文档中有获取access_token的方法介绍,通过get方式向https://oapi.dingtalk.com/gettokencorpid=id&corpsecret=secrect请求access_token数据,其中需要两个参数,分别是corpId和corpSecret,网页响应值就是access_token,具体java实现如下:

  1. 定义一个get请求的方法:  
  2. public class HttpHelper {  
  3. /* 
  4. * params: 
  5. *       url:需要Get请求的网址 
  6.  
  7. * return: 
  8. *       返回请求时网页相应的数据,用json存储 
  9. */  
  10. public static JSONObject httpGet(String url){  
  11.         //创建httpClient  
  12.     CloseableHttpClient httpClient=HttpClients.createDefault();  
  13.           
  14.     HttpGet httpGet=new HttpGet(url);                             //生成一个请求  
  15.     RequestConfig requestConfig = RequestConfig.custom().         //配置请求的一些属性  
  16.             setSocketTimeout(2000).setConnectTimeout(2000).build();  
  17.         httpGet.setConfig(requestConfig);                             //为请求设置属性  
  18.     
  19.         CloseableHttpResponse response=null;  
  20.           
  21.         try {  
  22.         response=httpClient.execute(httpGet);  
  23.               
  24.         //如果返回结果的code不等于200,说明出错了  
  25.         if (response.getStatusLine().getStatusCode() != 200) {  
  26.                         System.out.println("request url failed, http code=" + response.getStatusLine().getStatusCode()+ ", url=" + url);  
  27.                         return null;  
  28.                 }  
  29.               
  30.         HttpEntity entity = response.getEntity();                 //reponse返回的数据在entity中  
  31.               
  32.         if(entity!=null){  
  33.             String resultStr=EntityUtils.toString(entity,"utf-8");//将数据转化为string格式  
  34.                   
  35.             JSONObject result=JSONObject.parseObject(resultStr);  //将结果转化为json格式  
  36.             if(result.getInteger("errcode")==0){                  //如果返回值得errcode值为0,则成功  
  37.                 //移除一些没用的元素  
  38.                 result.remove("errcode");  
  39.                 result.remove("errmsg");  
  40.                 return result;                                    //返回有用的信息  
  41.             }  
  42.             else{                                                 //返回结果出错了,则打印出来  
  43.                 System.out.println("request url=" + url + ",return value=");  
  44.                                 System.out.println(resultStr);  
  45.                                 int errCode = result.getInteger("errcode");  
  46.                                 String errMsg = result.getString("errmsg");  
  47.                                 throw new Exception("ErrorCode:"+errCode+"ErrorMsg"+errMsg);   
  48.             }  
  49.         }  
  50.     } catch (ClientProtocolException e) {  
  51.         // TODO Auto-generated catch block  
  52.         System.out.println("request url=" + url + ", exception, msg=" + e.getMessage());  
  53.         e.printStackTrace();  
  54.     } catch (Exception e) {  
  55.         // TODO Auto-generated catch block  
  56.         System.out.println("request url=" + url + ", exception, msg=" + e.getMessage());  
  57.         e.printStackTrace();  
  58.     } finally {  
  59.         if (response != nulltry {  
  60.             response.close();  
  61.         } catch (IOException e) {  
  62.             e.printStackTrace();  
  63.         }  
  64.     }  
  65.           
  66.     return null;  
  67. }  

  1.       
  2. 然后是调用httpGet方法获得access_token的代码实现:  
  3. public static String getAccess_Token(String corpid,String corpsecret){    
  4.     String url="https://oapi.dingtalk.com/gettoken?"+"corpid="+corpid+"&corpsecret="+corpsecret;  
  5.           
  6.     JSONObject res=HttpHelper.httpGet(url);                      //将httpGet方法封装在HttpHelper类中  
  7.     String access_token="";  
  8.     if(res!=null){  
  9.         access_token=res.getString("access_token");  
  10.     }  
  11.     else{  
  12.         new Exception("Cannot resolve field access_token from oapi resonpse");  
  13.     }  
  14.     return access_token;  
  15. }  


4.获取ticket:

钉钉官方文档有关于ticket获取的介绍,通过get方式向https://oapi.dingtalk.com/get_jsapi_ticket?access_token=ACCESS_TOKE
请求ticket数据,请求时需要携带一个参数access_token,也就是在步骤3中获得的access。具体java代码实现如下:

  1. /* 
  2. * 向网页请求ticket值,用Get方式请求网页 
  3. * param: 
  4. *   access_token:上面得到的access_token值 
  5.  
  6. * return: 
  7. *   返回值是ticket 
  8. */  
  9. public static String getTicket(String access_token){  
  10.     String url="https://oapi.dingtalk.com/get_jsapi_ticket?"+  
  11.             "access_token="+access_token;  
  12.           
  13.     JSONObject res=HttpHelper.httpGet(url);                                //步骤3中有httpGet的定义,只是封装在HttpHelper类中  
  14.     String ticket="";  
  15.     if(res!=null){  
  16.         ticket=res.getString("ticket");  
  17.     }  
  18.     else{  
  19.         new Exception("Cannot resolve field ticket from oapi resonpse");  
  20.     }  
  21.     return ticket;  
  22. }  

5.获取签名signatrue:

钉钉官方文档中有关于获取签名的介绍,并且给出了使用的算法,参数说明。所以我们只需要调用它使用的算法,并且做一些格式调整即可。具体java代码实现如下:

  1. /* 
  2. * 生成签名的函数 
  3. * params: 
  4. *   ticket:签名数据 
  5. *   nonceStr:签名用的随机字符串,从properties文件中读取 
  6. *   timeStamp:生成签名用的时间戳 
  7. *   url:当前请求的URL地址 
  8. */  
  9. public static String getSign(String ticket, String nonceStr, long timeStamp, String url) throws Exception {  
  10.     String plain = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "×tamp=" + String.valueOf(timeStamp)  
  11.             + "&url=" + url;  
  12.     try {  
  13.         MessageDigest sha1 = MessageDigest.getInstance("SHA-1");    //安全hash算法  
  14.         sha1.reset();  
  15.         sha1.update(plain.getBytes("UTF-8"));                       //根据参数产生hash值  
  16.         return bytesToHex(sha1.digest());  
  17.     } catch (NoSuchAlgorithmException e) {  
  18.         throw new Exception(e.getMessage());  
  19.     } catch (UnsupportedEncodingException e) {  
  20.         throw new Exception(e.getMessage());  
  21.     }  
  22. }  
  23.   
  24. //将bytes类型的数据转化为16进制类型  
  25. private static String bytesToHex(byte[] hash) {                    //将字符串转化为16进制的数据  
  26.     Formatter formatter = new Formatter();  
  27.     for (byte b : hash) {  
  28.         formatter.format("%02x", b);  
  29.     }  
  30.     String result = formatter.toString();  
  31.     formatter.close();  
  32.     return result;  
  33. }  


6.封装好所有需要的参数,并且传递到企业应用网址的前端H5中。

需要的参数有corpId,agentId,ticket,signature,nonceStr,timeStamp,url。其中nonceStr,timeStamp,url用来在服务器后台生成signatrue签名,然后将ticket,nonceStr,timeStamp和signatrue传送到前台,前台网页就会调用jsapi的dd.config函数重新生成signatrue,和传进的signatrue进行比较,来实现验证过程。java实现如下:

  1. 6.封装好所有需要的参数,并且传递到企业应用网址的前端H5中。需要的参数有corpId,agentId,ticket,signature,nonceStr,timeStamp,url.  
  2. 其中nonceStr,timeStamp,url用来在服务器后台生成signatrue签名,然后将ticket,nonceStr,timeStamp和signatrue传送到前台,前台网页就会  
  3. 调用jsapi的dd.config函数重新生成signatrue,和传进的signatrue进行比较,来实现验证过程。  
  4. /* 
  5. * 将所有需要传送到前端的参数进行打包,在前端会调用jsapi提供的dd.config接口进行签名的验证 
  6. *params: 
  7. *   request:在钉钉中点击微应用图标跳转的url地址 
  8. *return: 
  9. *   将需要的参数打包好,按json格式打包 
  10. */  
  11. public static String getConfig(HttpServletRequest request){  
  12.     /* 
  13.     *以http://localhost/test.do?a=b&c=d为例 
  14.     *request.getRequestURL的结果是http://localhost/test.do 
  15.     *request.getQueryString的返回值是a=b&c=d 
  16.     */  
  17.     String urlString = request.getRequestURL().toString();  
  18.     String queryString = request.getQueryString();  
  19.           
  20.     String url=null;  
  21.     if(queryString!=null){  
  22.         url=urlString+queryString;  
  23.     }  
  24.     else{  
  25.         url=urlString;  
  26.     }  
  27.           
  28.     String corpId=PropertiesHelp.getValue("corpid");        //一些比较重要的不变得参数本人存储在properties文件中  
  29.     String corpSecret=PropertiesHelp.getValue("corpsecret");  
  30.     String nonceStr=PropertiesHelp.getValue("noncestr");  
  31.     String agentId =PropertiesHelp.getValue("agentid");     //agentid参数  
  32.     long timeStamp = System.currentTimeMillis() / 1000;     //时间戳参数  
  33.     String signedUrl = url;                                 //请求链接的参数,这个链接主要用来生成signatrue,并不需要传到前端  
  34.     String accessToken = null;                              //token参数  
  35.     String ticket = null;                                   //ticket参数  
  36.     String signature = null;                                //签名参数  
  37.               
  38.     try {  
  39.               
  40.         accessToken=getAccess_Token(corpId,corpSecret);  
  41.         ticket=getTicket(accessToken);  
  42.         signature=getSign(ticket,nonceStr,timeStamp,signedUrl);  
  43.               
  44.     } catch (Exception e) {  
  45.         // TODO Auto-generated catch block  
  46.         e.printStackTrace();  
  47.     }  
  48.           
  49.     return "{jsticket:'" + ticket + "',signature:'" + signature + "',nonceStr:'" + nonceStr + "',timeStamp:'"  
  50.     + timeStamp + "',corpId:'" + corpId + "',agentId:'" + agentId+ "'}";  
  51. }  


7.前台对传进来的参数进行验证,并且生成code值,并且将code值传送给后台服务器程序。

验证过程需要调用jsapi的一些借口,所以我们要在前台网页中引入相应的js文件,引入的方法就是直接在前台网页中包含jsapi的js文件,引入代码如下:
  1. <script type="text/javascript" src="http://g./ilw/ding/0.7.3/scripts/dingtalk.js">  
  2. </script>  
引入了jsapi之后,我们就需要自己编写js文件对它的相应的接口进行调用,从而获得code,如下的代码是本人根据实际应用编写的js代码,具有详细的注解:

[javascript] view plain copy
print?
  1. /***************************开始****************************/  
  2. /** 
  3.  * _config 这个参数是在前台的H5文件中我定义的,它的值是通过调用步骤6中封装好的参数来获得的 
  4.  */  
  5.  /* 
  6.  我们需要明白的一点是,所有的这些文件都是放在企业应用的服务器后台,和钉钉网站没有半毛钱的关系 
  7.  并且钉钉的jsapi中唯一的作用就是提供了对config的验证和获得code值 
  8.  对于其他值得获取,如access_token,ticket,sign,username,userid都是自己在后台写java代码通过get或者post方式向 
  9.  钉钉开发平台请求得来的,并不是从jsapi中的接口得来的 
  10.  */  
  11. dd.config({                                                //dd.config方法会对参数进行验证  
  12.         agentId : _config.agentid,  
  13.         corpId : _config.corpId,  
  14.         timeStamp : _config.timeStamp,  
  15.         nonceStr : _config.nonceStr,  
  16.         signature : _config.signature,  
  17.         jsApiList : [                              //需要调用的借口列表    
  18.             'runtime.info',            
  19.             'biz.contact.choose',              //选择用户接口  
  20.             'device.notification.confirm',     //confirm,alert,prompt都是弹出小窗口的接口     
  21.             'device.notification.alert',  
  22.             'device.notification.prompt',  
  23.             'biz.util.openLink' ]  
  24.          });  
  25.   
  26.   
  27. /* 
  28. *在dd.config()验证通过的情况下,就会执行ready()函数, 
  29. *dd.ready参数为回调函数,在环境准备就绪时触发,jsapi的调用需要保证在 
  30. *该回调函数触发后调用,否则无效,所以你会发现所有对jsapi接口的调用都会在 
  31. *ready的回调函数里面 
  32. */  
  33. dd.ready(function() {  
  34.   
  35.     /* 
  36.     *获取容器信息,返回值为ability:版本号,也就是返回容器版本 
  37.     *用来表示这个版本的jsapi的能力,来决定是否使用jsapi 
  38.     */  
  39.     dd.runtime.info({  
  40.         onSuccess : function(info) {  
  41.             logger.e('runtime info: ' + JSON.stringify(info));  
  42.         },  
  43.         onFail : function(err) {  
  44.             logger.e('fail: ' + JSON.stringify(err));  
  45.         }  
  46.     });  
  47.       
  48.     /* 
  49.     *获得免登授权码,需要的参数为corpid,也就是企业的ID 
  50.     *成功调用时返回onSuccess,返回值在function的参数info中,具体操作可以在function中实现 
  51.     *返回失败时调用onFail 
  52.     */  
  53.     dd.runtime.permission.requestAuthCode({  
  54.         corpId : _config.corpId,  
  55.         onSuccess : function(info) {                                                   //成功获得code值,code值在info中  
  56. //          alert('authcode: ' + info.code);  
  57.             /* 
  58.             *$.ajax的是用来使得当前js页面和后台服务器交互的方法 
  59.             *参数url:是需要交互的后台服务器处理代码,这里的userinfo对应WEB-INF -> classes文件中的UserInfoServlet处理程序 
  60.             *参数type:指定和后台交互的方法,因为后台servlet代码中处理Get和post的doGet和doPost 
  61.             *原本需要传输的参数可以用data来存储的,格式为data:{"code":info.code,"corpid":_config.corpid} 
  62.             *其中success方法和error方法是回调函数,分别表示成功交互后和交互失败情况下处理的方法 
  63.             */  
  64.             $.ajax({  
  65.                 url : 'userinfo?code=' + info.code + '&corpid='                //userinfo为本企业应用服务器后台处理程序  
  66.                         + _config.corpId,  
  67.                 type : 'GET',  
  68.                 /* 
  69.                 *ajax中的success为请求得到相应后的回调函数,function(response,status,xhr) 
  70.                 *response为响应的数据,status为请求状态,xhr包含XMLHttpRequest对象 
  71.                 */  
  72.                 success : function(data, status, xhr) {                                  
  73.                     var info = JSON.parse(data);  
  74.   
  75.                     alert("用户"+info.name+"登录成功");  
  76.   
  77.                 },  
  78.                 error : function(xhr, errorType, error) {  
  79.                     logger.e("yinyien:" + _config.corpId);  
  80.                     alert(errorType + ', ' + error);  
  81.                 }  
  82.             });  
  83.   
  84.         },  
  85.         onFail : function(err) {                                                       //获得code值失败  
  86.             alert('fail: ' + JSON.stringify(err));  
  87.         }  
  88.     });  
  89. });  
  90.   
  91.   
  92. /* 
  93. *在dd.config函数验证没有通过下执行这个函数 
  94. */  
  95. dd.error(function(err) {  
  96.     alert('dd error: ' + JSON.stringify(err));  
  97. });  
  98.   
  99.   
  100. /* 
  101. dd中借口的约定: 
  102. 所有接口都为异步 
  103. 接受一个object类型的参数,function在js中也是一个object 
  104. 成功回调 onSuccess(某些异步接口的成功回调,将在事件触发时被调用,具体详情请查看相关onSuccess回调时机,未做描述的即为同步接口) 
  105. 失败回调 onFail 
  106.  
  107. 模板如下: 
  108. dd.命名空间.功能.方法({ 
  109.     参数1: '', 
  110.     参数2: '', 
  111.     onSuccess: function(result) { 
  112.     //成功回调 
  113.   //{ 
  114.         //所有返回信息都输出在这里 
  115.   //} 
  116.     }, 
  117.     onFail: function(){ 
  118.     //失败回调 
  119.     } 
  120. }) 
  121. */  
  122.   
  123. /**************************************结束********************************/  
我们编写的这个js文件也需要引入到企业前台的h5中,具体的引入方法和引入jsapi方式是一样的。
  1. <script type="text/javascript" src="javascripts/opt.js">  
  2. </script>  
只不过上面是从链接中引入,这里是从本地写好的资源中引入。

8.在后台编写userinfoServlet来获取前台传入的code值,并且通过code值获取用户信息,然后在后台数据库中比对用户信息实现登录。

通过code值获取用户信息的方法在钉钉官方文档中有详细的解答,通过get请求方式向
https://oapi.dingtalk.com/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
发送请求,需要携带两个参数,access_token和code值,返回值就是用户的信息。

具体代码由于和后台交互不便透露,但原理已经很清晰,对于获取用户信息可以参考前面的获取access_token的方法,
对于后台用户数据的比对只需要一些简单的数据库知识。

9.至此所有的步骤都已完成,便可以实现免登陆。


(三)运作流程阐述:

首先,公司成员在钉钉客户端点击了公司创建的微应用,然后微应用会根据定向URL地址跳转到公司应用的网站首页,在网站首页的HTML源码(也可能还包含其他前端语言)中定义了_config变量,这个变量通过后台代码的getConfig(request)函数对值进行了初始化,网站首页的源码由上往下的执行,就会执行到我们自己写的那个免登的js代码,在这个代码中完成了和_config值的验证,然后就会从钉钉开放平台获取code值,获得code值后,便会向服务器后台的相应的servlet文件发送code,该servlet文件收到code后就会换取用户信息,并和后台数据库中的用户信息比对,如果存在,就向前台返回登录成功,并跳转到登录成功后的页面。

(四):结束

最好再次强调一下,这些操作都是在你的企业应用的前端和后台进行操作的,和钉钉没有半毛钱关系,我们只是调用了钉钉的一些接口,这些文件也都是放在企业应用对应的网站的源码中,并不是放到钉钉那里。


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多