零信任平台接入芋道框架
1. 任务背景
由于要求,需要将我们自己基于yudao前后端开发的系统对接零信任平台。需要在尽可能不侵入我们自己的系统情况下完成零信任平台到我们系统的单点登录。
2. 零信任平台接入芋道框架流程图
3. 零信任平台接入芋道框架时序图
4. 代码改动
4.1. 前端核心改动(auth.ts、index.ts、user.ts、permission.ts)
前端的主要目标是:在应用加载时捕获外部令牌,使用该令牌向后端请求应用自身的Token,然后用应用Token完成登录并处理好多租户逻辑。
1. permission.ts
(路由守卫)
改动一: 新增 handleSsoRedirect()
函数。
- 原因: 浏览器中的JavaScript无法直接读取初始页面加载时的HTTP请求头。此函数通过读取URL参数 (
?userToken=...
) 的方式来捕获零信任令牌,并将其存入浏览器缓存,这是打通前后端的第一座桥梁。 - 改动二: 修改
router.beforeEach
的else
逻辑块。 - 原因: 当应用内没有Token时,不再是立即跳转到登录页,而是进入一个
try...catch
流程,优先尝试执行SSO登录 (userStore.ssoLogin()
)。如果SSO成功,则正常进入系统;如果SSO失败(例如没有外部令牌),则回退到原来的逻辑,跳转到登录页。
2. store/modules/user.ts
(Pinia用户状态)
- 改动一: 新增
ssoLogin()
action。
- 原因: 这是执行SSO登录的核心业务逻辑。它负责调用API接口 (
ssoLoginApi
),并在成功后接收后端返回的应用Token和租户ID,然后调用工具函数将它们存入缓存。 - 改动二: 修正
ssoLogin
方法中的数据处理。 - 原因: 最初的
setToken(res.data)
是错误的,因为框架的axios拦截器已经解包了响应,res
本身就是数据体。修正为setToken(res)
才不会导致TypeError
错误。 - 改動三: 在
ssoLogin
方法中增加setTenantId(res.tenantId)
调用。 - 原因: 为了解决若依Pro框架的多租户权限问题。后端在SSO成功后会返回用户所属的
tenantId
,前端必须将此ID存入缓存,以便后续所有API请求都能自动携带tenant-id
请求头。
3. api/login.ts
(API接口定义)
- 改动: 新增
ssoLoginApi()
函数。
- 原因: 定义一个专门用于发起SSO登录请求的API函数。它负责从缓存中读取
zeroTrustToken
,并将其作为请求头,发送给后端专门处理SSO的过滤器接口 (/admin-api/system/auth/sso-login
)。
4. utils/auth.ts
(认证工具函数)
- 改动一: 新增
getZeroTrustToken
,setZeroTrustToken
,removeZeroTrustToken
函数。
- 原因: 提供一套标准的函数来管理从URL捕获到的、临时的零信任令牌对象,实现其在浏览器缓存中的存、取、删。
- 改动二: 确保
setTenantId
函数被正确导出和导入。 - 原因: 以便
user.ts
中的ssoLogin
action可以调用它来存储租户ID。
4.2. 后端核心改动
增加一个零信任模块,对其他模块不产生影响
后端的总体目标是:提供一个专门的接口(由Filter实现)来接收外部令牌,验证它,完成用户映射/创建,并返回一个应用自身的合法会话(Token)。
5. 零信任单点登录流程简述
5.1. 前端触发与令牌传递
- 用户发起访问:用户通过包含
userToken
和appToken
参数的URL(如http://your-app/?userToken=xxx&appToken=xxx
)访问应用。 - Nginx代理与转发:Nginx接收到请求,将Token注入请求头(如
RZZX-USERTOKEN
),并代理至前端服务。 - 前端捕获Token:前端应用(如Vue)加载时,从URL参数中提取Token并存入本地(如LocalStorage)。
- 发起SSO登录请求:前端路由守卫检测到未登录状态,自动调用SSO登录接口,并将存储的Token通过请求头发送至后端。
5.2. 后端认证与用户处理
- 过滤器拦截与验证:后端定义一个专门的认证过滤器(如
ZeroTrustAuthenticationFilter
),拦截SSO登录请求(如/admin-api/system/auth/sso-login
),提取请求头中的Token。 - 与零信任平台交互:认证服务(如
ZeroTrustAuthService
)调用零信任平台的接口(通常是基于HTTP的REST API)来验证Token的有效性并获取用户唯一标识(如idcard
或pid
)。 - 本地用户映射与创建:
- 系统使用从零信任平台获取的用户标识,查询本地用户库。
- 如果用户存在,则更新其最新信息。
- 如果用户不存在,则根据业务规则在本地创建一个新用户账户,并为其分配一个默认的租户ID(如
tenantId=1
),这个过程确保了新用户能够立即在正确的租户上下文中工作。
- 创建本地会话:用户映射成功后,调用框架的令牌服务(如
oauth2TokenApi.createAccessToken
)为该用户创建本地会话(生成应用的Access Token和Refresh Token)。
5.3. 登录完成与后续访问
- 返回前端关键信息:后端将生成的本地Token、租户ID(
tenantId
)等信息返回给前端。 - 前端建立登录状态:前端接收到响应后,将本地Token和租户ID等信息存储起来,并更新应用状态为已登录,随后跳转到系统主页面。
- 正常访问与租户隔离:此后用户访问业务接口时,前端会在请求头(如
Authorization: Bearer <access_token>
和tenant-id: <tenantId>
)中自动附加Token和租户ID。后端的租户过滤器(如TenantSecurityWebFilter
)会据此识别用户身份并确保其只能访问所属租户的数据,实现数据隔离。
5.4. 统一单点登出
- 用户发起登出:用户在前端点击登出。
- 通知零信任平台:前端调用登出接口,后端不仅清除本地会话(如从Redis删除Token),还会调用零信任平台的令牌失效接口,使零信任侧的Token失效。
- 前端清理与重定向:前端清除本地存储的所有Token和状态,并可能重定向至零信任平台的登录页面,完成全局登出。
6. 零信任单点登录完整流程详解
整个流程可以分为七个主要阶段:
- 发起跳转:用户从外部系统被重定向到您的应用。
- Nginx 代理:Nginx作为网关,处理初始请求。
- 前端捕获与SSO触发:Vue应用加载,捕获令牌并发起SSO请求。
- 后端认证:核心认证逻辑,与零信任平台交互并创建本地会话。
- 前端会话建立:前端存储本地Token,完成登录。
- 登录后正常访问:用户以登录状态访问系统其他接口。
- 统一登出:用户登出,同时通知零信任平台。
6.1.1. 阶段一:发起跳转
- 动作: 外部系统(或我们的测试HTML页面)将用户重定向到您的应用网关地址,并在URL参数中携带零信任平台的用户令牌和应用令牌。
- 需求: 对应您的需求文档 第1条。
- 示例:
- 用户浏览器访问:
http://localhost:8000/?userToken=test-user-token-123456&appToken=test-app-token-abcdef
localhost:8000
是您的Nginx代理地址。
- 用户浏览器访问:
6.1.2. 阶段二:Nginx 代理
- 动作: Nginx监听到
8000
端口的请求,根据您的nginx.conf
配置进行处理。 - 代码依据:
nginx.conf
- Nginx
map $arg_userToken $user_token_header {default $arg_userToken;
}
map $arg_appToken $app_token_header {default $arg_appToken;
}server {listen 8000;# ...location / {proxy_pass http://127.0.0.1:83; # 代理到前端proxy_set_header RZZX-USERTOKEN $user_token_header;proxy_set_header RZZX-APPTOKEN $app_token_header;# ...}location /admin-api {proxy_pass http://127.0.0.1:48083; # 代理到后端}
}
- 流程:
- Nginx的
map
指令读取URL中的userToken
和appToken
参数。 proxy_pass
将请求转发给运行在83
端口的前端Vue应用。proxy_set_header
在这次请求中,将URL参数转换为了HTTP请求头RZZX-USERTOKEN
和RZZX-APPTOKEN
。(关键:这一步只是为了“传递”信息,前端JS本身无法直接读取请求头)
- Nginx的
6.1.3. 阶段三:前端捕获与SSO触发
- 动作: 浏览器加载前端应用,Vue Router开始工作。
- 需求: 对应您的需求文档 第2条 (通过URL参数间接实现)。
- 代码依据:
src/permission.ts
- 流程:
handleSsoRedirect()
函数首先执行,它通过new URLSearchParams(window.location.search)
从当前浏览器URL中捕获userToken
和appToken
。- 调用
setZeroTrustToken({ userToken, appToken })
(位于src/utils/auth.ts
),将这两个令牌存入浏览器的localStorage
中,以备后续使用。 router.beforeEach
路由守卫启动,getAccessToken()
返回false
(因为此时还没有应用自身的Token)。- 代码进入
try...catch
块,执行await userStore.ssoLogin()
。 userStore.ssoLogin()
(位于src/store/modules/user.ts
) 调用ssoLoginApi()
(位于src/api/login.ts
)。ssoLoginApi
函数从localStorage
中读出刚刚存入的zeroTrustTokens
,将它们作为请求头,向后端/admin-api/system/auth/sso-login
发起一个真正的SSO登录 POST 请求。
6.1.4. 阶段四:后端认证
- 动作: 后端接收到SSO登录请求,执行完整的认证、用户映射、会话创建流程。
- 需求: 对应您的需求文档 第3、4、5、6条。
- 代码依据:
ZeroTrustAuthenticationFilter.java
,ZeroTrustAuthServiceImpl.java
,ZeroTrustServiceImpl.java
- 流程:
- 过滤器拦截:
ZeroTrustAuthenticationFilter
拦截到/admin-api/system/auth/sso-login
请求,并从中提取RZZX-USERTOKEN
和RZZX-APPTOKEN
请求头。 - 服务调用: 过滤器调用
zeroTrustAuthService.authenticate(...)
。 - 与零信任平台交互 (
ZeroTrustAuthServiceImpl
->ZeroTrustServiceImpl
):
- 过滤器拦截:
- 验证令牌: 发起
POST /.../user/token/verify
请求到Mockoon,验证令牌有效性。 - 获取PID: 发起
POST /.../user/token/get
请求到Mockoon,获取用户pid
。 - 获取用户信息: 发起
POST /.../user/query
请求到Mockoon,使用pid
获取用户的idcard
等信息。
- 验证令牌: 发起
- 用户映射/创建 (
ZeroTrustAuthServiceImpl
):
- 用户映射/创建 (
- 使用
idcard
调用userMapper.selectByUsername(idcard)
在本地system_users
表中查找用户。 - 如果
localUser == null
,则调用createUser
方法,创建一个新用户,并设置默认的角色、部门以及租户ID (newUser.setTenantId(1L)
)。 - 如果用户已存在,则直接使用该用户信息。
- 使用
- 生成应用Token:
authenticate
方法返回AdminUserDO
对象给过滤器。 - 返回响应:
ZeroTrustAuthenticationFilter
调用oauth2TokenApi.createAccessToken(...)
为该用户生成一个若依框架自身的accessToken
和refreshToken
。然后,它构造一个包含accessToken
,refreshToken
,expiresTime
,zeroTrustToken
, 和tenantId
的Map
,并将其作为成功的JSON响应返回给前端。
- 生成应用Token:
6.1.5. 阶段五:前端会话建立
- 动作: 前端接收到后端成功的响应,存储应用Token,完成登录状态的建立。
- 代码依据:
src/store/modules/user.ts
,src/utils/auth.ts
- 流程:
ssoLoginApi()
的Promise
成功 resolve,ssoLogin
action 中的res
变量获得了后端返回的包含Token和租户ID的对象。setToken(res)
被调用,将accessToken
和refreshToken
存入localStorage
。setTenantId(res.tenantId)
被调用,将tenantId
存入localStorage
。userStore.ssoLogin()
执行成功,permission.ts
中的await
结束。- 路由守卫调用
next({ ...to, replace: true })
,再次进入导航流程。
6.1.6. 阶段六:登录后正常访问
- 动作: 用户以已登录状态,访问系统内的其他页面和接口。
- 流程:
- 路由守卫
permission.ts
再次执行。 - 这一次,
getAccessToken()
返回true
。 - 代码进入
if(getAccessToken())
的逻辑,开始加载用户信息、动态路由等,最终成功渲染出系统主页。 - 当用户操作页面,触发新的API请求(如获取字典数据)时,前端的
axios
请求拦截器会自动从localStorage
中读取accessToken
和tenantId
,并将它们添加到请求头中。 - 后端的
TokenAuthenticationFilter
和TenantSecurityWebFilter
会验证这些请求头,确保用户已登录且有权访问相应租户的数据。
- 路由守卫
6.1.7. 阶段七:统一登出
- 动作: 用户点击登出按钮,前端和后端同时清理会话,并通知零信任平台。
- 需求: 对应您的需求文档 第7条。
- 代码依据:
ZeroTrustAuthController.java
,src/api/login.ts
- 流程:
- 前端调用
userStore.loginOut()
。 loginOut()
调用logoutApi()
。logoutApi
从localStorage
中读取原始的零信任令牌 (zeroTrustToken
),并将其作为请求头发给后端的/admin-api/zerosystem/auth/logout
接口。- 后端的
ZeroTrustAuthController
的logout
方法被调用。 - 通知零信任平台: 调用
zeroTrustAuthService.offlineToken()
,向Mockoon发起POST /.../token/change
请求,使零信任令牌下线。 - 销毁本地会话: 调用
oauth2TokenService.removeAccessToken()
,从Redis中删除当前应用的accessToken
,使其失效。 - 前端在
logoutApi
调用成功后,执行removeToken()
,removeZeroTrustToken()
等方法,清理浏览器本地存储,并重置userStore
状态,最后跳转到登录页。
- 前端调用
至此,整个单点登录与登出的闭环流程全部完成。