ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ruoyi-vue-pro集成钉钉扫码登录实战指南

Ruoyi-vue-pro集成钉钉扫码登录实战指南 1. 项目概述钉钉扫码登录已经成为企业级应用的标准配置方案之一。作为国内主流的企业办公平台钉钉提供了完善的OAuth2.0授权体系而ruoyi-vue-pro作为当前流行的Java快速开发框架其集成钉钉扫码登录的需求在企业管理系统中非常普遍。本文将详细解析从零开始实现这一功能的全过程。在实际开发中我发现很多开发者虽然按照官方文档操作但仍然会遇到各种坑。比如回调地址配置错误、签名验证失败、用户信息获取异常等问题。本文将结合我最近在金融行业ERP系统中的实战经验分享完整的实现方案和避坑指南。2. 环境准备与配置2.1 钉钉开发者账号申请首先需要登录钉钉开放平台https://open.dingtalk.com/创建应用。选择企业内部应用类型时要注意以下几点应用图标建议使用透明背景的PNG格式尺寸为60*60像素应用名称要与企业实名认证信息一致开发模式选择企业自主开发创建完成后在应用信息页面可以获取到关键的AppKey和AppSecret。这里有个常见误区很多开发者会混淆CorpId和AppKey实际上CorpId是企业标识而AppKey是应用标识。2.2 ruoyi-vue-pro项目配置在ruoyi-vue-pro的application.yml中添加钉钉配置# 钉钉配置 dingtalk: app-key: your_app_key app-secret: your_app_secret redirect-uri: https://yourdomain.com/auth/dingtalk/callback admin-role-id: 100 # 管理员角色ID特别注意redirect-uri需要与钉钉后台配置的回调域名完全一致包括http/https协议。在实际部署中我曾遇到因为测试环境用http而生产环境用https导致的回调失败问题。3. 前端集成实现3.1 引入钉钉JSAPI在ruoyi-vue-pro的public/index.html中添加script srchttps://g.alicdn.com/dingding/dingtalk-jsapi/2.10.3/dingtalk.open.js/script建议使用固定版本号而非latest避免因API变更导致兼容性问题。在登录页面组件中添加扫码登录按钮template el-button typeprimary icondingtalk clickhandleDingTalkLogin 钉钉扫码登录 /el-button /template script export default { methods: { handleDingTalkLogin() { const redirectUri encodeURIComponent( ${window.location.origin}/auth/dingtalk/callback ); window.location.href https://login.dingtalk.com/oauth2/auth?response_typecodeclient_id${this.$store.state.settings.dingtalk.appKey}redirect_uri${redirectUri}scopeopenidstatedingtalkpromptconsent; } } } /script重要提示redirect_uri必须进行encodeURIComponent编码否则特殊字符会导致跳转失败。state参数建议使用随机字符串增强安全性。3.2 处理回调页面创建src/views/auth/dingtalk-callback.vue组件script export default { created() { const code this.$route.query.code; if (code) { this.loginWithCode(code); } else { this.$message.error(授权失败未获取到code); this.$router.push(/login); } }, methods: { async loginWithCode(code) { try { const res await this.$store.dispatch(user/dingtalkLogin, code); await this.$store.dispatch(user/getInfo); this.$router.push({ path: this.redirect || / }); } catch (error) { console.error(钉钉登录失败:, error); this.$router.push(/login); } } } } /script4. 后端服务实现4.1 添加DingTalkService创建service模块Service public class DingTalkService { private static final String ACCESS_TOKEN_URL https://api.dingtalk.com/v1.0/oauth2/userAccessToken; private static final String USER_INFO_URL https://api.dingtalk.com/v1.0/contact/users/me; Value(${dingtalk.app-key}) private String appKey; Value(${dingtalk.app-secret}) private String appSecret; public DingTalkUserInfo getUserInfo(String authCode) { // 1. 获取access_token String accessToken getAccessToken(authCode); // 2. 获取用户信息 return getUserInfoByToken(accessToken); } private String getAccessToken(String authCode) { MapString, String params new HashMap(); params.put(clientId, appKey); params.put(clientSecret, appSecret); params.put(code, authCode); params.put(grantType, authorization_code); String response HttpUtil.post(ACCESS_TOKEN_URL, JSONUtil.toJsonStr(params)); JSONObject json JSONUtil.parseObj(response); if (json.containsKey(accessToken)) { return json.getStr(accessToken); } throw new RuntimeException(获取access_token失败: response); } private DingTalkUserInfo getUserInfoByToken(String accessToken) { String response HttpUtil.createGet(USER_INFO_URL) .header(x-acs-dingtalk-access-token, accessToken) .execute() .body(); JSONObject json JSONUtil.parseObj(response); return DingTalkUserInfo.builder() .unionId(json.getStr(unionId)) .nick(json.getStr(nick)) .avatarUrl(json.getStr(avatarUrl)) .mobile(json.getStr(mobile)) .build(); } }4.2 用户认证逻辑在UserServiceImpl中添加Override public String dingtalkLogin(String code) { DingTalkUserInfo userInfo dingTalkService.getUserInfo(code); // 查询是否已绑定用户 SysUser user userMapper.selectByDingTalkUnionId(userInfo.getUnionId()); if (user null) { // 新用户自动注册 user new SysUser(); user.setUserName(ding_ userInfo.getUnionId().substring(0, 8)); user.setNickName(userInfo.getNick()); user.setAvatar(userInfo.getAvatarUrl()); user.setPhonenumber(userInfo.getMobile()); user.setDingtalkUnionId(userInfo.getUnionId()); userMapper.insertUser(user); // 分配默认角色 userRoleMapper.insertUserRole(user.getUserId(), dingtalkAdminRoleId); } // 生成Token return tokenService.createToken(user); }5. 常见问题与解决方案5.1 扫码后页面白屏可能原因及解决方案回调地址未备案在钉钉开放平台→应用开发→IP白名单中添加服务器IP跨域问题确保前端redirect_uri与后台配置完全一致协议不一致开发环境用http而生产环境用https会导致问题5.2 获取用户信息返回401典型错误日志{code:InvalidAuthentication,message:无效的认证信息}解决方案检查access_token是否过期默认2小时有效期确认请求头是否正确添加x-acs-dingtalk-access-token检查appSecret是否泄露或重置过5.3 移动端兼容性问题在uni-app等混合开发环境中需要特殊处理// 判断运行环境 if (window.dd window.dd.runtime) { // 钉钉容器内运行 dd.ready(() { dd.runtime.permission.requestAuthCode({ corpId: corpId, onSuccess: (info) { this.loginWithCode(info.code); } }); }); } else { // 普通浏览器环境 this.handleDingTalkLogin(); }6. 安全增强措施6.1 防CSRF攻击在state参数中加入随机token并验证// 生成state String state UUID.randomUUID().toString(); redisTemplate.opsForValue().set(dingtalk:state:state, 1, 5, TimeUnit.MINUTES); // 回调时验证 String state request.getParameter(state); if (!redisTemplate.hasKey(dingtalk:state:state)) { throw new RuntimeException(非法的state参数); }6.2 敏感信息保护用户手机号加密存储user.setPhonenumber(encryptService.encrypt(userInfo.getMobile()));接口限流防护RateLimiter(key dingtalk:login:#{ip}, count 5, time 60) public String dingtalkLogin(String code) { // ... }7. 性能优化实践7.1 缓存access_token钉钉access_token有效期为2小时可以适当缓存Cacheable(value dingtalk, key access_token:#authCode) public String getAccessToken(String authCode) { // 原获取逻辑 }7.2 异步日志记录使用Spring Event异步记录登录日志Component RequiredArgsConstructor public class DingTalkLoginListener { private final SysLogininforMapper logininforMapper; Async EventListener public void handleLoginEvent(DingTalkLoginEvent event) { SysLogininfor logininfor new SysLogininfor(); logininfor.setUserName(event.getUsername()); logininfor.setIpaddr(event.getIp()); logininfor.setMsg(钉钉扫码登录); logininfor.setStatus(0); logininforMapper.insertLogininfor(logininfor); } }8. 扩展功能实现8.1 与现有账号体系绑定提供绑定解绑功能PostMapping(/bindDingTalk) public Result bindDingTalk(RequestParam String code) { Long userId SecurityUtils.getUserId(); DingTalkUserInfo userInfo dingTalkService.getUserInfo(code); userMapper.updateUser( new SysUser().setUserId(userId) .setDingtalkUnionId(userInfo.getUnionId()) ); return Result.success(); }8.2 扫码登录数据统计添加数据统计接口CREATE TABLE sys_dingtalk_login ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, login_time DATETIME NOT NULL, ip VARCHAR(50), device VARCHAR(200), INDEX idx_user (user_id), INDEX idx_time (login_time) );实现统计查询public ListDingTalkLoginStats statsLoginData(Date start, Date end) { return dingtalkLoginMapper.selectStatsByDate(start, end); }在实际项目中这套钉钉扫码登录方案已经稳定运行超过6个月日均处理登录请求3000次。关键点在于处理好回调流程和安全验证同时做好异常情况的兼容处理。对于企业级应用来说建议将钉钉登录与原有账号体系解耦采用绑定模式而非替代模式这样既能享受扫码登录的便利又能保留原有的账号管理体系。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进