
把团队里七八个系统的登录账号收敛到同一个入口这件事我拖了快一年。年前终于下了决心用 Hadess 做统一认证服务同时把钉钉集成进去员工在钉钉里扫个码就能登录内部系统再也不用记七八套账号密码。整个落地过程比预想中曲折一些钉钉的回调域名校验、敏感字段权限申请、账号自动匹配策略每个环节都有各自的坑。这篇文章把我最终跑通的方案、踩过的坑、排查思路一次讲清楚代码和配置都是可以直接抄走的程度。项目标题是 Hadess 实战但它并不神秘。你可以把它理解为团队内部的一个认证中心负责你是谁这个核心问题所有系统都通过它来确认用户身份。钉钉集成是身份源对接统一认证登录是最终的业务效果。把这两件事串起来你会发现企业内部的账号体系其实可以收敛得很清爽。1. 为什么做统一认证以及 Hadess 的整体思路1.1 内部系统账号乱象先说背景。很多团队都有这个问题公司内部跑着 OA、Wiki、日志平台、BI 报表、工单系统等一堆内部工具每个系统都有自己的账号体系。有的系统是自己建的 user 表有的是直接套开源框架的登录模块还有一两个系统只支持 LDAP。员工入职时要挨个系统注册账号离职时又要在每个系统里手动停用管理员想查一下某个人的整体权限分布都无从下手。这些系统还有个通病密码策略不统一。有的要求三个月改一次有的要求必须有大小写加特殊符号有的干脆是初始密码从来不提醒改。结果就是员工在电脑边上贴便利贴或者把密码存在手机备忘录里安全风险反而比什么都不做更大。我当时的诉求很明确所有内部系统统一到一个登录入口一次登录处处通行账号生命周期跟着钉钉通讯录走入职自动开通、离职自动失效员工用钉钉扫码就能登录不需要额外记忆任何密码。1.2 Hadess 的定位把认证逻辑从业务系统里抽出来Hadess 在这套方案里扮演的角色是统一认证中心它要替所有业务系统解决三件事一是接收用户的登录请求二是对接钉钉身份源完成身份确认三是签发和管理统一的登录凭证。业务系统这边不需要再关心用户密码怎么存、密码怎么校验、会话怎么维持只需要在入口处做一次重定向用户未登录就跳到 HadessHadess 认证通过后带着一次性票据回来业务系统用这个票据换一个访问凭证之后该干嘛干嘛。这个把认证抽出来的思路看似简单但能解决一个很现实的问题以后新接入一个系统不需要再开发一套登录模块只需要在 Hadess 里配置一个应用填好回调地址和密钥业务系统侧做几百行代码的接入即可。团队里新系统上线的速度提升是很明显的。1.3 为什么选钉钉作为身份源员工身份信息从哪来这是统一认证落地前必须想清楚的问题。我们团队内部员工全部使用钉钉钉钉通讯录里的组织架构是现成的人员台账天然适合做身份源。相比自建账号体系钉钉的优势非常明显。第一个优势是无需预先录入。新员工入职后人事把账号开通进钉钉他天然就拥有了进入内部系统的资格不需要管理员再手动在认证中心里建一条用户记录。第二个优势是扫码免密钉钉 App 本身就是企业内部高频应用扫码授权是员工已经习惯的操作几乎零学习成本。第三个优势是钉钉开放平台提供了标准 OAuth2 授权码模式和主流的 SSO 协议衔接非常顺畅Hadess 对接起来不需要写什么偏门代码。当然也有替代方案比如用企业微信、飞书、LDAP 或云身份服务。但我当时的判断标准很简单哪个工具全员覆盖率高、用户不需要额外装 App就选哪个。从这个角度看钉钉是很多企业里最不需要犹豫的选择。2. 钉钉侧配置从建应用到拿权限2.1 创建企业内部应用获取 AppKey 和 AppSecret钉钉集成第一步是在钉钉开放平台创建一个企业内部应用。登录钉钉开发者后台选择企业内部应用创建应用后系统会生成一对关键凭证AppKey 和 AppSecret。这两个值对应 OAuth2 协议里的 client_id 和 client_secretHadess 配置身份源时要用到的就是它们。注意 AppSecret 只在创建时可以完整查看后面任何时候再次进入页面都只能重置所以创建后马上复制到安全的地方保存。这个密钥是要放在 Hadess 服务端配置里的绝对不能写进前端代码或提交到代码仓库。应用创建时还要选择登录与免登这个能力。钉钉开放平台把很多能力集中在同一个应用下扫码登录、H5 微应用免登、消息推送等都在应用内管理我们只需要用到登录相关能力其他保持不开启就好权限面越小越安全。2.2 配置回调域名勾选必要的权限点应用创建完接下来要配置回调域名。这个配置在应用详情页的登录与免登区域作用是声明钉钉授权成功后允许把用户跳转回哪个地址。我们的实际配置指向 Hadess 的一个回调接口比如https://sso.example.com/callback/dingtalk这条记录的关键在于协议、域名、端口、路径必须与 Hadess 回调接口完全一致。钉钉在回调时会做精确匹配哪怕末尾多一个斜杠都会校验失败。这个细节在测试环境很常见我一度以为是代码写错了排查半天发现是回调 URL 末尾多了个斜杠钉钉直接拒绝了跳转。权限点方面需要根据账号匹配策略来选择。如果打算用手机号自动匹配内部账号通常要申请个人手机号信息权限。这个权限属于敏感权限审批比较谨慎建议尽早提交申请。如果拿不到手机号权限也可以退而求其次用 unionId 做首次绑定、后续识别的方案这个我后面详细展开。这里给一个权限点参考表权限点用途建议个人基础信息获取用户昵称、头像、unionId必选个人手机号信息获取用户手机号用于自动匹配内部账号按需申请审批较慢通讯录个人信息通过 userid 查询企业通讯录用户详情需要查组织架构时再申请通讯录部门信息获取部门列表和成员归属需要做数据权限时再申请权限申请通过后建议在配置里把应用的服务器出口 IP白名单也设置一下。钉钉开放平台支持限制 API 调用来源 IP只允许填写 Hadess 服务所在的公网出口 IP这样即使 AppSecret 泄露外部也无法调用接口。但要注意一旦设置了白名单Hadess 服务的所有出网请求都会走钉钉 API如果服务部署在多个地区或多个出口必须把所有出口 IP 都加进去漏了一个就会看到接口突然报 Forbidden。3. 认证核心流程拆解与代码实现3.1 钉钉扫码登录的完整时序整个登录链路可以拆成七个步骤我画不出来时序图用文字描述也一样直观用户访问业务系统发现未登录被重定向到 Hadess 的登录页。Hadess 登录页提供钉钉扫码登录按钮点击后跳转到钉钉开放平台的 OAuth2 授权页。用户在钉钉 App 里确认授权钉钉服务器将浏览器重定向回 Hadess 回调地址并携带授权码 code。Hadess 拿着 code 调钉钉接口换取用户访问令牌 accessToken。Hadess 拿着 accessToken 调钉钉用户信息接口拿到用户唯一标识 unionId、昵称、头像。Hadess 根据 unionId 查找本地账号绑定关系识别出这是哪个内部用户创建统一登录会话。Hadess 生成一次性票据重定向回业务系统业务系统拿着票据到 Hadess 换取访问凭证后续携带凭证访问。这个流程里每一个环节都有对应的钉钉官方接口但有几个细节值得先说出来。授权码 code 是一次性的只能用一次有效期通常只有 5 分钟。如果服务端换 token 失败想重试不能拿同一个 code 再换一次只能让用户重新扫码。我们也因此在回调接口里做了完善的状态记录避免因为网络抖动导致用户明明扫码成功却要再扫一遍。还有一个容易忽略的是 state 参数。跳转钉钉授权页之前Hadess 会生成随机 state 值存在当前会话里钉钉回调时会原样带回这个 state服务端必须校验它与会话中的值一致否则拒绝处理。这个机制用来防止登录流程被 CSRF 攻击不能省。实际开发中我们同时把来源系统编号和回调地址编码进 state回调后直接就能知道该跳回哪个业务系统省了一步去查会话的时间。3.2 服务端换取用户信息并匹配内部账号代码层面Hadess 回调接口的核心逻辑大致如下我用 Spring Boot 风格写了一个精简版本GetMapping(/callback/dingtalk) public void dingtalkCallback(String code, String state, HttpServletRequest request, HttpServletResponse response) { // 1. 校验 state防止 CSRF if (!state.equals(request.getSession().getAttribute(oauth_state))) { throw new BizException(state校验失败); } // 2. 用 code 换取钉钉 userAccessToken MapString, Object tokenBody Map.of( clientId, appKey, clientSecret, appSecret, code, code, grantType, authorization_code ); String tokenJson restTemplate.postForObject( https://api.dingtalk.com/v1.0/oauth2/userAccessToken, tokenBody, String.class); String accessToken parseAccessToken(tokenJson); // 3. 用 accessToken 获取用户基础信息 HttpHeaders headers new HttpHeaders(); headers.set(x-acs-dingtalk-access-token, accessToken); ResponseEntityDingUser userResp restTemplate.exchange( https://api.dingtalk.com/v1.0/contact/users/me, HttpMethod.GET, new HttpEntity(headers), DingUser.class ); DingUser dingUser userResp.getBody(); // 4. 根据 unionId 查找账号绑定关系未绑定时按手机号匹配或进入绑定页 UserAccount account accountService.findByDingUnionId(dingUser.getUnionId()); if (account null) { account accountService.matchByMobile(dingUser.getMobile()); if (account null) { // 跳转绑定引导页让用户输入工号或企业邮箱完成首次绑定 redirectToBindPage(response, dingUser.getUnionId()); return; } accountService.bindDingUnionId(account.getId(), dingUser.getUnionId()); } // 5. 生成统一会话和一次性票据重定向回业务系统 String ticket sessionService.createTicket(account); redirectBackToBusinessSystem(response, ticket); }这段代码里有几个值得展开的设计点。首先是账号匹配策略。第一次扫码登录时用户还没有和内部账号建立绑定关系此时如果钉钉接口能返回手机号我们可以尝试用手机号匹配内部账号。这个方法在国内企业非常实用因为员工在钉钉里的手机号基本都与人事系统一致。但手机号匹配存在误配风险比如员工更换手机号或钉钉账号是私人号码注册的所以匹配成功后要强制走一次绑定确认而不是直接放行。如果手机号不可用或者匹配失败就让用户进入一个绑定引导页输入工号或企业邮箱Hadess 验证通过后建立 unionId 和内部账号的绑定关系。这里我们做了一个权衡首次绑定多花十几秒换来之后每次登录都快速且安全。实际上批量老员工迁移时我们还专门开发了一个导入工具管理员可以从钉钉通讯录导出手机号批量灌入绑定关系省去了大量首次登录时的绑定操作。其次是登录凭证。ticket 是一次性的有效期 5 分钟只能被业务系统兑换一次。兑换成功后 Hadess 返回一个 JWT 访问令牌里面包含用户 ID、姓名、unionId 和过期时间密钥使用 HS256 签名。各业务系统共享同一个密钥或者通过 Hadess 暴露的公钥接口验签之后每次请求只需要校验 JWT 合法性不需要再回 Hadess 查会话这样既支持无状态服务也方便水平扩展。3.3 业务系统接入几十行代码换掉整套登录模块业务系统接入 Hadess 属于标准的 OAuth2 客户端接入流程。核心就三步配置 Hadess 回跳地址、处理票据兑换、校验令牌。业务系统的拦截器检测到当前请求未携带合法 JWT 时会构造一个跳转地址指向 Hadess 登录页同时带上自己的应用 ID 和回调地址。用户登录完成Hadess 会把用户带回业务系统回调地址并携带 ticket业务系统后端调用 Hadess 的票据兑换接口拿到 JWT 后写入本地 Cookie 或前端存储后续请求都带着这个 JWT 访问。// 业务系统一侧的接入伪代码 public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) res; String token extractToken(request); if (StringUtils.isBlank(token) || !jwtService.verify(token)) { String loginUrl ssoConfig.getHadessLoginUrl() ?app_id ssoConfig.getAppId() redirect_uri URLEncoder.encode(currentFullUrl(request), UTF-8); response.sendRedirect(loginUrl); return; } // 从 token 解析用户信息写入 request 上下文 request.setAttribute(currentUser, jwtService.parse(token)); chain.doFilter(req, res); } }接入过程中最容易踩的坑是业务系统回调地址的闭环问题。我们有一个系统部署在内网用户从内网域名访问时回调地址是 http://intra.example.com 这样的形式Hadess 登录成功后跳回这个地址但如果用户是从公网域名访问的跳回内网地址就会断掉。最终的处理方案是业务系统重定向到 Hadess 时把所有可能的回调域名都注册到 Hadess 应用配置里Hadess 只放行已注册域名下的回调地址。这个白名单列表一开始比较小后面加两个域名是常有的事所以建议应用配置里预留扩展空间而不是写死一个地址。4. 常见问题与排查技巧实录4.1 问题速查表上线至今我整理了实际运维中遇到的典型问题直接做成表格方便大家对照排查。现象常见原因排查方式跳转钉钉授权页后提示 redirect_uri 不匹配回调域名末尾斜杠、端口不一致或路径大小写错误登录钉钉开发者后台比对配置的回调 URL 与实际请求地址扫码后回调接口收到 code 但换 token 失败授权码超过 5 分钟时效或已使用过一次让用户重新走一遍登录流程确认没有重复回调回调后页面死循环跳转业务系统 Cookie 域与 Hadess 会话域不一致检查业务系统写 Cookie 的 domain 配置建议统一为一二级域名获取用户信息时接口报 Forbidden应用未拿到对应权限点或服务器出口 IP 不在白名单到钉钉开发者后台检查权限审批状态、IP 白名单配置用户信息里手机号为空未申请个人手机号信息权限或接口不返回该字段改用 unionId 绑定方案不依赖手机号做匹配登录成功后跳回业务系统但业务系统换票失败ticket 只能换一次可能回调或网关重试导致二次兑换在票据兑换接口做幂等处理记录 ticket 状态并返回第一次的令牌4.2 踩过的几个隐蔽坑第一个隐蔽的坑是 IP 白名单。早前使用钉钉接口一直正常某天突然全部返回无权限错误一开始怀疑是密钥被重置排查发现是因为 Hadess 服务迁移到了新的云服务器出口 IP 变了而钉钉开发者后台的服务器出口 IP 白名单还停留在旧地址。这个问题的诡异之处在于它不影响登录页跳转只影响服务端 API 调用容易让人定位到错误方向上。第二个坑是账号绑定表的脏数据。Hadess 上线第二天就出现了两个员工互相登录到对方账号的现象排查后发现问题出在手机号匹配逻辑上一名员工在钉钉里绑定的手机号已经停用同一号码又被另一个新员工使用导致新员工首次登录时匹配到了旧账号。此后我们加了保护策略手机号匹配成功后如果账号已经绑定过其他钉钉 unionId则禁止自动绑定必须走人工确认流程。第三个坑是 JWT 注销不及时。JWT 本身是无状态的服务端无法主动让一个已签发的 token 失效。员工从某个系统退出登录另外几个系统里的 token 仍然有效直到过期。我们最终的做法是给 JWT 设置较短的有效期比如 30 分钟同时引入一个刷新 token 机制会话要持续存在时由客户端用刷新 token 换新的访问 token一旦用户主动登出刷新 token 被撤销访问 token 很快也会自然过期。这个方案在安全和体验之间取了平衡目前用起来比较顺手。5. 一些值得收藏的实战建议经过这一轮完整的实战有几个经验我认为比代码本身更值得分享。第一钉钉的应用权限申请要尽早启动。内部应用创建的审批通常是即时完成的但敏感权限的审批要走企业管理员流程有的企业还需要多个审批层级耗时可能从几小时到数天。我在项目启动第一天就提交了手机号权限申请开发过程中其他功能做完时权限正好审批通过几乎没有等待。第二回调地址和域名使用统一的公共前缀。Hadess 的登录页、回调接口、票据兑换接口建议使用同一套域名不要测试环境用一套、生产环境用另一套也不要让不同环境共享同一个回调地址。我们因为开发环境和生产环境的回调 URL 长得非常像曾经出现过配置复制粘贴错误导致生产环境跳转测试环境回调地址的事故。第三绑定流程一定要做可逆操作。员工在钉钉里换了手机号或者重新注册了钉钉账号会导致原 unionId 无法识别。Hadess 里必须提供解绑和重新绑定的入口并且解绑操作要记录日志。这个功能平时用不到一旦有人换手机就会发现它是救命的。第四从运维视角看建议给 Hadess 的凭证和相关密钥建立定期轮换机制。AppSecret、JWT 签名密钥都不应该永久不变至少半年换一次。换密钥时要注意协调好所有业务系统的更新时间尽量在低峰期操作并准备好回滚方案。对我来说这套方案最大的收获不是省掉了多少登录页面而是把账号相关的事情从各个业务系统里彻底解放了出来。新系统接入时不用再考虑密码存储、会话管理、权限模型这些重复劳动只要对接 Hadess 就能获得一套完整的身份体系。钉钉集成和统一认证登录听起来是个挺大的工程拆开看无非是把跳转、回调、换令牌、建会话这几个动作做扎实剩下的就是耐心填坑了。