釘釘實現企業級微應用免登陸詳解
(一)基本概述:
釘釘中實現免登陸的核心思想就是通過corpId和corpSecret這兩個引數來獲得免登陸碼Code,繼而通過Code來獲取使用者資訊,並在後臺資料庫中比對該使用者資訊是否存在,如果比對成功就免登陸成功。具體實現的流程圖如下:
(二)過程詳解:
1.註冊企業使用者和建立微應用:
這個過程比較簡單,略過。2.獲取corpId,corpSecret,agentId:
可以登入釘釘企業使用者賬號直接獲得,可以儲存在本地檔案中,便於後面存取,本人儲存在本地的properties檔案中。略過。
3.獲取access_token:
在釘釘官方文件中有獲取access_token的方法介紹,通過get方式向https://oapi.dingtalk.com/gettokencorpid=id&corpsecret=secrect
定義一個get請求的方法: public class HttpHelper { /* * params: * url:需要Get請求的網址 * * return: * 返回請求時網頁相應的資料,用json儲存 */ public static JSONObject httpGet(String url){ //建立httpClient CloseableHttpClient httpClient=HttpClients.createDefault(); HttpGet httpGet=new HttpGet(url); //生成一個請求 RequestConfig requestConfig = RequestConfig.custom(). //配置請求的一些屬性 setSocketTimeout(2000).setConnectTimeout(2000).build(); httpGet.setConfig(requestConfig); //為請求設定屬性 CloseableHttpResponse response=null; try { response=httpClient.execute(httpGet); //如果返回結果的code不等於200,說明出錯了 if (response.getStatusLine().getStatusCode() != 200) { System.out.println("request url failed, http code=" + response.getStatusLine().getStatusCode()+ ", url=" + url); return null; } HttpEntity entity = response.getEntity(); //reponse返回的資料在entity中 if(entity!=null){ String resultStr=EntityUtils.toString(entity,"utf-8");//將資料轉化為string格式 JSONObject result=JSONObject.parseObject(resultStr); //將結果轉化為json格式 if(result.getInteger("errcode")==0){ //如果返回值得errcode值為0,則成功 //移除一些沒用的元素 result.remove("errcode"); result.remove("errmsg"); return result; //返回有用的資訊 } else{ //返回結果出錯了,則打印出來 System.out.println("request url=" + url + ",return value="); System.out.println(resultStr); int errCode = result.getInteger("errcode"); String errMsg = result.getString("errmsg"); throw new Exception("ErrorCode:"+errCode+"ErrorMsg"+errMsg); } } } catch (ClientProtocolException e) { // TODO Auto-generated catch block System.out.println("request url=" + url + ", exception, msg=" + e.getMessage()); e.printStackTrace(); } catch (Exception e) { // TODO Auto-generated catch block System.out.println("request url=" + url + ", exception, msg=" + e.getMessage()); e.printStackTrace(); } finally { if (response != null) try { response.close(); } catch (IOException e) { e.printStackTrace(); } } return null; }
然後是呼叫httpGet方法獲得access_token的程式碼實現: public static String getAccess_Token(String corpid,String corpsecret){ String url="https://oapi.dingtalk.com/gettoken?"+"corpid="+corpid+"&corpsecret="+corpsecret; JSONObject res=HttpHelper.httpGet(url); //將httpGet方法封裝在HttpHelper類中 String access_token=""; if(res!=null){ access_token=res.getString("access_token"); } else{ new Exception("Cannot resolve field access_token from oapi resonpse"); } return access_token; }
4.獲取ticket:
釘釘官方文件有關於ticket獲取的介紹,通過get方式向https://oapi.dingtalk.com/get_jsapi_ticket?access_token=ACCESS_TOKE請求ticket資料,請求時需要攜帶一個引數access_token,也就是在步驟3中獲得的access。具體java程式碼實現如下:
/*
* 向網頁請求ticket值,用Get方式請求網頁
* param:
* access_token:上面得到的access_token值
*
* return:
* 返回值是ticket
*/
public static String getTicket(String access_token){
String url="https://oapi.dingtalk.com/get_jsapi_ticket?"+
"access_token="+access_token;
JSONObject res=HttpHelper.httpGet(url); //步驟3中有httpGet的定義,只是封裝在HttpHelper類中
String ticket="";
if(res!=null){
ticket=res.getString("ticket");
}
else{
new Exception("Cannot resolve field ticket from oapi resonpse");
}
return ticket;
}
5.獲取簽名signatrue:
釘釘官方文件中有關於獲取簽名的介紹,並且給出了使用的演算法,引數說明。所以我們只需要呼叫它使用的演算法,並且做一些格式的調整即可。具體java程式碼實現如下:/*
* 生成簽名的函式
* params:
* ticket:簽名資料
* nonceStr:簽名用的隨機字串,從properties檔案中讀取
* timeStamp:生成簽名用的時間戳
* url:當前請求的URL地址
*/
public static String getSign(String ticket, String nonceStr, long timeStamp, String url) throws Exception {
String plain = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "×tamp=" + String.valueOf(timeStamp)
+ "&url=" + url;
try {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); //安全hash演算法
sha1.reset();
sha1.update(plain.getBytes("UTF-8")); //根據引數產生hash值
return bytesToHex(sha1.digest());
} catch (NoSuchAlgorithmException e) {
throw new Exception(e.getMessage());
} catch (UnsupportedEncodingException e) {
throw new Exception(e.getMessage());
}
}
//將bytes型別的資料轉化為16進位制型別
private static String bytesToHex(byte[] hash) { //將字串轉化為16進位制的資料
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
6.封裝好所有需要的引數,並且傳遞到企業應用網址的前端H5中。
需要的引數有corpId,agentId,ticket,signature,nonceStr,timeStamp,url。其中nonceStr,timeStamp,url用來在伺服器後臺生成signatrue簽名,然後將ticket,nonceStr,timeStamp和signatrue傳送到前臺,前臺網頁就會呼叫jsapi的dd.config函式重新生成signatrue,和傳進的signatrue進行比較,來實現驗證過程。java實現如下:
6.封裝好所有需要的引數,並且傳遞到企業應用網址的前端H5中。需要的引數有corpId,agentId,ticket,signature,nonceStr,timeStamp,url.
其中nonceStr,timeStamp,url用來在伺服器後臺生成signatrue簽名,然後將ticket,nonceStr,timeStamp和signatrue傳送到前臺,前臺網頁就會
呼叫jsapi的dd.config函式重新生成signatrue,和傳進的signatrue進行比較,來實現驗證過程。
/*
* 將所有需要傳送到前端的引數進行打包,在前端會呼叫jsapi提供的dd.config介面進行簽名的驗證
*params:
* request:在釘釘中點選微應用圖示跳轉的url地址
*return:
* 將需要的引數打包好,按json格式打包
*/
public static String getConfig(HttpServletRequest request){
/*
*以http://localhost/test.do?a=b&c=d為例
*request.getRequestURL的結果是http://localhost/test.do
*request.getQueryString的返回值是a=b&c=d
*/
String urlString = request.getRequestURL().toString();
String queryString = request.getQueryString();
String url=null;
if(queryString!=null){
url=urlString+queryString;
}
else{
url=urlString;
}
String corpId=PropertiesHelp.getValue("corpid"); //一些比較重要的不變得引數本人儲存在properties檔案中
String corpSecret=PropertiesHelp.getValue("corpsecret");
String nonceStr=PropertiesHelp.getValue("noncestr");
String agentId =PropertiesHelp.getValue("agentid"); //agentid引數
long timeStamp = System.currentTimeMillis() / 1000; //時間戳引數
String signedUrl = url; //請求連結的引數,這個連結主要用來生成signatrue,並不需要傳到前端
String accessToken = null; //token引數
String ticket = null; //ticket引數
String signature = null; //簽名引數
try {
accessToken=getAccess_Token(corpId,corpSecret);
ticket=getTicket(accessToken);
signature=getSign(ticket,nonceStr,timeStamp,signedUrl);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return "{jsticket:'" + ticket + "',signature:'" + signature + "',nonceStr:'" + nonceStr + "',timeStamp:'"
+ timeStamp + "',corpId:'" + corpId + "',agentId:'" + agentId+ "'}";
}
7.前臺對傳進來的引數進行驗證,並且生成code值,並且將code值傳送給後臺伺服器程式。
驗證過程需要呼叫jsapi的一些藉口,所以我們要在前臺網頁中引入相應的js檔案,引入的方法就是直接在前臺網頁中包含jsapi的js檔案,引入程式碼如下:<script type="text/javascript" src="http://g.alicdn.com/ilw/ding/0.7.3/scripts/dingtalk.js">
</script>
引入了jsapi之後,我們就需要自己編寫js檔案對它的相應的介面進行呼叫,從而獲得code,如下的程式碼是本人根據實際應用編寫的js程式碼,具有詳細的註解:
/***************************開始****************************/
/**
* _config 這個引數是在前臺的H5檔案中我定義的,它的值是通過呼叫步驟6中封裝好的引數來獲得的
*/
/*
我們需要明白的一點是,所有的這些檔案都是放在企業應用的伺服器後臺,和釘釘網站沒有半毛錢的關係
並且釘釘的jsapi中唯一的作用就是提供了對config的驗證和獲得code值
對於其他值得獲取,如access_token,ticket,sign,username,userid都是自己在後臺寫java程式碼通過get或者post方式向
釘釘開發平臺請求得來的,並不是從jsapi中的介面得來的
*/
dd.config({ //dd.config方法會對引數進行驗證
agentId : _config.agentid,
corpId : _config.corpId,
timeStamp : _config.timeStamp,
nonceStr : _config.nonceStr,
signature : _config.signature,
jsApiList : [ //需要呼叫的藉口列表
'runtime.info',
'biz.contact.choose', //選擇使用者介面
'device.notification.confirm', //confirm,alert,prompt都是彈出小視窗的介面
'device.notification.alert',
'device.notification.prompt',
'biz.util.openLink' ]
});
/*
*在dd.config()驗證通過的情況下,就會執行ready()函式,
*dd.ready引數為回撥函式,在環境準備就緒時觸發,jsapi的呼叫需要保證在
*該回調函式觸發後呼叫,否則無效,所以你會發現所有對jsapi介面的呼叫都會在
*ready的回撥函式裡面
*/
dd.ready(function() {
/*
*獲取容器資訊,返回值為ability:版本號,也就是返回容器版本
*用來表示這個版本的jsapi的能力,來決定是否使用jsapi
*/
dd.runtime.info({
onSuccess : function(info) {
logger.e('runtime info: ' + JSON.stringify(info));
},
onFail : function(err) {
logger.e('fail: ' + JSON.stringify(err));
}
});
/*
*獲得免登授權碼,需要的引數為corpid,也就是企業的ID
*成功呼叫時返回onSuccess,返回值在function的引數info中,具體操作可以在function中實現
*返回失敗時呼叫onFail
*/
dd.runtime.permission.requestAuthCode({
corpId : _config.corpId,
onSuccess : function(info) { //成功獲得code值,code值在info中
// alert('authcode: ' + info.code);
/*
*$.ajax的是用來使得當前js頁面和後臺伺服器互動的方法
*引數url:是需要互動的後臺伺服器處理程式碼,這裡的userinfo對應WEB-INF -> classes檔案中的UserInfoServlet處理程式
*引數type:指定和後臺互動的方法,因為後臺servlet程式碼中處理Get和post的doGet和doPost
*原本需要傳輸的引數可以用data來儲存的,格式為data:{"code":info.code,"corpid":_config.corpid}
*其中success方法和error方法是回撥函式,分別表示成功互動後和互動失敗情況下處理的方法
*/
$.ajax({
url : 'userinfo?code=' + info.code + '&corpid=' //userinfo為本企業應用伺服器後臺處理程式
+ _config.corpId,
type : 'GET',
/*
*ajax中的success為請求得到相應後的回撥函式,function(response,status,xhr)
*response為響應的資料,status為請求狀態,xhr包含XMLHttpRequest物件
*/
success : function(data, status, xhr) {
var info = JSON.parse(data);
alert("使用者"+info.name+"登入成功");
},
error : function(xhr, errorType, error) {
logger.e("yinyien:" + _config.corpId);
alert(errorType + ', ' + error);
}
});
},
onFail : function(err) { //獲得code值失敗
alert('fail: ' + JSON.stringify(err));
}
});
});
/*
*在dd.config函式驗證沒有通過下執行這個函式
*/
dd.error(function(err) {
alert('dd error: ' + JSON.stringify(err));
});
/*
dd中藉口的約定:
所有介面都為非同步
接受一個object型別的引數,function在js中也是一個object
成功回撥 onSuccess(某些非同步介面的成功回撥,將在事件觸發時被呼叫,具體詳情請檢視相關onSuccess回撥時機,未做描述的即為同步介面)
失敗回撥 onFail
模板如下:
dd.名稱空間.功能.方法({
引數1: '',
引數2: '',
onSuccess: function(result) {
//成功回撥
//{
//所有返回資訊都輸出在這裡
//}
},
onFail: function(){
//失敗回撥
}
})
*/
/**************************************結束********************************/
我們編寫的這個js檔案也需要引入到企業前臺的h5中,具體的引入方法和引入jsapi方式是一樣的。<script type="text/javascript" src="javascripts/opt.js">
</script>
只不過上面是從連結中引入,這裡是從本地寫好的資源中引入。8.在後臺編寫userinfoServlet來獲取前臺傳入的code值,並且通過code值獲取使用者資訊,然後在後臺數據庫中比對使用者資訊實現登入。
通過code值獲取使用者資訊的方法在釘釘官方文件中有詳細的解答,通過get請求方式向
https://oapi.dingtalk.com/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
傳送請求,需要攜帶兩個引數,access_token和code值,返回值就是使用者的資訊。
具體程式碼由於和後臺互動不便透露,但原理已經很清晰,對於獲取使用者資訊可以參考前面的獲取access_token的方法,
對於後臺使用者資料的比對只需要一些簡單的資料庫知識。