1. 程式人生 > >釘釘實現企業級微應用免登陸詳解

釘釘實現企業級微應用免登陸詳解

(一)基本概述:

釘釘中實現免登陸的核心思想就是通過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實現如下:

定義一個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的方法,
對於後臺使用者資料的比對只需要一些簡單的資料庫知識。

9.至此所有的步驟都已完成,便可以實現免登陸。

(三)運作流程闡述:

首先,公司成員在釘釘客戶端點選了公司建立的微應用,然後微應用會根據定向URL地址跳轉到公司應用的網站首頁,在網站首頁的HTML原始碼(也可能還包含其他前端語言)中定義了_config變數,這個變數通過後臺程式碼的getConfig(request)函式對值進行了初始化,網站首頁的原始碼由上往下的執行,就會執行到我們自己寫的那個免登的js程式碼,在這個程式碼中完成了和_config值的驗證,然後就會從釘釘開放平臺獲取code值,獲得code值後,便會向伺服器後臺的相應的servlet檔案傳送code,該servlet檔案收到code後就會換取使用者資訊,並和後臺資料庫中的使用者資訊比對,如果存在,就向前臺返回登入成功,並跳轉到登入成功後的頁面。

(四):結束

最好再次強調一下,這些操作都是在你的企業應用的前端和後臺進行操作的,和釘釘沒有半毛錢關係,我們只是呼叫了釘釘的一些介面,這些檔案也都是放在企業應用對應的網站的原始碼中,並不是放到釘釘那裡。