ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java 实现钉钉微应用免登 H5 首页:从 code 到手机号的完整链路

Java 实现钉钉微应用免登 H5 首页:从 code 到手机号的完整链路 简介本资源面向使用Java开发钉钉企业内部应用的开发者聚焦“钉钉微应用免登进入H5系统首页”这一典型场景帮助读者打通前端获取免登授权码与后端校验用户身份的完整链路。资源包内含1个PDF文档大小约129KB以图文形式梳理了从钉钉开放平台创建H5微应用、配置公网IP白名单、记录agentId、appKey、appSecret与corpId到开通企业通讯录接口权限、发布应用的准备流程。正文重点讲解ddNoLogin.html中调用requestAuthCode获取code、通过AJAX将code传给后端、后端借助gettoken与getuserinfo接口换取用户信息并重定向至首页的实现思路同时涉及access_token定时刷新与Redis缓存、免登成功后发送消息通知等扩展点。目前已有2113人学习下载适合需要快速落地钉钉免登功能、理清前后端协作与接口调用顺序的Java开发者参考。1. 钉钉微应用免登进 H5 首页一个被低估的 Java 后端活儿很多团队做钉钉微应用时第一反应是“前端拿个 code 丢给后端就完事了”真到联调才发现code 换 userid 报 400、token 缓存过期、公网 IP 没加白名单、发消息一天只能发一次。这个项目的核心就是用 Java 把“钉钉微应用免登进入某 H5 系统首页”跑通——用户在钉钉里点微应用前端通过 JS-API 拿到免登授权码后端用 appKey/appSecret 换 access_token再换 userid、拉手机号跟 H5 系统用户表比对存在就放行到首页不存在返回“您无权限”。适合正在做企业内部 H5 系统对接钉钉的 Java 后端也适合想搞清楚免登链路到底经过几次 HTTP 请求的开发者。下面按“资源是什么 → 怎么用 → 坑在哪”拆开讲。2. 免登链路拆解从 corpId 到手机号的四次握手2.1 为什么必须走“前端拿 code 后端换身份”这条路钉钉免登的本质是前端不接触任何敏感凭证只负责拿一个一次性的免登授权码 code后端拿着 code 和 access_token 去钉钉开放平台换用户身份。这样 appSecret 永远不出现在浏览器里是这套方案能上生产的前提。整条链路一共四次关键请求缺一不可步骤请求方目标关键参数返回1前端 JS-API钉钉客户端corpIdcode2后端gettoken 接口appkey、appsecretaccess_token3后端getuserinfo 接口access_token、codeuserid4后端user/get 接口access_token、useridmobile、name 等第 2 步的 access_token 有效期是 2 小时有效期内重复获取会返回相同结果并自动续期。所以正确做法不是每次请求都去换 token而是缓存起来定时刷新。项目里用的是“每隔 1 小时 50 分钟刷新一次缓存进 Redis”这个时间点卡在 2 小时过期之前留了 10 分钟缓冲是常见做法。第 3 步的 code 是一次性的用过即废且有效期很短。这意味着前端拿到 code 后必须立刻发给后端不能存起来慢慢用。第 4 步拿到的 mobile 才是跟 H5 系统用户表比对的关键字段——因为钉钉的 userid 是钉钉体系内的你的 H5 系统大概率是用手机号或自己的用户 ID 建的账号。2.2 前端 ddNoLogin.html只做一件事拿 code 就发走前端页面不需要 dd.config 鉴权因为 requestAuthCode 这个方法本身不需要鉴权。这一点很多人会搞混以为所有 JS-API 都要先 config结果白白多写一堆签名逻辑。!DOCTYPE html html head title微应用登陆/title meta charsetutf-8 meta nameviewport contentwidthdevice-width,initial-scale1 user-scalable0 / script srchttps://cdn.bootcss.com/jquery/3.3.1/jquery.min.js/script script typetext/javascript srchttp://g.alicdn.com/dingding/open-develop/1.9.0/dingtalk.js/script /head body div idddNoLogin/div script typetext/javascript dd.ready(function() { // 1.获取免登授权码code此方法不需要dd.config鉴权 dd.runtime.permission.requestAuthCode({ corpId: corpId, // 企业id由后端渲染或配置注入 onSuccess: function(result) { var code result.code; getUserInfo(code); // 拿到code立刻发给后端 }, onFail: function(err) { alert(出错了, err); } }); }); function getUserInfo(code) { $.ajax({ type: GET, url: /xxx/noLogin?code code, async: false, dataType: json, contentType: application/json;charsetutf-8, success: (function(res) { if (res.code 0000) { window.location.href /#/xxxxx; // 免登成功跳首页 } else { $(#ddNoLogin).html(res.msg); // 无权限展示提示 } }), }); } /script /body /html逻辑说明dd.ready 保证钉钉 JS 环境就绪后再调 requestAuthCode。corpId 是四个固定参数之一从钉钉开发者后台首页获取。onSuccess 里拿到的 code 通过 AJAX 同步发给后端/xxx/noLogin后端返回 code 为 0000 表示免登成功前端跳转 H5 首页否则把后端返回的 msg 渲染到页面上用户看到“您无权限访问”。参数说明corpId 必须和微应用所属企业一致填错会直接 onFail。async 设为 false 是为了确保跳转前拿到结果但生产环境更推荐用回调或 Promise 处理避免阻塞。url 里的/xxx/noLogin要和后端 Controller 的RequestMapping对齐。2.3 后端定时任务token 缓存进 Redis 的正确姿势access_token 不能每次请求都去换否则一是浪费调用次数二是并发场景下容易拿到不同 token 导致互相覆盖。项目里用 Spring 的Scheduled定时刷新缓存进 Redis。/** * 定时获取钉钉的token */ Component EnableScheduling public class DdTokenTask { Autowired private JedisClient jedisClient; public static final long cacheTime 1000 * 60 * 55 * 2; // 1小时50分钟 Value(${dtalk.tokenUrl}) private String tokenUrl; Value(${dtalk.app.key}) private String appKey; Value(${dtalk.app.secret}) private String appSecret; Value(${dtalk.redisTokenKey}) private String tokenKey; Value(${dtalk.taskRun}) private String taskRun; /** * 每隔1小时50分钟获取钉钉的access_token */ Scheduled(fixedRate cacheTime) Async public void getDdTokenTask() { if (true.equals(taskRun)) { System.out.println(----------------获取钉钉token的定时任务开始了 DateUtil.formatDateToString(new Date(), HH:mm:ss)); String accessTokenUrl tokenUrl ?appkey appKey appsecret appSecret; // 访问获取access_token 有效期是2小时 String accessToken JsonUtil.getJsonNode(HttpUtil.doGet(accessTokenUrl)).get(access_token).asText(); // 放入到redis中 jedisClient.set(tokenKey, accessToken); System.out.println(----------------获取钉钉token的定时任务结束了token accessToken); } } }逻辑说明Scheduled(fixedRate cacheTime)表示以固定频率执行cacheTime 设为 1 小时 50 分钟比 token 的 2 小时有效期提前 10 分钟刷新。Async让任务异步执行不阻塞主线程。taskRun 是个开关方便本地开发时关掉定时任务避免多个环境抢 token。参数说明tokenUrl 对应https://oapi.dingtalk.com/gettokenappKey 和 appSecret 从微应用后台获取redisTokenKey 是缓存 key 名。项目里没用钉钉官方 SDK而是直接用 HTTP 请求原因是公司私服没有该 SDK 依赖——这是很现实的取舍HTTP 方式少一个依赖但需要自己处理 JSON 解析和异常。注意如果部署了多个实例每个实例都会跑定时任务导致 token 被反复刷新。常见做法是加分布式锁或者只让一个实例执行刷新其他实例只读 Redis。3. 免登接口落地noLogin 方法里的四次 HTTP 调用3.1 Controller 完整实现与参数传递后端 noLogin 接口是整个免登的核心它串起了“换 userid → 拉手机号 → 比对用户 → 发消息”四件事。RestController RequestMapping(/ddUser) Api(value /ddUser, description 钉钉H5微应用登录, tags {DdLoginController}) public class DdLoginController { Autowired private JedisClient jedisClient; Value(${dtalk.userUrl}) private String userUrl; Value(${dtalk.userDetailUrl}) private String userDetailUrl; Value(${dtalk.redisTokenKey}) private String tokenKey; Value(${dtalk.agentId}) private Integer agentId; GetMapping(/noLogin) ApiOperation(钉钉免登) ApiImplicitParam(paramType query, name code, value 免登授权码, dataType String) public WebResponse noLogin(RequestParam(code) String code, HttpServletResponse response) { // 2.获取access_token从Redis缓存读取 String accessToken jedisClient.get(tokenKey); // 3.获取用户userid String userIdUrl userUrl ?access_token accessToken code code; JsonNode user JsonUtil.getJsonNode(HttpUtil.doGet(userIdUrl)); if (user.get(errcode).asInt() ! 0) { // 有些公司的公网ip不固定导致微应用中设置的不对这里就会报错 return WebResponse.resFail(user.get(errmsg).asText()); } String userId user.get(userid).asText(); // 4.获取用户详情 手机号 String userInfoUrl userDetailUrl ?access_token accessToken userid userId; JsonNode userInfo JsonUtil.getJsonNode(HttpUtil.doGet(userInfoUrl)); String mobile userInfo.get(mobile).asText(); System.out.println(钉钉用户的手机号 mobile); // 通过手机号获取该用户 SysUser sysUser sysUserService.getByMobile(mobile); if (sysUser null) { return WebResponse.resFail(您无权限访问, null); } // 钉钉发送免登成功消息给用户 sendMessage(accessToken, userId, userInfo.get(name).asText()); return WebResponse.resSuccess(免登成功, loginUserInfo); } }逻辑说明先从 Redis 拿 access_token避免每次请求都去换。然后用 code 换 userid这里判断 errcode 是否为 0不为 0 说明 code 无效或 token 过期直接把钉钉返回的 errmsg 透传给前端。拿到 userid 后再拉用户详情取 mobile 字段。用 mobile 去 H5 系统用户表查查不到就返回“您无权限访问”。查到就发消息并返回成功。参数说明userUrl 对应https://oapi.dingtalk.com/user/getuserinfouserDetailUrl 对应https://oapi.dingtalk.com/user/get。agentId 是微应用的标识发消息时必须传。code 是前端传来的免登授权码一次性使用。3.2 工作通知消息为什么消息里要加时间戳免登成功后给用户发一条工作通知是项目里加的小需求。钉钉的工作通知接口有个限制给同一个用户发送相同内容一天只能发一次发送不同内容一天可以 500 次。所以项目里在消息内容中拼了当前时间保证每次内容都不同。// 钉钉发送消息给用户 private void sendMessage(String token, String userId, String userName) { String messageUrl https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?access_token token; MapString, Object map new HashMap(); map.put(agent_id, agentId.longValue()); map.put(userid_list, userId); map.put(to_all_user, false); String content 用户 userName 在 DateUtil.formatDateToString(new Date(), yyyy-MM-dd HH:mm:ss) 时成功登录xxH5端并进入到xxx页面; String msg {\msgtype\:\text\,\text\:{\content\: \ content \ }}; JSONObject jsonObj JSONObject.parseObject(msg); map.put(msg, jsonObj); HttpUtil.doPost(messageUrl, map, UTF-8, 20000, null); }逻辑说明agent_id 是微应用 IDuserid_list 是接收人列表to_all_user 设为 false 表示不发给全员。msg 是一个 JSON 对象msgtype 为 textcontent 里拼了用户名、时间和页面信息。加时间戳是为了绕过“相同内容一天一次”的限制。参数说明messageUrl 里的 access_token 就是前面缓存的 token。HttpUtil.doPost 的超时设为 20000 毫秒因为发消息接口偶尔会慢。注意 userid_list 传的是钉钉的 userid不是手机号。注意工作通知消息的接口权限需要在钉钉后台开通否则会返回权限不足。另外消息发送失败不会影响免登主流程建议用 try-catch 包起来别让发消息的异常把登录搞挂了。4. 避坑排查免登联调时最容易翻车的五个点4.1 现象code 换 userid 返回 400提示 invalid code原因code 是一次性的且有效期极短。常见触发场景是前端拿到 code 后没有立刻发请求或者用户刷新了页面导致 code 被重复使用。另一个原因是 corpId 填错导致 code 根本不属于这个企业。解决前端在 onSuccess 回调里第一时间发 AJAX不要做任何异步等待。后端收到 code 后立即调用 getuserinfo不要先做其他耗时操作。corpId 从钉钉后台首页复制别手敲。4.2 现象gettoken 返回 400提示 invalid appkey or appsecret原因appKey 或 appSecret 填错或者微应用类型选错了。项目里特别强调是在“企业内部开发”中创建 H5 微应用不是“第三方企业应用”。两者拿到的凭证体系不一样用错了就换不到 token。解决登录 open-dev.dingtalk.com确认应用在“企业内部开发”分类下。重新复制 appKey 和 appSecret注意不要带空格。如果还是不行检查应用是否已发布未发布的应用部分接口不可用。4.3 现象getuserinfo 返回 400提示 ip not in whitelist原因钉钉要求调用服务端接口的服务器公网 IP 在微应用的白名单里。很多公司出口 IP 不固定或者用了多台机器负载均衡只填了一个 IP。解决用curl ifconfig.me查看当前公网 IP填到微应用的服务器出口 IP 配置里。如果 IP 会变常见做法是联系运维固定出口 IP或者把所有可能的出口 IP 都加上。项目代码里也做了处理errcode 不为 0 时直接把 errmsg 返回给前端方便定位。4.4 现象token 突然失效所有免登请求报错原因定时任务没跑起来或者 Redis 里的 token 被清掉了。另一个常见原因是多个实例同时刷新 token后刷新的覆盖了先刷新的导致部分请求拿到旧 token。解决检查EnableScheduling是否生效taskRun 配置是否为 true。多实例部署时用分布式锁保证只有一个实例执行刷新或者把刷新逻辑抽到单独的定时任务服务里。Redis 的 key 设置合理的过期时间但不要短于刷新周期。4.5 现象发消息返回 400提示 send too fast 或超过频率限制原因给同一个用户发送了相同内容触发了“一天一次”的限制。或者短时间内给大量用户发消息触发了接口频率限制。解决在消息内容里拼时间戳或随机数保证每次内容不同。批量发送时加间隔不要瞬间打满。项目里的做法是在 content 里拼yyyy-MM-dd HH:mm:ss简单有效。5. 进阶技巧把免登做成可复用的认证切面免登逻辑写在一个 Controller 里能跑但如果有多个 H5 页面都要免登复制粘贴就会失控。我一般会把“code 换用户”这段抽成一个独立的服务方法返回一个统一的 LoginUser 对象Controller 只负责调它和跳转。Service public class DingTalkAuthService { Autowired private JedisClient jedisClient; Value(${dtalk.userUrl}) private String userUrl; Value(${dtalk.userDetailUrl}) private String userDetailUrl; Value(${dtalk.redisTokenKey}) private String tokenKey; /** * 用免登code换取系统用户失败返回null */ public SysUser authByCode(String code) { String accessToken jedisClient.get(tokenKey); if (accessToken null) { throw new RuntimeException(token未就绪请检查定时任务); } // code换userid String userIdUrl userUrl ?access_token accessToken code code; JsonNode user JsonUtil.getJsonNode(HttpUtil.doGet(userIdUrl)); if (user.get(errcode).asInt() ! 0) { return null; } String userId user.get(userid).asText(); // userid换手机号 String userInfoUrl userDetailUrl ?access_token accessToken userid userId; JsonNode userInfo JsonUtil.getJsonNode(HttpUtil.doGet(userInfoUrl)); String mobile userInfo.get(mobile).asText(); return sysUserService.getByMobile(mobile); } }这样 Controller 里就只剩三行调 authByCode、判空、返回结果。多个页面共用同一套逻辑改一处全生效。验证免登是否真的通了我习惯按这个顺序走一遍先在钉钉开发者后台确认应用已发布、IP 白名单已配、接口权限已开然后在手机钉钉里点微应用看前端是否拿到 code再看后端日志里 getuserinfo 返回的 errcode 是否为 0最后看 Redis 里 token 是否存在且未过期。这四步走完基本能定位到是哪一环断了。还有一个容易忽略的点H5 系统的用户表里手机号字段必须和钉钉返回的 mobile 格式一致。钉钉返回的是不带国家码的 11 位手机号如果你的系统存的是带 86 的格式比对就会失败。我一般会在比对前做一次归一化去掉 86 和空格。从那以后我每次接钉钉免登都强制先把 token 定时任务和 IP 白名单这两件事确认一遍再开始写业务代码。这两处不出问题后面的链路基本就是顺的。希望帮到你。本文还有配套的精品资源点击获取
返回列表