企业微信单点登录(SSO)说明

企业微信单点登录(SSO)说明

本文档说明 RuoYi-Cloud 前端 login_sso.vue 及配套后端如何实现企业微信 OAuth 授权码单点登录,与账号密码登录的差异,以及接入与排查要点。


一、总体说明

项目 说明
SSO 类型 企业微信网页授权(OAuth2 授权码 code
前端入口 /loginSso?code={授权码}
后端接口 POST /auth/loginSso,请求体 { "code": "..." }
用户匹配 企业微信 userid ↔ 本地 sys_user.qywx_id
鉴权结果 与普通登录相同,返回 JWT(access_token),写入 Cookie Admin-Token

与普通登录的区别:

  • 不需要用户名、密码、图形验证码。
  • 网关 ValidateCodeFilter/auth/loginSso 直接放行,不做验证码校验。
  • 后端通过企业微信 API 用 code 换取用户身份,再按 qywx_id 查本地用户(不校验密码)。

典型使用场景: 用户在企业微信客户端或配置了可信域名的浏览器中打开应用,企业微信 OAuth 回调到 /loginSso?code=xxx,页面自动完成登录。


二、端到端流程

ruoyi-system Redis 企业微信 API ruoyi-auth ruoyi-gateway 前端 login_sso.vue 企业微信 ruoyi-system Redis 企业微信 API ruoyi-auth ruoyi-gateway 前端 login_sso.vue 企业微信 #mermaid-svg-e0TYSyKnt2hQImbq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-e0TYSyKnt2hQImbq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-e0TYSyKnt2hQImbq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-e0TYSyKnt2hQImbq .error-icon{fill:#552222;}#mermaid-svg-e0TYSyKnt2hQImbq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-e0TYSyKnt2hQImbq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-e0TYSyKnt2hQImbq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-e0TYSyKnt2hQImbq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-e0TYSyKnt2hQImbq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-e0TYSyKnt2hQImbq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-e0TYSyKnt2hQImbq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-e0TYSyKnt2hQImbq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-e0TYSyKnt2hQImbq .marker.cross{stroke:#333333;}#mermaid-svg-e0TYSyKnt2hQImbq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-e0TYSyKnt2hQImbq p{margin:0;}#mermaid-svg-e0TYSyKnt2hQImbq .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-e0TYSyKnt2hQImbq text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-e0TYSyKnt2hQImbq .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-e0TYSyKnt2hQImbq .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-e0TYSyKnt2hQImbq .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-e0TYSyKnt2hQImbq .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-e0TYSyKnt2hQImbq #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-e0TYSyKnt2hQImbq .sequenceNumber{fill:white;}#mermaid-svg-e0TYSyKnt2hQImbq #sequencenumber{fill:#333;}#mermaid-svg-e0TYSyKnt2hQImbq #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-e0TYSyKnt2hQImbq .messageText{fill:#333;stroke:none;}#mermaid-svg-e0TYSyKnt2hQImbq .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-e0TYSyKnt2hQImbq .labelText,#mermaid-svg-e0TYSyKnt2hQImbq .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-e0TYSyKnt2hQImbq .loopText,#mermaid-svg-e0TYSyKnt2hQImbq .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-e0TYSyKnt2hQImbq .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-e0TYSyKnt2hQImbq .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-e0TYSyKnt2hQImbq .noteText,#mermaid-svg-e0TYSyKnt2hQImbq .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-e0TYSyKnt2hQImbq .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-e0TYSyKnt2hQImbq .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-e0TYSyKnt2hQImbq .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-e0TYSyKnt2hQImbq .actorPopupMenu{position:absolute;}#mermaid-svg-e0TYSyKnt2hQImbq .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-e0TYSyKnt2hQImbq .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-e0TYSyKnt2hQImbq .actor-man circle,#mermaid-svg-e0TYSyKnt2hQImbq line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-e0TYSyKnt2hQImbq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} altToken 不存在 altcode 为空code 存在 302 跳转 /loginSso?code=xxxcreated() 调用 loginSso()跳转 /login(普通登录)POST /auth/loginSso { code }跳过验证码,转发请求读取 qywxTokenGET gettoken(corpid, corpsecret)access_token缓存 qywxTokenGET auth/getuserinfo(code)userid, user_ticketPOST auth/getuserdetail(user_ticket)userid, mobile 等Feign getByQywxId(userid)LoginUser(角色、权限)tokenService.createToken(){ access_token, expires_in }setToken + 跳转 / 或 redirect路由守卫 GetInfo + GenerateRoutes


三、前端逻辑

3.1 路由与白名单

配置 位置 作用
路由 /loginSso ruoyi-ui/src/router/index.js 加载 login_sso.vuehidden: true
免登录白名单 ruoyi-ui/src/permission.js ['/login', '/register', '/loginSso'],无 Token 也可访问
聊天嵌入排除 ruoyi-ui/src/App.vue SSO 页不加载第三方客服嵌入脚本

3.2 login_sso.vue 核心流程

页面无 UI(空模板),在 created() 生命周期立即执行 SSO:

  1. 从 URL 读取 this.$route.query.code
  2. 无 code$router.push('/login'),走普通登录。
  3. 有 code
    • this.$store.dispatch('LoginSso', { code })
    • 成功:$router.push(this.redirect || '/')
    • 失败:console.log 后跳转 /login

注释中提及「请通过企业微信客户端打开」;当前实现是无 code 时静默转普通登录页,未弹错误提示。

3.3 Vuex 与 API

Storeruoyi-ui/src/store/modules/user.jsLoginSso):

  • 调用 loginSso(code) API。
  • 将返回的 access_tokenexpires_in 写入 Cookie 与 Vuex(与普通 Login 一致)。

APIruoyi-ui/src/api/login.js):

javascript 复制代码
POST /auth/loginSso
headers: { isToken: false }
body: { code }

3.4 登录后的路由守卫

SSO 只负责拿到 Token。后续与普通登录相同(permission.js):

  1. 检测到 Cookie 中有 Token。
  2. 若尚未加载用户信息:GetInfoGenerateRoutes → 动态注册路由。
  3. 再进入目标页(默认首页 /)。

四、后端逻辑

4.1 入口:TokenController.loginSso

文件:ruoyi-auth/src/main/java/com/ruoyi/auth/controller/TokenController.java

步骤:

步骤 动作 企业微信 API
1 从 Redis 读 qywxToken;若无则请求 GET /cgi-bin/gettoken?corpid=&corpsecret=
2 用前端传入的 code 换用户票据 GET /cgi-bin/auth/getuserinfo?access_token=&code=
3 user_ticket 拉取用户详情 POST /cgi-bin/auth/getuserdetail
4 qywxUser.getUserid() 做本地登录 内部 sysLoginService.loginNoCaptcha(...)
5 签发 JWT tokenService.createToken(userInfo)

失败时返回 R.fail(...),例如:

  • 「系统未获取到 token,登录异常」
  • 「系统未获取到人员票据登录异常」
  • 「系统未获取到企业微信人员信息,登录异常」

4.2 本地用户校验:SysLoginService.loginNoCaptcha

文件:ruoyi-auth/src/main/java/com/ruoyi/auth/service/SysLoginService.java

与普通 login 相比去掉验证码和密码校验,保留:

  • IP 黑名单检查
  • 通过 Feign 调用 remoteUserService.getByQywxId(username, INNER)
  • 用户删除/停用状态校验
  • 登录成功日志

4.3 用户查询:SysUserController.getByQywxId

文件:ruoyi-modules/ruoyi-system/.../SysUserController.java

  • 内部接口:GET /user/info/getByQywxId/{qywxId}@InnerAuth
  • SQL:where u.qywx_id = #{qywxId}SysUserMapper.xml
  • 查不到用户:R.fail("未查询到指定账户,请联系管理员绑定手机号")
  • 查到后组装 LoginUser(用户、角色、permissions,含 ROLE_ACTIVITI_USER

4.4 网关验证码过滤

文件:ruoyi-gateway/.../ValidateCodeFilter.java

  • /auth/login/auth/register 需要验证码(若开启)。
  • /auth/loginSso 显式跳过 ,直接 chain.filter(exchange)

五、配置项

5.1 企业微信应用配置

文件:ruoyi-auth/src/main/resources/bootstrap.yml(生产环境通常在 Nacos 共享配置中覆盖)

yaml 复制代码
qywx:
  corp-id:           # 企业 ID
  application-list:
    - secret: <应用 Secret>
      agentId: 
      application-name: 
      application-desc: 
  url: https://qyapi.weixin.qq.com
  • corp-id + application-list[0].secret 用于获取 access_token
  • agentId 用于企业微信管理后台配置「网页授权回调域 / 应用主页」等(需与前端部署域名一致)。

5.2 企业微信管理后台(需运维配置)

  1. 在对应自建应用下配置可信域名OAuth 回调地址
  2. 回调地址应指向前端:https://{域名}/loginSso(企业微信会在其后追加 ?code=)。
  3. 用户须从企业微信内打开或满足 OAuth 环境,否则拿不到有效 code

5.3 本地用户绑定

sys_user 表需维护 qywx_id 字段,值与企业微信成员 userid 一致。未绑定则 SSO 失败,需管理员在系统中维护后再试。


六、涉及文件索引

层级 文件 职责
前端页面 ruoyi-ui/src/views/login_sso.vue 解析 code、触发 SSO、跳转
前端路由 ruoyi-ui/src/router/index.js 注册 /loginSso
前端守卫 ruoyi-ui/src/permission.js SSO 页白名单、登录后拉用户信息
前端状态 ruoyi-ui/src/store/modules/user.js LoginSso action
前端 API ruoyi-ui/src/api/login.js loginSso()
前端 Token ruoyi-ui/src/utils/auth.js Cookie Admin-Token
认证服务 ruoyi-auth/.../TokenController.java loginSso 接口
登录服务 ruoyi-auth/.../SysLoginService.java loginNoCaptcha
用户服务 ruoyi-system/.../SysUserController.java getByQywxId
用户 Mapper ruoyi-system/.../SysUserMapper.xml selectUserByQywxId
网关 ruoyi-gateway/.../ValidateCodeFilter.java SSO 跳过验证码
领域模型 ruoyi-auth/.../QywxLoginInfo.java, QywxUser.java 企业微信响应映射

七、异常与排查

现象 可能原因 排查方向
直接跳到 /login URL 无 code 是否从企业微信授权入口进入;回调 URL 是否配置为 /loginSso
「未查询到指定账户...」 sys_user.qywx_id 未绑定 核对企业微信 userid 与库中 qywx_id
「系统未获取到 token」 corp-id/secret 错误或网络不通 检查 bootstrap.yml/Nacos;手动调 gettoken
「人员票据/login 异常」 code 过期、重复使用或非 OAuth 环境 重新从企业微信入口打开;code 一次性有效
有 Token 但菜单空白 GetInfo 失败或角色未配置 /system/user/getInfo 与角色菜单权限
IP 黑名单 loginNoCaptcha 中 IP 校验 检查系统登录 IP 黑名单配置

八、实现注意点(维护参考)

  1. Redis 缓存 TTLTokenControllersetCacheObject("qywxToken", accessToken, 7200L, TimeUnit.MICROSECONDS) 使用的是微秒单位,7200 微秒远小于企业微信 token 有效期(通常 7200 秒)。若线上频繁请求 gettoken,应改为 TimeUnit.SECONDS 并与 QywxAccessTokenService(system 模块,约 7000 秒)对齐。
  2. 企业微信 API 错误码 :当前主要判断返回对象是否为 null,未统一校验 errcode。生产环境建议在接口层解析 errcode != 0 并记录 errmsg
  3. login_sso.vueredirect :data 里定义了 redirect 但未从 query 赋值;若需 SSO 后回到原页面,可参考 login.vuethis.$route.query.redirect 读取。
  4. 敏感配置corp-idsecret 不应提交到公开仓库;生产应放在 Nacos 等配置中心。

九、与普通登录对比简表

项目 普通登录 /login SSO /loginSso
凭证 用户名 + 密码 + 验证码 企业微信 OAuth code
接口 POST /auth/login POST /auth/loginSso
验证码 网关校验 跳过
用户识别 用户名 + 密码 企业微信 userid → qywx_id
Token 存储 Cookie Admin-Token 相同
登录后流程 GetInfo + 动态路由 相同

文档基于仓库当前代码整理;若 Nacos 或企业微信后台配置与本地 bootstrap.yml 不一致,以运行环境为准,实操可以参考https://mp.csdn.net/mp_blog/creation/editor/146337519