一、背景描述
公司做一个项目要集成到钉钉上,但是呢公司里又没有人做过,所以需要自己研究了。有了需求,就要开始行动。
首先当然是要去钉钉开放平台查看相关资料,钉钉开放平台地址:应用类型介绍 - 钉钉开放平台 。
二、准备工作
第一、你需要有一个 自己的 企业钉钉,进入企业钉钉管理,如下图所示
第二、看创建H5微应用的教程,链接地址:开发一个钉钉H5微应用 - 钉钉开放平台 ,这个教程上面有很详细的创建微应用的步骤以及注意事项。
三、查看微应用配置信息
以下的AgentId、AppKey、AppSecret会用到,所以找个地方保存一下,如果你是管理员那就更好了,随时都可以登录钉钉组织查看哦。
3.1 基础信息
3.2 开发管理
重点来喽:服务器出口IP,自己刚开始配置的时候不清楚到底是什么意思,也不懂服务器出口IP是干什么用的,虽然官网教程上写的有哈。其实呢,这个服务器出口IP(1、本地测试时:填写你 电脑的IP + 端口 + 映射路径;2、测试环境或者生产环境时:你的测试或者生产 服务器IP + 端口 + 映射路径或者是域名 + 端口号 + 映射路径)就是应用所在的电脑IP,可以这样简单理解哈。
3.3 权限管理
注意:如果想要获取钉钉用户的手机号,需要开通 企业员工手机号信息 这一个权限哦。
一般只需要开启以下几个权限即可,其他权限可根据需要自行开通哦。
3.4 版本管理与发布
只有点击确认发布后,才可以在钉钉工作台看到微应用。
四、核心代码
引入POM依赖:
<!-- 钉钉SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>1.0.1</version>
</dependency>
接下来就是核心代码实现喽,可能会有点长哈
4.1 Controller层
package com.iot.daily.dingding.web; import cn.hutool.core.bean.BeanUtil; import com.alibaba.fastjson.JSONObject; import com.dingtalk.api.DefaultDingTalkClient; import com.dingtalk.api.DingTalkClient; import com.dingtalk.api.request.OapiUserGetRequest; import com.dingtalk.api.request.OapiUserGetuserinfoRequest; import com.dingtalk.api.response.OapiUserGetResponse; import com.dingtalk.api.response.OapiUserGetuserinfoResponse; import com.taobao.api.ApiException; import com.iot.daily.common.domain.vo.JsonResult; import com.iot.daily.common.util.CommonUtil; import com.iot.daily.dingding.config.DingAppConfig; import com.iot.daily.dingding.config.DingUrlConstant; import com.iot.daily.dingding.domain.ConfigDTO; import com.iot.daily.dingding.domain.ServiceResult; import com.iot.daily.dingding.domain.UserDTO; import com.iot.daily.dingding.exception.DingtalkEncryptException; import com.iot.daily.dingding.service.DingAuthService; import com.iot.daily.dingding.service.TokenService; import com.iot.daily.dingding.util.JsApiSignature; import com.iot.daily.module.entity.DailyViewUser; import com.iot.daily.organization.entity.User; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.ModelAndView; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.Map; import java.util.Optional; /** * <p>DingLoginController 此类用于:钉钉企业内部应用免登(H5微应用)</p> * <p>@author:hujm</p> * <p>@date:2021年05月18日 15:06</p> * <p>@remark:钉钉企业内部微应用DEMO, 实现了身份验证(免登)功能</p> */ @Api(value = "dingAuthController", tags = "钉钉企业内部应用免登(H5微应用)") @Slf4j @Controller @RequestMapping(value = "/ding") public class DingAuthController { @Resource private TokenService tokenService; @Resource private DingAppConfig dingAppConfig; @Resource private DingAuthService dingAuthService; /** * 欢迎页面,通过 /welcome 访问,判断后端服务是否启动 * * @return 字符串 welcome */ @ApiOperation(value = "日报跳转的首页地址") @RequestMapping("/login") public String mobileLogin() { return "mobile/dinglogin"; } /** * 欢迎页面,通过 /welcome 访问,判断后端服务是否启动 * * @return 字符串 welcome */ @ApiOperation(value = "日报跳转的首页地址") @RequestMapping("/index") public String mobileIndex() { return "mobile/index"; } /** * 钉钉跳转到页面 * * @param request 请求 * @param response 响应 * @return ModelAndView 页面 */ @RequestMapping("/toIndex") public ModelAndView toDingView(HttpServletRequest request, HttpServletResponse response) { String view = request.getParameter("view"); Map<String, Object> pMap = CommonUtil.getParameterMap(request); return new ModelAndView(view, pMap); } /** * 钉钉用户登录,显示当前登录用户的userId和名称 * * @param authCode 免登临时authCode * @return 当前用户 */ @ApiOperation(value = "钉钉用户登录,显示当前登录用户的userId和名称") @PostMapping("/avoidLogin") @ResponseBody public JsonResult login(@RequestBody String authCode, HttpServletRequest request) { String accessToken; // 获取accessToken ServiceResult<String> accessTokenSr = tokenService.getAccessToken(); if (!accessTokenSr.isSuccess()) { return JsonResult.fail(Integer.parseInt(accessTokenSr.getCode()), accessTokenSr.getMessage()); } accessToken = accessTokenSr.getResult(); JSONObject jsonObject = JSONObject.parseObject(authCode); String getAuthCode = (String) jsonObject.get("authCode"); // 获取用户userId ServiceResult<String> userIdSr = getUserInfo(accessToken, getAuthCode); if (!userIdSr.isSuccess()) { return JsonResult.fail(Integer.parseInt(userIdSr.getCode()), userIdSr.getMessage()); } // 获取用户详情 JsonResult userInfo = this.getUser(accessToken, userIdSr.getResult()); UserDTO userDTO = (UserDTO) userInfo.getResult(); log.info("根据accessToken和用户userId查询出的用户信息 userDTO = {}", userDTO); HttpSession session = request.getSession(); User user = this.getUserFromUserDTO(userDTO); session.setAttribute("date", String.valueOf(System.currentTimeMillis())); session.setAttribute("user", user); return userInfo; } /** * 封装User对象 * * @param userDTO userDTO对象 * @return User对象 */ private User getUserFromUserDTO(UserDTO userDTO) { User user = new User(); user.setUserName(userDTO.getUserName()); user.setTrueName(userDTO.getTrueName()); user.setUserId(userDTO.getUserId()); user.setAgentSid(userDTO.getAgentSid()); user.setCreateDate(userDTO.getCreateDate()); user.setDingUserId(userDTO.getDingUserId()); user.setDepartment(userDTO.getAgentName()); user.setFid(userDTO.getFid()); user.setHandset(userDTO.getHandset()); user.setHeadPortrait(userDTO.getHeadPortrait()); user.setIsAdmin(userDTO.getIsAdmin()); user.setIsAlarm(userDTO.getIsAlarm()); user.setIsLeader(userDTO.getIsLeader()); user.setLoginCount(userDTO.getLoginCount()); user.setLoginLastDate(userDTO.getLoginLastDate()); user.setLoginLastIp(userDTO.getLoginLastIp()); user.setOrgEmail(userDTO.getOrgEmail()); user.setPosition(userDTO.getPosition()); user.setPassword(userDTO.getPassword()); user.setPwdLastDate(userDTO.getPwdLastDate()); user.setRoleId(userDTO.getRoleId()); user.setUserLevel(userDTO.getUserLevel()); user.setUserState(userDTO.getUserState()); return user; } /** * 访问/user/getuserinfo接口获取用户userId * * @param accessToken access_token * @param authCode 临时授权码 * @return 用户userId或错误信息 */ private ServiceResult<String> getUserInfo(String accessToken, String authCode) { DingTalkClient client = new DefaultDingTalkClient(DingUrlConstant.URL_GET_USER_INFO); OapiUserGetuserinfoRequest request = new OapiUserGetuserinfoRequest(); request.setCode(authCode); request.setHttpMethod("GET"); OapiUserGetuserinfoResponse response; try { response = client.execute(request, accessToken); } catch (ApiException e) { log.error("Failed to {}", DingUrlConstant.URL_GET_USER_INFO, e); return ServiceResult.failure(e.getErrCode(), "Failed to getUserInfo: " + e.getErrMsg()); } if (!response.isSuccess()) { return ServiceResult.failure(response.getErrorCode(), response.getErrmsg()); } return ServiceResult.success(response.getUserid()); } /** * 访问/user/get 获取用户名称 * * @param accessToken access_token * @param userId 用户userId * @return 用户名称或错误信息 */ private JsonResult getUser(String accessToken, String userId) { DingTalkClient client = new DefaultDingTalkClient(DingUrlConstant.URL_USER_GET); OapiUserGetRequest request = new OapiUserGetRequest(); request.setUserid(userId); request.setHttpMethod("GET"); OapiUserGetResponse response; try { response = client.execute(request, accessToken); } catch (ApiException e) { log.error("Failed to {}", DingUrlConstant.URL_USER_GET, e); return JsonResult.fail(Integer.parseInt(e.getErrCode()), "Failed to getUserName: " + e.getErrMsg()); } UserDTO user = this.assembleUserDTO(response, accessToken); return JsonResult.ok(user); } /** * 封装返回的用户信息 * * @param response response * @param accessToken accessToken * @return 用户信息 */ private UserDTO assembleUserDTO(OapiUserGetResponse response, String accessToken) { UserDTO user = new UserDTO(); String userid = response.getUserid(); String mobile = response.getMobile(); try { DailyViewUser dailyViewUser = Optional.ofNullable(dingAuthService.getUserByUserId(userid)) .orElse(dingAuthService.getUserByUsername(mobile)); BeanUtil.copyProperties(dailyViewUser, user); user.setAccessToken(accessToken); user.setName(response.getName()); user.setAvatar(response.getAvatar()); } catch (Exception e) { log.error("E|DingAuthController|assembleUserDTO()|根据用户钉钉userId或者用户钉钉手机号mobile查询用户信息失败!"); } return user; } @ApiOperation(value = "钉钉配置接口") @PostMapping("/config") @ResponseBody public JsonResult config(HttpServletRequest request) { ConfigDTO config = new ConfigDTO(); String url = request.getRequestURL().toString(); String queryString = request.getQueryString(); if (queryString != null) { url = url + queryString; } ServiceResult<String> jsTicketSr = tokenService.getJsTicket(); if (!jsTicketSr.isSuccess()) { return JsonResult.fail(Integer.parseInt(jsTicketSr.getCode()), jsTicketSr.getMessage()); } config.setAgentId(dingAppConfig.getAgentId()); config.setCorpId(dingAppConfig.getCorpId()); config.setJsticket(jsTicketSr.getResult()); config.setNonceStr(JsApiSignature.genNonce()); config.setTimeStamp(System.currentTimeMillis() / 1000); String sign; try { sign = JsApiSignature.sign(url, config.getNonceStr(), config.getTimeStamp(), config.getJsticket()); } catch (DingtalkEncryptException e) { return JsonResult.fail(e.getCode(), e.getMessage()); } config.setSignature(sign); return JsonResult.ok(config); } }
4.2 Service层以及ServiceImpl
package com.iot.daily.dingding.service; import com.iot.daily.module.entity.DailyViewUser; /** * <p>DingAuthService 此接口用于:</p> * <p>@author:hujm</p> * <p>@date:2021年05月20日 15:56</p> * <p>@remark:</p> */ public interface DingAuthService { /** * 根据用户手机号查询是否是公司员工 * * @param userId 钉钉用户id * @return UIOT员工信息 */ DailyViewUser getUserByUserId(String userId); /** * 根据用户手机号查询是否是公司员工 * * @param mobile 钉钉用户手机号 * @return UIOT员工信息 */ DailyViewUser getUserByUsername(String mobile); }
package com.iot.daily.dingding.service; import com.dingtalk.api.DefaultDingTalkClient; import com.dingtalk.api.request.OapiGetJsapiTicketRequest; import com.dingtalk.api.request.OapiGettokenRequest; import com.dingtalk.api.response.OapiGetJsapiTicketResponse; import com.dingtalk.api.response.OapiGettokenResponse; import com.taobao.api.ApiException; import com.iot.daily.common.constant.CommonConstants; import com.iot.daily.common.util.cache.RedisCache; import com.iot.daily.dingding.config.DingAppConfig; import com.iot.daily.dingding.config.DingUrlConstant; import com.iot.daily.dingding.domain.ServiceResult; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.concurrent.TimeUnit; /** * <p>DingLoginController 此类用于:获取access_token 和 jsTicket方法</p> * <p>@author:hujm</p> * <p>@date:2021年05月18日 15:06</p> * <p>@remark:钉钉企业内部微应用DEMO, 实现了身份验证(免登)功能</p> */ @Slf4j @Service public class TokenService { @Resource private RedisCache redisCache; /** * 缓存时间:一小时50分钟 */ private static final long CACHE_TTL = 60 * 55 * 2; @Resource private DingAppConfig dingAppConfig; /** * 在此方法中,为了避免频繁获取access_token, * 在距离上一次获取access_token时间在两个小时之内的情况, * 将直接从持久化存储中读取access_token * <p> * 因为access_token和jsapi_ticket的过期时间都是7200秒 * 所以在获取access_token的同时也去获取了jsapi_ticket * 注:jsapi_ticket是在前端页面JSAPI做权限验证配置的时候需要使用的 * 具体信息请查看开发者文档--权限验证配置 * * @return accessToken 或错误信息 */ public ServiceResult<String> getAccessToken() { // 从持久化存储中读取 String accessToken = redisCache.get(CommonConstants.DAILY_DING_ACCESS_TOKEN); log.info("从Redis缓存中获取到的accessToken = {}", accessToken); if (accessToken != null) { return ServiceResult.success(accessToken); } DefaultDingTalkClient client = new DefaultDingTalkClient(DingUrlConstant.URL_GET_TOKEN); OapiGettokenRequest request = new OapiGettokenRequest(); OapiGettokenResponse response; request.setAppkey(dingAppConfig.getAppKey()); request.setAppsecret(dingAppConfig.getAppSecret()); request.setHttpMethod("GET"); try { response = client.execute(request); } catch (ApiException e) { log.error("getAccessToken failed", e); return ServiceResult.failure(e.getErrCode(), e.getErrMsg()); } accessToken = response.getAccessToken(); log.info("向Redis缓存中存取accessToken = {}", accessToken); redisCache.set(CommonConstants.DAILY_DING_ACCESS_TOKEN, accessToken, CACHE_TTL, TimeUnit.SECONDS); return ServiceResult.success(accessToken); } /** * 获取JSTicket, 用于js的签名计算 * 正常的情况下,jsapi_ticket的有效期为7200秒,所以开发者需要在某个地方设计一个定时器,定期去更新jsapi_ticket * * @return jsTicket或错误信息 */ public ServiceResult<String> getJsTicket() { // 从持久化存储中读取 String ticket = redisCache.get(CommonConstants.DAILY_DING_JS_TICKET); if (ticket != null) { return ServiceResult.success(ticket); } String accessToken; ServiceResult<String> tokenSr = getAccessToken(); if (!tokenSr.isSuccess()) { return ServiceResult.failure(tokenSr.getCode(), tokenSr.getMessage()); } accessToken = tokenSr.getResult(); DefaultDingTalkClient client = new DefaultDingTalkClient(DingUrlConstant.URL_GET_JSTICKET); OapiGetJsapiTicketRequest request = new OapiGetJsapiTicketRequest(); OapiGetJsapiTicketResponse response; request.setHttpMethod("GET"); try { response = client.execute(request, accessToken); } catch (ApiException e) { log.error("getAccessToken failed", e); return ServiceResult.failure(e.getErrCode(), e.getErrMsg()); } if (!response.isSuccess()) { return ServiceResult.failure(response.getErrorCode(), response.getErrmsg()); } ticket = response.getTicket(); redisCache.set(CommonConstants.DAILY_DING_JS_TICKET, ticket, CACHE_TTL, TimeUnit.SECONDS); return ServiceResult.success(ticket); } }
上面TokenService类中的两个常量分别是:
/**
* 日报系统钉钉JSTICKET命名空间和ACCESSTOKEN命名空间
*/
public static final String DAILY_DING_JS_TICKET = "dailyApplication:login:ticket:";
public static final String DAILY_DING_ACCESS_TOKEN = "dailyApplication:login:accessToken:";
package com.iot.daily.dingding.service.impl; import com.iot.daily.dingding.service.DingAuthService; import com.iot.daily.module.dao.DailyViewUserMapper; import com.iot.daily.module.entity.DailyViewUser; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * <p>DingAuthServiceImpl 此类用于:</p> * <p>@author:hujm</p> * <p>@date:2021年05月20日 15:56</p> * <p>@remark:</p> */ @Slf4j @Service public class DingAuthServiceImpl implements DingAuthService { @Resource private DailyViewUserMapper dailyViewUserMapper; @Override public DailyViewUser getUserByUserId(String userId) { if (userId == null) { log.error("E|DingAuthServiceImpl|getUserByUserId()|根据用户手机号查询UIOT用户时,当前钉钉用户id为空!"); return new DailyViewUser(); } DailyViewUser dailyViewUser = dailyViewUserMapper.getDailyViewUserByUserId(userId); log.info("E|DingAuthServiceImpl|getUserByUserId()|根据用户手机号查询UIOT用户,userId = {}", userId); return dailyViewUser; } @Override public DailyViewUser getUserByUsername(String mobile) { if (mobile == null) { log.error("E|DingAuthServiceImpl|getUserByUsername()|根据用户手机号查询UIOT用户时,当前钉钉用户id为空!"); return new DailyViewUser(); } DailyViewUser dailyViewUser = dailyViewUserMapper.getDailyViewUserByUsername(mobile); log.info("E|DingAuthServiceImpl|getUserByUsername()|根据用户手机号查询UIOT用户,userId = {}", mobile); return dailyViewUser; } }
4.3 相关配置类
package com.iot.daily.dingding.config; import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; /** * <p>DingAppConfig 此类用于:应用凭证配置</p> * <p>@author:hujm</p> * <p>@date:2021年05月18日 17:25</p> * <p>@remark:</p> */ @Data @Configuration public class DingAppConfig { @Value("${dingtalk.app_key}") private String appKey; @Value("${dingtalk.app_secret}") private String appSecret; @Value("${dingtalk.agent_id}") private String agentId; @Value("${dingtalk.corp_id}") private String corpId; }
package com.iot.daily.dingding.config; /** * 钉钉开放接口网关常量 * * @author Administrator */ public class DingUrlConstant { private static final String HOST = "https://oapi.dingtalk.com"; /** * 获取access_token url */ public static final String URL_GET_TOKEN = HOST + "/gettoken"; /** * 获取jsapi_ticket url */ public static final String URL_GET_JSTICKET = HOST + "/get_jsapi_ticket"; /** * 通过免登授权码获取用户信息 url */ public static final String URL_GET_USER_INFO = HOST + "/user/getuserinfo"; /** * 根据用户id获取用户详情 url */ public static final String URL_USER_GET = HOST + "/user/get"; /** * 获取部门列表 url */ public static final String URL_DEPARTMENT_LIST = HOST + "/department/list"; /** * 获取部门用户 url */ public static final String URL_USER_SIMPLELIST = HOST + "/user/simplelist"; }
package com.iot.daily.dingding.domain; import java.io.Serializable; /** * <p>DingLoginController 此类用于:service层返回对象列表封装</p> * <p>@author:hujm</p> * <p>@date:2021年05月18日 15:06</p> * <p>@remark:service层返回对象列表封装</p> */ public class ServiceResult<T> implements Serializable { private boolean success = false; private String code; private String message; private T result; private ServiceResult() { } public static <T> ServiceResult<T> success(T result) { ServiceResult<T> item = new ServiceResult<T>(); item.success = true; item.result = result; item.code = "0"; item.message = "success"; return item; } public static <T> ServiceResult<T> failure(String errorCode, String errorMessage) { ServiceResult<T> item = new ServiceResult<T>(); item.success = false; item.code = errorCode; item.message = errorMessage; return item; } public static <T> ServiceResult<T> failure(String errorCode) { ServiceResult<T> item = new ServiceResult<T>(); item.success = false; item.code = errorCode; item.message = "failure"; return item; } public boolean hasResult() { return result != null; } public boolean isSuccess() { return success; } public T getResult() { return result; } public String getCode() { return code; } public String getMessage() { return message; } }
以上代码为核心代码,如果想要查看全部代码请移步至码云上哦,地址:daily: 公司每天需要员工填写的日报内容 。或者找博主索取哦!
五、免登录注意事项
钉钉免登录应用注意事项:
- 创建应用;
- 配置出口IP与访问首页;
- 开放权限(手机号码信息、通讯录部门信息读权限、成员信息读权限、通讯录部门成员读权限);
完结!