MaxKey集成JeecgBoot实现单点登录:OAuth2统一身份认证实战
1. 项目背景与整体思路拆解先说结论MaxKey和JeecgBoot的集成本质上解决的是企业里“账号泛滥”和“重复登录”这两个老大难问题。我最早接到这个需求是因为公司内部同时跑了OA、报表、低代码平台好几套系统每个系统都有一套独立的账号体系。业务人员每天要在不同系统间切换密码记混了就去点“找回密码”IT部门光是处理密码重置工单就占用不少精力。后来我们决定用统一身份认证的思路来收口调研了一圈最终选了MaxKey作为认证中心JeecgBoot作为业务接入方。1.1 为什么选MaxKey而不是自己写认证自己写一套统一登录不是不行但投入产出比太低了。单点登录的核心不只是“登录”这一个动作它背后要处理会话管理、令牌签发、密钥轮换、客户端注册、审计日志、密码策略、多因素认证这一整套体系。这些功能从零开始开发没有两三个月拿不下来而且后续的安全维护成本更高。MaxKey是开源项目基于Java技术栈天然和JeecgBoot的Spring Boot生态匹配。它支持OAuth2.0、OIDC、CAS、SAML等多种协议这意味着将来不只是JeecgBoot其他系统比如泛微OA、金蝶云、自研的后台管理系统都可以通过标准协议接入进来不用针对每个系统单独做定制开发。这一点非常重要因为很多公司在实际落地时往往不是只接一个系统而是“先试点一个再批量铺开”协议的标准化决定了这个架构能走多远。1.2 协议选型为什么最终用OAuth2.0授权码模式MaxKey支持的协议很多但对接JeecgBoot我们用的OAuth2.0授权码模式。原因有几个第一JeecgBoot本身对OAuth2的集成支持比较完善社区里已经有可参考的案例踩坑成本低。第二授权码模式是OAuth2所有模式里安全性最高的。它经过了两步交互客户端拿到的授权码并不直接暴露给前端令牌的换取发生在后端完成access_token不会经过浏览器端能有效降低令牌被截获的风险。第三授权码模式天然支持刷新令牌access_token失效后可以用refresh_token静默续期用户的体验是“登录一次长期有效”减少了频繁重新认证的打扰。第四也是比较实际的一点JeecgBoot未来如果要做移动端或者第三方应用接入授权码模式通过PKCE扩展也能适应这些场景。选型的时候往前多想一步后面就不会被迫推翻重来。1.3 整体架构与登录流程设计整个集成的架构其实非常清晰我画个流程说明一下用户访问JeecgBoot的业务页面发现未登录JeecgBoot将请求重定向到MaxKey的授权端点。用户在MaxKey的统一登录页完成认证用户名密码、验证码等。MaxKey认证通过后生成授权码并回调到JeecgBoot预先配置的回调地址。JeecgBoot后端用授权码向MaxKey的令牌端点换取access_token和refresh_token。JeecgBoot调用MaxKey的用户信息端点获取用户基本信息用户名、邮箱、手机号等。JeecgBoot根据拿到的用户信息在自己的用户表里查找或创建对应的本地用户然后建立本地会话。这个流程里最关键的设计决策是JeecgBoot不直接信任前端传过来的用户身份而是以MaxKey的用户信息端点返回结果为准。很多人第一次做对接容易犯的错误是前端在URL参数里带个用户名就放行了这等于把身份认证的信任边界直接暴露给了攻击者。后端换token、后端拉取用户信息、后端建立会话这个链路必须完整走通。2. 环境准备MaxKey与JeecgBoot部署要点动手配置之前先把环境准备好。这部分看着基础但很多问题排查到最后发现都是环境不对导致的所以单独拿出来说说。2.1 MaxKey服务端部署方式选择MaxKey服务端官方提供了多种部署方式包括二进制包、Docker镜像、源码编译。我建议优先考虑Docker Compose方式原因很简单MaxKey依赖的组件比较多包括MySQL或PostgreSQL数据库、Redis缓存Docker Compose可以一次性把这些服务编排起来避免手动配置数据库连接、初始化脚本这些繁琐的步骤。version: 3 services: maxkey-mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: maxkey123 MYSQL_DATABASE: maxkey volumes: - ./mysql-data:/var/lib/mysql ports: - 3306:3306 maxkey-redis: image: redis:6-alpine ports: - 6379:6379 maxkey-server: image: maxkeytop/maxkey:latest ports: - 9527:9527 depends_on: - maxkey-mysql - maxkey-redis environment: MYSQL_HOST: maxkey-mysql MYSQL_PORT: 3306 MYSQL_DATABASE: maxkey MYSQL_USERNAME: root MYSQL_PASSWORD: maxkey123 REDIS_HOST: maxkey-redis REDIS_PORT: 6379 volumes: - ./maxkey-logs:/opt/maxkey/logs注意几个细节。MySQL数据库一旦初始化完成后数据库实例的编码和排序规则要确认是utf8mb4否则后面账户同步如果涉及中文用户名或者特殊字符存储会有问题。Redis主要用来管理会话和授权码的临时状态生产环境建议给Redis设置密码不要裸奔在公网。MaxKey默认端口是9527配置反向代理时要把这个端口映射出去。2.2 JeecgBoot环境准备JeecgBoot的部署相对简单它的后端是基于Spring Boot的前端是Vue3。本地开发环境需要准备的工具包括JDK 1.8或更高版本建议JDK 8即可兼容性最好。Maven 3.6配置好阿里云镜像仓库国内拉取依赖速度会快很多。Node.js 16npm或yarn安装前端依赖。RedisJeecgBoot的本地缓存和会话管理依赖Redis。MySQL 5.7JeecgBoot的业务数据存储在MySQL。这些工具的安装和配置网上资料很多我就不展开了重点提醒几个容易踩的坑。Maven一定要配阿里云仓库否则第一次构建项目时下载依赖可能要等半小时以上。Node.js版本不要太新有些老项目的依赖对Node版本有要求我自己遇到过Node 18下构建报错、退回Node 16就正常的情况。JDK环境变量配置好之后命令行执行java -version必须能看到正确的版本信息很多IDE启动失败都是环境变量配置不正确导致的。2.3 网络规划与域名配置MaxKey和JeecgBoot之间的交互都是HTTP请求需要保证两个服务之间网络是通的。生产环境建议用内网域名通信避免走公网。同时MaxKey的回调地址需要配置成JeecgBoot实际可访问的地址如果配置了Nginx反向代理要注意回调地址是外网访问地址还是内网地址这个不一致会导致授权码回调失败。我在本地实验时习惯在hosts文件里配置两个虚拟域名比如maxkey.local指向127.0.0.1jeecg.local也指向127.0.0.1。这样配置的好处是后续调整端口或部署环境时不用反复修改代码里的回调地址。3. MaxKey端配置注册应用与用户体系MaxKey部署完成之后通过浏览器访问http://maxkey.local:9527默认管理员账号是admin初始密码需要参考官方文档首次登录会要求修改密码。登录进入管理控制台后核心需要配置两块一个是接入应用的信息一个是用户和权限体系。3.1 在MaxKey中注册客户端应用在MaxKey管理控制台的“应用管理”中新增一个应用。这里有几个关键参数应用名称建议写“JeecgBoot”方便后续维护时辨认。应用协议选OAuth2.0。回调地址这是最重要的参数。它必须是JeecgBoot后端实际处理OAuth2回调的接口地址。比如JeecgBoot部署在http://jeecg.local:8080回调地址就是http://jeecg.local:8080/jeecg-boot/sso/callback。如果这个地址配错了MaxKey虽然会生成授权码但回调请求发不到正确的位置整个登录流程就断了。授权范围一般选择read或all。范围决定了访问令牌能获取哪些用户信息端点。我们最终需要获取用户的基本信息所以选all比较省事。配置完成后MaxKey会生成一个Client ID和Client Secret。这两个值要妥善保管Client ID相当于应用的身份证号Client Secret是应用与MaxKey通信的密钥。特别提醒Client Secret不要打包进前端代码也不要提交到Git仓库一旦泄露任何人都可以伪造你的应用去向MaxKey申请令牌。在实际项目里我们会把它配置在后端的环境变量或配置中心。3.2 MaxKey用户体系与JeecgBoot的映射关系MaxKey用户体系里有两个核心概念用户User和组织机构Organization。在对接JeecgBoot时业务上通常只需要同步用户信息不一定要同步组织机构。因为JeecgBoot本身有自己的一套组织架构和角色权限体系强行走组织机构同步反而会造成两边数据结构不一致维护起来很麻烦。在MaxKey里创建一个测试用户比如用户名是zhangsan填写真实姓名、邮箱、手机号。这些字段后续会作为属性传递给JeecgBoot。这里要注意一个设计问题JeecgBoot本地的用户表中用户名username字段必须是唯一的。而MaxKey里用户名也是唯一的。所以最自然的映射关系是MaxKey的username直接对应JeecgBoot的username。但如果MaxKey的用户名格式和JeecgBoot本地用户的用户名格式不一致比如MaxKey用工号JeecgBoot用手机号那就需要在JeecgBoot侧做一次映射转换。这种映射逻辑最好实现在独立的适配层不要散落在业务代码里。后续如果调整映射规则只改一处就可以了。3.3 MaxKey的应用权限与访问策略MaxKey支持为应用配置访问策略比如只允许某些用户或某些用户组访问这个应用。这个功能在企业场景下非常实用比如只允许IT部门的用户通过单点登录访问JeecgBoot其他用户即使有MaxKey账号也不能访问JeecgBoot。配置逻辑也很简单在应用详情里选择“访问控制”指定允许访问的用户组即可。默认情况下是允许所有合法用户访问如果不需要做限制这步可以跳过。4. JeecgBoot端集成OAuth2客户端封装JeecgBoot端的集成是整个项目的关键。它的核心工作包括引入OAuth2客户端依赖、配置认证服务器参数、实现回调接口、将MaxKey用户信息转换为本地会话。4.1 在JeecgBoot中配置OAuth2客户端参数JeecgBoot的后端是一个标准的Spring Boot工程。我们在pom.xml中引入Spring Security OAuth2 Client依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-oauth2-client/artifactId /dependency然后在application.yml中配置OAuth2客户端相关信息spring: security: oauth2: client: registration: maxkey: client-id: your-client-id client-secret: your-client-secret authorization-grant-type: authorization_code redirect-uri: {baseUrl}/login/oauth2/code/maxkey scope: all provider: maxkey: authorization-uri: http://maxkey.local:9527/maxkey/authz/authorize token-uri: http://maxkey.local:9527/maxkey/authz/token user-info-uri: http://maxkey.local:9527/maxkey/api/oauth/v1/me user-name-attribute: username这里需要解释几个参数的含义。authorization-grant-type指定了授权模式我们用的是authorization_code即授权码模式。redirect-uri是Spring Security自动生成的回调地址{baseUrl}会自动替换成当前JeecgBoot服务的实际地址。authorization-uri、token-uri、user-info-uri是MaxKey暴露的端点地址这三个地址必须和MaxKey实际部署的地址完全一致任何路径拼写错误都会导致流程中断。user-name-attribute指定了哪个字段作为用户的唯一标识对应MaxKey用户信息返回JSON中的username字段。这个字段会用于后续查找本地用户。4.2 实现回调接口与令牌处理Spring Security OAuth2 Client依赖已经帮我们处理了大部分底层逻辑包括授权码的接收、令牌的换取。但这些逻辑是框架自动完成的如果我们要在登录成功后执行一些自定义逻辑比如在本地用户表中查找或创建用户就需要覆写相关处理器。一个比较简洁的做法是自定义OAuth2UserService。当MaxKey返回用户信息后框架会调用这个Service来加载用户。我们在加载用户时同步在本地创建会话。Service public class MaxKeyOAuth2UserService extends DefaultOAuth2UserService { Autowired private SysUserService sysUserService; Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { OAuth2User oAuth2User super.loadUser(userRequest); String username oAuth2User.getAttribute(username); String email oAuth2User.getAttribute(email); String displayName oAuth2User.getAttribute(displayName); // 检查本地用户是否存在不存在则创建 SysUser localUser sysUserService.getUserByName(username); if (localUser null) { localUser new SysUser(); localUser.setUsername(username); localUser.setEmail(email); localUser.setRealname(displayName); sysUserService.saveUser(localUser); } return oAuth2User; } }这段代码的逻辑是先从MaxKey返回的OAuth2User对象中提取用户属性然后调用本地用户服务查找用户。如果本地没有这个用户就自动创建实现“首次登录即自动开通账号”的效果。这里有一个业务决策需要注意新创建的用户应该赋予什么角色和权限默认情况下我给的是普通用户角色后续由管理员手动分配权限。企业里也可以做成自动映射比如根据MaxKey的用户组动态分配JeecgBoot的角色但这需要额外开发且要考虑安全边界。4.3 单点登录回调URL与OAuth2授权码流程虽然Spring Security帮我们处理了大部分逻辑但理解整个授权码流程对排查问题非常有帮助。特别是在对接过程中我们需要查看日志、比对请求参数如果脑子里没有完整的流程模型定位问题会非常吃力。完整流程如下第一步用户访问JeecgBoot任意受保护页面JeecgBoot检测到未登录Spring Security将请求重定向到authorization-uri同时携带参数response_typecode、client_idyour-client-id、redirect_uri...。第二步MaxKey展示统一登录页面用户输入用户名密码。认证通过后MaxKey生成一个授权码code并重定向到redirect_uri。第三步JeecgBoot后端接收到携带code的回调请求Spring Security自动使用这个code向token-uri发起POST请求换取access_token。第四步获取到access_token后Spring Security调用user-info-uri获取用户信息。第五步用户信息返回后OAuth2UserService将用户信息与本地用户绑定创建本地会话。整个流程涉及两次HTTP重定向和两次后端HTTP调用。我在排查问题时习惯于在后端日志里跟踪这几个关键节点的请求参数和响应结果是否有code参数到达回调接口换取token的POST请求是否成功返回的是200还是400用户信息接口返回的字段名是否与代码中取属性名一致4.4 实现JeecgBoot本地会话与MaxKey的对接用户认证成功之后JeecgBoot需要建立自己的一套会话机制。因为前端和业务系统内部并不直接依赖MaxKey的token而是依赖JeecgBoot自身的session或JWT。一种合理的做法是在OAuth2UserService中完成本地用户查找或创建后再通过JeecgBoot自己的登录状态管理工具生成一个本地的登录令牌返回给前端。这样业务系统内部的权限控制逻辑不需要做任何修改仍然是基于JeecgBoot的认证体系。这里有一个细节值得注意如果JeecgBoot使用的是JWT模式本地的JWT中需要包含用户ID、用户名、角色等常用信息。这个JWT的有效期可以由我们自行控制比如设为8小时或24小时。MaxKey发出来的access_token的有效期一般比较短比如默认5分钟或30分钟它只用于与MaxKey之间的通信不能直接作为JeecgBoot的业务令牌。5. 账户同步机制从MaxKey到JeecgBoot单点登录解决了“登录一次、到处使用”的问题但还有一个前置问题没有解决JeecgBoot本地用户表里的账号从哪里来如果每次都是第一个人登录时自动创建确实能解决新账号的问题但无法解决“离职账号回收”和“用户信息变更”的问题。所以我们需要一个账户同步机制。5.1 账户同步的常见方案对比账户同步有几种常见的实现思路我分别说说利弊方案一实时同步。在MaxKey里增加用户时通过消息队列或Webhook实时推送用户信息到JeecgBoot。优点是及时性好用户新增后马上就可以登录。缺点是需要修改MaxKey的代码或配置集成成本高。方案二定时同步。JeecgBoot写一个定时任务每隔一定时间比如5分钟调用MaxKey的用户管理API拉取全量用户或增量用户然后更新本地用户表。优点是实现简单不侵入MaxKey。缺点是有时延用户新增后最多要等一个同步周期才能登录。方案三SCIM协议同步。SCIM是专门用于身份管理的标准协议MaxKey支持SCIM 2.0JeecgBoot如果也能支持这个协议两边可以无缝对接。但实际上大多数业务系统不会直接实现SCIM协议所以这个方案更多是理想化的落地成本不低。在实际项目中我选择的是定时同步实时创建的混合策略用户第一次通过单点登录访问时实时创建本地账号后续用户信息的变更比如手机号、邮箱、姓名通过定时任务从MaxKey批量同步更新。这种策略兼顾了体验和实现复杂度。5.2 通过MaxKey API实现批量用户同步MaxKey提供了用户管理相关的API可以用来批量拉取用户列表。比如获取用户列表的接口大致是GET /maxkey/api/users Authorization: Bearer {access_token}返回的用户信息包括用户名、姓名、邮箱、手机号、部门、状态等。我们可以在JeecgBoot里写一个定时任务调用这个接口将用户列表与本地用户表做比对Component public class MaxKeyUserSyncTask { Autowired private SysUserService sysUserService; Scheduled(cron 0 */5 * * * ?) // 每5分钟执行一次 public void syncUsers() { ListMaxKeyUser remoteUsers maxKeyApiClient.getAllUsers(); for (MaxKeyUser remoteUser : remoteUsers) { SysUser localUser sysUserService.getUserByName(remoteUser.getUsername()); if (localUser null) { // 本地不存在创建新用户 createLocalUser(remoteUser); } else { // 本地存在更新用户信息 updateLocalUser(localUser, remoteUser); } } } }这个任务的核心逻辑是“比对差异、增量更新”。注意不要简单粗暴地删除本地不存在的用户因为JeecgBoot本地用户可能关联了业务数据比如某个用户发起了一个审批流程如果直接删除账号会导致流程的历史数据关联不上。正确的做法是把本地用户状态标记为“已停用”而不是物理删除。5.3 用户状态同步与离职账号处理账户同步最容易忽略的是用户状态字段。MaxKey用户有“启用/禁用”的概念JeecgBoot用户也有。当某员工离职后管理员在MaxKey里禁用该账号那么JeecgBoot本地对应的账号也应该被禁用。处理逻辑如下private void syncUserStatus(MaxKeyUser remoteUser, SysUser localUser) { if (disabled.equals(remoteUser.getStatus())) { localUser.setStatus(2); // JeecgBoot中2表示禁用 } else { localUser.setStatus(1); } sysUserService.updateById(localUser); }这个功能非常重要。如果不做状态同步离职人员的账号在JeecgBoot里依然是启用的他可以通过直接访问JeecgBoot的登录页面如果本地密码没有失效进入系统这会造成数据泄露的安全隐患。所以账号生命周期管理是账户同步里不可缺失的一环。5.4 账户同步的日志与监控账户同步任务看似简单但执行频率高、涉及全量数据一旦出问题影响面很大。比如MaxKey接口突然超时定时任务抛出异常如果吞掉了异常会导致当天所有用户变更没有同步。所以一定要记录同步日志。我一般会记录三个维度任务执行记录开始时间、结束时间、同步数量、异常记录哪些用户同步失败、失败原因、变更记录哪个用户新增了、哪个用户被禁用了。后续如果出现数据不一致的问题通过日志能快速定位是哪一次的同步异常导致的。6. 常见问题与排查技巧实录无论配置文档写得多详细实际对接过程中总会遇到各种问题。下面这些是我在MaxKeyJeecgBoot对接过程中真实踩过的坑整理成速查表希望对你有帮助。6.1 授权码回调404或redirect_uri不匹配这个问题出现频率最高。原因通常是MaxKey应用配置的回调地址与JeecgBoot实际接收回调的地址不一致。注意检查几个点MaxKey应用配置里填写的回调地址是否和JeecgBoot的redirect-uri参数完全一致包括协议、域名、端口、路径必须是精确匹配。如果JeecgBoot配置了Nginx反向代理回调地址是外网地址还是内网地址可能两边看到的是不同的地址需要统一。Spring Security的redirect-uri模板里如果写了{baseUrl}要看它最终解析成的实际地址是什么。排查方法在后端日志中搜索redirect_uri把MaxKey回调请求里携带的redirect_uri参数值和MaxKey应用配置里的回调地址做对比逐字符比对。6.2 用户信息端点返回字段与代码不匹配OAuth2用户信息接口返回的字段名是MaxKey定义的不同版本的MaxKey可能字段名有差异。比如有的版本用username有的版本用preferred_username还有的版本返回displayName和nickname两个不同字段。我的建议是在写代码之前先用Postman模拟一次完整的OAuth2流程拿到用户信息接口的真实返回JSON对照着字段名来写属性映射代码。不要想当然地根据文档写文档经常和实际返回不一致。另外如果用户信息接口返回的是嵌套JSON结构比如{user: {username: zhangsan}}那么属性解析要相应地写成oAuth2User.getAttribute(user)再转成Map再取里面的username字段。6.3 授权码换token失败或token无效授权码换token失败最常见的两个原因一个是client_id和client_secret配置错误一个是授权码已经过期。授权码的有效期通常很短一般是几分钟如果JeecgBoot处理回调逻辑耗时过长比如回调里做了大量数据库操作可能授权码已经失效了。排查时先看MaxKey的日志确认token端点是否收到了请求。再看响应内容如果是invalid_grant说明授权码无效或已过期重新走一遍登录流程就好。如果是invalid_client检查client配置。还有一点容易忽略JeecgBoot发起换取token的服务器IP和时间要在MaxKey允许的范围内。MaxKey可能配置了IP白名单如果JeecgBoot的服务器IP不在白名单内即使client配置正确也会被拒绝。6.4 登录成功后本地用户不存在或权限异常如果OAuth2流程已经走通但登录后用户无法访问JeecgBoot的业务页面优先检查本地用户的角色和权限是否配置正确。在OAuth2UserService中自动创建的用户默认可能没有分配任何角色。需要在用户创建的逻辑中为新用户指定一个默认角色。比如localUser.setRoles(Collections.singletonList(ROLE_USER));JeecgBoot的权限模型比较灵活控制到按钮级别。如果用户登录后页面菜单为空大概率是角色没有关联菜单权限。可以先用管理员账号登录后台给这个新用户手工分配一下权限验证是不是权限分配的问题。6.5 高频排查点速查问题现象可能原因排查优先级点击登录后页面跳转到MaxKey再跳回来出现404回调地址不一致高MaxKey登录成功后报错“授权码无效”client配置错误或授权码过期高登录成功但页面提示“无权限”本地用户未分配角色中用户能登录但用户信息是空的user-info-uri返回的字段名不匹配中定时同步任务执行失败MaxKey接口超时或token过期中同步后用户状态未更新状态字段映射逻辑缺失低7. 经验总结与扩展建议整个MaxKey和JeecgBoot的对接做下来我个人最大的体会是单点登录这类企业级集成技术上并没有特别高不可攀的门槛真正的难点在于对接前的架构思考和对细节的耐心。几个建议分享给大家第一先定协议再写代码。OAuth2、OIDC、CAS这几个协议各有适合的场景不要一上来就看代码。先把协议理解清楚再开始动手能少走很多弯路。第二一切以真实接口返回为准。不要完全信任文档文档和实际版本之间会有偏差。对接过程中多用Postman或curl模拟请求看看真实返回的数据长什么样。第三预留扩展的可能。比如这次接的是JeecgBoot但下次可能接的是泛微OA再下次可能是金蝶系统。所以MaxKey端配置的应用信息要分类管理JeecgBoot端的OAuth2配置要独立封装不要和业务代码耦合在一起。这个项目如果继续往下扩展可以往几个方向走一是增加多因素认证比如MaxKey开启短信验证码或TOTP动态口令二是把MaxKey的审计日志接入公司的日志平台方便安全审计三是在JeecgBoot里增加一个“单点登录用户自助绑定”功能让用户首次登录时可以选择绑定已有的本地账号而不是自动创建新账号。最后再分享一个小细节。在实际部署时MaxKey的访问一定要全站HTTPS因为授权码和令牌都是通过URL参数和请求头传递的如果走明文HTTP令牌被截获的风险非常大。生产环境不要图省事跳过这一步这是身份认证系统的基本安全底线。