Java钉钉开发_02_免登授权(身份验证)(附源码)
Posted shirayner
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java钉钉开发_02_免登授权(身份验证)(附源码)相关的知识,希望对你有一定的参考价值。
源码已上传GitHub: https://github.com/shirayner/DingTalk_Demo
一、本节要点
1.免登授权的流程
(1)签名校验
(2)获取code,并传到后台
(3)根据code获取userid
(4)根据userid获取用户信息,(此处可进行相应业务处理)
(5)将用户信息传到前端,前端拿到用户信息,并做相应处理
2.计算签名信息(signature)
2.1 待签名参数
ticket | jsapi_ticket |
nonceStr | 随机字符串,随机生成 |
timeStamp | 时间戳 |
url | 当前网页的URL,不包含#及其后面部分 |
2.2签名流程
(1)字典序
将所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式 (即 key1=value1&key2=value2…)拼接成字符串string1
如:String string1= "jsapi_ticket=" + jsTicket + "&noncestr=" + nonceStr + "×tamp=" + timeStamp + "&url=" + url;
(2)SHA-1签名,得到 signature
/** * @desc : 3.生成签名的函数 * * @param ticket jsticket * @param nonceStr 随机串,自己定义 * @param timeStamp 生成签名用的时间戳 * @param url 需要进行免登鉴权的页面地址,也就是执行dd.config的页面地址 * @return * @throws Exception String */ public static String getSign(String jsTicket, String nonceStr, Long timeStamp, String url) throws Exception { String plainTex = "jsapi_ticket=" + jsTicket + "&noncestr=" + nonceStr + "×tamp=" + timeStamp + "&url=" + url; System.out.println(plainTex); try { MessageDigest crypt = MessageDigest.getInstance("SHA-1"); crypt.reset(); crypt.update(plainTex.getBytes("UTF-8")); return byteToHex(crypt.digest()); } catch (NoSuchAlgorithmException e) { throw new Exception(e.getMessage()); } catch (UnsupportedEncodingException e) { throw new Exception(e.getMessage()); } } //将bytes类型的数据转化为16进制类型 private static String byteToHex(byte[] hash) { Formatter formatter = new Formatter(); for (byte b : hash) { formatter.format("%02x", new Object[] { Byte.valueOf(b) }); } String result = formatter.toString(); formatter.close(); return result; }
3.签名校验的流程
3.1 后端准备好前端校验参数
后台方法:getConfig(HttpServletRequest)
public static String getConfig(HttpServletRequest request){ //1.准备好参与签名的字段 /* *以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 queryStringEncode = null; String url; if (queryString != null) { queryStringEncode = URLDecoder.decode(queryString); url = urlString + "?" + queryStringEncode; } else { url = urlString; } String nonceStr=UUID.randomUUID().toString(); //随机数 long timeStamp = System.currentTimeMillis() / 1000; //时间戳参数 String signedUrl = url; String accessToken = null; String ticket = null; String signature = null; //签名 //2.进行签名,获取signature try { accessToken=AuthHelper.getAccessToken(Env.CORP_ID, Env.CORP_SECRET); ticket=AuthHelper.getJsapiTicket(accessToken); signature=getSign(ticket,nonceStr,timeStamp,signedUrl); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("accessToken:"+accessToken); System.out.println("ticket:"+ticket); System.out.println("nonceStr:"+nonceStr); System.out.println("timeStamp:"+timeStamp); System.out.println("signedUrl:"+signedUrl); System.out.println("signature:"+signature); System.out.println("agentId:"+Env.AGENTID); System.out.println("corpId:"+Env.CORP_ID); String configValue = "{jsticket:\'" + ticket + "\',signature:\'" + signature + "\',nonceStr:\'" + nonceStr + "\',timeStamp:\'" + timeStamp + "\',corpId:\'" + Env.CORP_ID + "\',agentId:\'" + Env.AGENTID + "\'}"; System.out.println(configValue); return configValue; }
3.2 前端接收后台参数
在前端调用后端方法,获取dd.config所需的校验参数:‘url’,‘nonceStr’,‘agentId’,‘timeStamp’,‘corpId’,‘signature’。
<script type="text/javascript"> var _config =<%=com.ray.dingtalk.auth.AuthHelper.getConfig(request)%>; </script>
3.3 执行前端 dd.config ,进行签名校验
dd.config 用接收到的 nonceStr、agentId、timeStamp、corpId这四个参数去钉钉官方后端计算出一个签名(signature ), 并将这个签名与我们后端所计算的signature来进行比对,若一致,则校验通过。若不一致,则是我们后端计算签名的时候出错了。此时可根据错误消息提示去进行调试。
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\', \'device.notification.alert\', //confirm,alert,prompt都是弹出小窗口的接口 \'device.notification.prompt\', \'biz.ding.post\', \'biz.util.openLink\' ] });
3.4 异常:js加载顺序有误所引起的 前端什么信息都不提示
出现这个原因,可能是自己js出错了。我的原因是js加载顺序有误。
请注意这几个js的加载顺序: _config,jquery-3.2.1.min.js 必须在auth.js之前加载
<script src="js/jquery-3.2.1.min.js"></script> <script type="text/javascript" src="http://g.alicdn.com/dingding/open-develop/1.6.9/dingtalk.js"></script> <script type="text/javascript"> var _config =<%=com.ray.dingtalk.auth.AuthHelper.getConfig(request)%>; </script> <script type="text/javascript" src="js/auth.js"></script>
4. 将code送往后端:ajax
签名校验成功之后,即dd.config校验成功之后,会执行dd.ready函数,这时我们就可以使用钉钉的jsapi了。
签名校验成功后,我们就可以调用获取免登授权码(CODE)的jsapi,来获取code,然后通过ajax方式将这个code传到后台userInfoServlet
/**获取免登授权码 CODE * */ dd.runtime.permission.requestAuthCode({ corpId : _config.corpId, onSuccess : function(info) { //成功获得code值,code值在info中 alert(\'authcode: \' + info.code); /* *$.ajax的是用来使得当前js页面和后台服务器交互的方法 *参数url:是需要交互的后台服务器处理代码,userInfoServlet *参数type:指定和后台交互的方法,因为后台servlet代码中处理Get和post的doGet和doPost *data:负责传递请求参数 *其中success方法和error方法是回调函数,分别表示成功交互后和交互失败情况下处理的方法 */ $.ajax({ type : "POST", url : "http://p65s3p.natappfree.cc/DingTalk_Demo/userInfoServlet", data : { code : info.code }, success : function(data, status, xhr) { alert(data); var userInfo = JSON.parse(data); document.getElementById("userName").innerhtml = userInfo.name; document.getElementById("userId").innerHTML = userInfo.userid; // 图片 if(info.avatar.length != 0){ var img = document.getElementById("userImg"); img.src = info.avatar; img.height = \'200\'; img.width = \'200\'; } }, error : function(xhr, errorType, error) { logger.e("yinyien:" + _config.corpId); alert(errorType + \', \' + error); } }); }, onFail : function(err) { //获得code值失败 alert(\'fail: \' + JSON.stringify(err)); } });
5.根据code获取userid
private static final String GET_USERINFO_BYCODE_URL="https://oapi.dingtalk.com/user/getuserinfo?access_token=ACCESSTOKEN&code=CODE"; /** 5.根据免登授权码Code查询免登用户userId * @desc :钉钉服务器返回的用户信息为: * userid 员工在企业内的UserID * deviceId 手机设备号,由钉钉在安装时随机产生 * is_sys 是否是管理员 * sys_level 级别,0:非管理员 1:超级管理员(主管理员) 2:普通管理员(子管理员) 100:老板 * * @param accessToken * @param code * @throws Exception void */ public JSONObject getUserInfo(String accessToken,String code) throws Exception { //1.获取请求url String url=GET_USERINFO_BYCODE_URL.replace("ACCESSTOKEN", accessToken).replace("CODE", code); //2.发起GET请求,获取返回结果 JSONObject jsonObject=HttpHelper.httpGet(url); System.out.println("jsonObject:"+jsonObject.toString()); //3.解析结果,获取User if (null != jsonObject) { //4.请求成功,则返回jsonObject if (0==jsonObject.getInteger("errcode")) { return jsonObject; } //5.错误消息处理 if (0 != jsonObject.getInteger("errcode")) { int errCode = jsonObject.getInteger("errcode"); String errMsg = jsonObject.getString("errmsg"); throw new Exception("error code:"+errCode+", error message:"+errMsg); } } return null; }
6.根据userid获取用户信息
private static final String GET_USER_URL="https://oapi.dingtalk.com/user/get?access_token=ACCESSTOKEN&userid=USERID"; /** 2.根据userid获取成员详情 * @desc :获取成员详情 * 参考文档: https://open-doc.dingtalk.com/docs/doc.htm?spm=0.0.0.0.jjSfQQ&treeId=371&articleId=106816&docType=1#s0 * @param accessToken * @param userId void * @throws Exception */ public JSONObject getUser(String accessToken, String userId) throws Exception { //1.获取请求url String url=GET_USER_URL.replace("ACCESSTOKEN", accessToken).replace("USERID", userId); //2.发起GET请求,获取返回结果 JSONObject jsonObject=HttpHelper.httpGet(url); System.out.println("jsonObject:"+jsonObject.toString()); //3.解析结果,获取User if (null != jsonObject) { //4.请求成功,则返回jsonObject if (0==jsonObject.getInteger("errcode")) { return jsonObject; } //5.错误消息处理 if (0 != jsonObject.getInteger("errcode")) { int errCode = jsonObject.getInteger("errcode"); String errMsg = jsonObject.getString("errmsg"); throw new Exception("error code:"+errCode+", error message:"+errMsg); } } return null; }
7.将用户信息传到前端
注意:传输格式为json
//3.通过userid换取用户信息 JSONObject jsonObject=us.getUser(accessToken, userId); result=JSON.toJSON(jsonObject); PrintWriter out = response.getWriter(); out.print(result); out.close(); out = null;
8.前端接收用户信息后做相应处理
jsp中代码:
<div align="center"> <img id="userImg" alt="头像" src=""> </div> <div align="center"> <span>UserName:</span> <div id="userName" style="display: inline-block"></div> </div> <div align="center"> <span>UserId:</span> <div id="userId" style="display: inline-block"></div> </div>
js中代码:发送code的ajax调用成功后
success : function(data, status, xhr) { alert(data); //接收后端发送过来的用户信息 var userInfo = JSON.parse(data); //收到用户信息后所做的处理 document.getElementById("userName").innerHTML = userInfo.name; document.getElementById("userId").innerHTML = userInfo.userid; // 图片 if(info.avatar.length != 0){ var img = document.getElementById("userImg"); img.src = info.avatar; img.height = \'200\'; img.width = \'200\'; } },
二、代码实现
1.钉钉参数配置——Env.java
将Env.java中的配置修改成你自己的
package com.ray.dingtalk.config; /**@desc : 企业应用接入时的常量定义 * * @author: shirayner * @date : 2017年9月27日 下午4:57:36 */ public class Env { /** * 企业应用接入秘钥相关 */ public static final String CORP_ID = "ding6d4828968696691535c2f4657eb6378f"; public static final String CORP_SECRET = "ZigmkCY4VcsGUhLIzmfxOmP0ElJbGI5uBhn-2mPelovnjPcA6e4LrjpYXQQw89Q4"; public static final String SSO_Secret = "YgIGtCHmcwAmOuKsAo_lgqJJiOwyez2G6vBvhCf1zwR6kZ5DGMJsxOcUgK5p1C"; public static final String AGENTID = "128838526"; /** * DING API地址 */ public static final String OAPI_HOST = "https://oapi.dingtalk.com"; /** * 企业应用后台地址,用户管理后台免登使用 */ public static final String OA_BACKGROUND_URL = ""; /** * 企业通讯回调加密Token,注册事件回调接口时需要传递给钉钉服务器 */ public static final String TOKEN = ""; public static final String ENCODING_AES_KEY = ""; }
2.Http请求工具类——HttpHelper.java
主要包括发送GET请求和POST请求
package com.ray.dingtalk.util; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Formatter; import javax.servlet.http.HttpServletRequest; import org.apache.http.HttpEntity; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.util.EntityUtils; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.ray.dingtalk.auth.AuthHelper; import com.ray.dingtalk.config.Env; /** * HTTP请求封装,建议直接使用sdk的API */ public class HttpHelper { /** * @desc :1.发起GET请求 * * @param url * @return JSONObject * @throws Exception */ public static JSONObject httpGet(String url) throws Exception { //1.创建httpClient CloseableHttpClient httpClient = HttpClients.createDefault(); //2.生成一个请求 HttpGet httpGet = new HttpGet(url); //3.配置请求的属性 RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(2000).setConnectTimeout(2000).build(); httpGet.setConfig(requestConfig); //4.发起请求,获取响应信息 CloseableHttpResponse response = null; try { response = httpClient.execute(httpGet, new BasicHttpContext()); //如果返回结果的code不等于200,说明出错了 if (response.getStatusLine().getStatusCode() != 200) { System.out.println("request url failed, http code=" + response.getStatusLine().getStatusCode() + ", url=" + url); return null; } //5.解析请求结果 HttpEntity entity = response.getEntity(); //reponse返回的数据在entity中 if (entity != null) { String resultStr = EntityUtils.toString(entity, "utf-8"); //将数据转化为string格式 JSONObject result = JSON.parseObject(resultStr); //将String转换为 JSONObject if (result.getInteger("errcode") == 0) { 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("error code:"+errCode+", error message:"+errMsg); } } } catch (IOException e) { 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; } /** 2.发起POST请求 * @desc : * * @param url * @param data * @return * @throws Exception JSONObject */ public static JSONObject httpPost(String url, Object data) throws Exception { HttpPost httpPost = new HttpPost(url); CloseableHttpResponse response = null; CloseableHttpClient httpClient = HttpClients.createDefault(); RequestConfig requestConfig = RequestConfig.custom(). setSocketTimeout(2000).setConnectTimeout(2000).build(); httpPost.setConfig(requestConfig); httpPost.addHeader("Content-Type", "application/json"); try { StringEntity requestEntity = new StringEntity(JSON.toJSONString(data), "utf-8"); httpPost.setEntity(requestEntity); response = httpClient.execute(httpPost, new BasicHttpContext()); if (response.getStatusLine().getStatusCode() != 200) { System.out.println("request url failed, http code=" + response.getStatusLine().getStatusCode() + ", url=" + url); return null; } HttpEntity entity = response.getEntity(); if (entity != null) { String resultStr = EntityUtils.toString(entity, "utf-8"); JSONObject result = JSON.parseObject(resultStr); if (result.getInteger("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("error code:"+errCode+", error message:"+errMsg); } } } catch (IOException e) { 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; } }
3.钉钉相关接口权限的获取工具类——AuthHelper.java
主要包括:AccessToken、JsapiTicket、以及签名校验的工具类
package com.ray.dingtalk.auth; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Formatter; import java.util.HashMap; import java.util.Map; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.alibaba.fastjson.JSONObject; import com.ray.dingtalk.config.Env; import com.ray.dingtalk.util.HttpHelper; /** * 钉钉相关配置参数的获取工具类 * @desc : AccessToken和jsticket的获取封装 * * @author: shirayner * @date : 2017年9月27日 下午5:00:25 */ public class AuthHelper { //private static Logger log = LoggerFactory.getLogger(AuthHelper.class); //获取access_token的接口地址,有效期为7200秒 private static final String GET_ACCESSTOKEN_URL="https://oapi.dingtalk.com/gettoken?corpid=CORPID&corpsecret=CORPSECRET"; //获取getJsapiTicket的接口地址,有效期为7200秒 private static final String GET_JSAPITICKET_URL="https://oapi.dingtalk.com/get_jsapi_ticket?access_token=ACCESSTOKE"; /** 1.获取access_token * @desc : * * @param corpId * @param corpSecret * @return * @throws Exception String */ public static String getAccessToken(String corpId,String corpSecret) throws Exception { //1.获取请求url String url=GET_ACCESSTOKEN_URL.replace("CORPID", corpId).replace("CORPSECRET", corpSecret); //2.发起GET请求,获取返回结果 JSONObject jsonObject=HttpHelper.httpGet(url); //3.解析结果,获取accessToken String accessToken=""; if (null != jsonObject) { accessToken=jsonObject.getString("access_token"); //4.错误消息处理 if (0 != jsonObject.getInteger("errcode")) { int errCode = jsonObject.getInteger("errcode"); String errMsg = jsonObject.getString("errmsg"); throw new Exception("error code:"+errCode+", error message:"+errMsg); } } return accessToken; } /** * 2、获取JSTicket, 用于js的签名计算 * 正常的情况下,jsapi_ticket的有效期为7200秒,所以开发者需要在某个地方设计一个定时器,定期去更新jsapi_ticket * @throws Exception */ public static String getJsapiTicket(String accessToken) throws Exception { //1.获取请求url String url=GET_JSAPITICKET_URL.replace("ACCESSTOKE", accessToken); //2.发起GET请求,获取返回结果 JSONObject jsonObject=HttpHelper.httpGet(url); //3.解析结果,获取ticket String ticket=""; if (null != jsonObject) { ticket=jsonObject.getString("ticket"); //4.错误消息处理 if (0 != jsonObject.getInteger("errcode")) { int errCode = jsonObject.getInteger("errcode"); String errMsg = jsonObject.getString("errmsg"); throw new Exception("error code:"+errCode+", error message:"+errMsg); } } return ticket; } /** * @desc : 3.生成签名的函数 * * @param ticket jsticket * @param nonceStr 随机串,自己定义 * @param timeStamp 生成签名用的时间戳 * @param url 需要进行免登鉴权的页面地址,也就是执行dd.config的页面地址 * @return * @throws Exception String */ public static String getSign(String jsTicket, String nonceStr, Long timeStamp, String url) throws Exception { String plainTex = "jsapi_ticket=" + jsTicket + "&noncestr=" + nonceStr + "×tamp=" + timeStamp + "&url=" + url; System.out.println(plainTex); try { MessageDigest crypt = MessageDigest.getInstance("SHA-1"); crypt.reset(); crypt.update(plainTex.getBytes("UTF-8")); return byteToHex(crypt.digest()); } catch (NoSuchAlgorithmException e) { throw new Exception(e.getMessage()); } catch (UnsupportedEncodingException e) { throw new Exception(e.getMessage()); } } //将bytes类型的数据转化为16进制类型 private 钉钉开发入门,微应用识别用户身份,获取用户免登授权码code,获取用户userid,获取用户详细信息Java企业微信开发_09_身份验证之移动端网页授权(有完整项目源码)
Java钉钉开发_异常_01_error code:50002, error message:请求的员工userid不在授权范围内