RuoYi-Cloud 免登录与页面内嵌实现文档
概述
本项目实现了两个核心功能:
- RuoYi-Cloud 免登录(SSO):通过 Token 方式跳过登录界面,自动获取系统访问凭证
- RuoYi-Vue 内嵌 RuoYi-Cloud 页面:通过 iframe 将 RuoYi-Cloud 的视频直播页面嵌入到 RuoYi-Vue 系统中
一、前端实现(RuoYi-Cloud)
1.1 内嵌页面改造
原始视频直播页面
原始页面为完整的实时视频直播页,包含设备树(DeviceTree)、EasyPlayer 播放器、分屏控制、云台操作等功能。
vue
<template>
<div class="app-container">
<el-row :gutter="20">
<splitpanes class="default-theme">
<pane size="20">
<el-col>
<DeviceTree @clickEvent="clickEvent" :isContextmenu="false"></DeviceTree>
</el-col>
</pane>
<pane size="80">
<el-col>
<div id="live" class="live-container">
<div v-loading="loading" class="live-content" element-loading-text="拼命加载中">
<div class="video-container">
<div class="control-bar">
<!-- 分屏选择、清空、保存布局、恢复布局、全屏等工具栏 -->
</div>
<div class="player-container">
<div ref="playBox" class="play-grid" :style="liveStyle">
<!-- EasyPlayer 播放器循环渲染 -->
</div>
</div>
</div>
</div>
</div>
</el-col>
</pane>
</splitpanes>
</el-row>
</div>
</template>
新建内嵌专用页面
改造要点:
- 外层增加
.embed-page容器,设置height: 100vh; width: 100% - 重置
html, body, #app的margin: 0; padding: 0; height: 100% - 其余 DOM 结构、脚本逻辑、样式保持不变
vue
<template>
<div class="embed-page">
<div class="app-container">
<el-row :gutter="20">
<!-- 与原始页面完全相同的 splitpanes + DeviceTree + 播放器结构 -->
</el-row>
</div>
</div>
</template>
<style scoped lang="scss">
html, body, #app { margin: 0; padding: 0; height: 100%; }
.embed-page { height: 100vh; width: 100%; }
/* 其余样式与原始页面一致 */
</style>
1.2 登录页改造
原始登录页
完整的登录页面,包含账号、密码、验证码输入框,以及炫酷的科技感动画背景(网格地面、扫描线、信号点、REC 录制指示、十字准星等)。
新建 SSO 专用登录页(fpjklogin.vue)
核心思路 :保留原登录页的脚本逻辑和样式,但将模板内容清空,并新增 getLoginByNameAndTokenJ() 方法处理 SSO 登录。
vue
<template>
<div class="login">
<!-- 模板内容留空,仅作占位 -->
</div>
</template>
<script setup lang="ts">
import { getCodeImg } from "@/api/login"
import Cookies from "js-cookie"
import { encrypt, decrypt } from "@/utils/jsencrypt"
import useUserStore from '@/store/modules/user'
import usePermissionStore from '@/store/modules/permission'
import { isHttp } from '@/utils/validate'
import CryptoJS from 'crypto-js'
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const route = useRoute()
const router = useRouter()
/**
* SSO 单点登录核心方法
* 从 URL 中读取 accessToken 参数,格式:用户名$加密后的密码
*/
function getLoginByNameAndTokenJ(): void {
// 1. 获取地址栏中的 token
const accessToken = route.query.accessToken as string
// 2. 校验 token 是否存在
if (!accessToken) {
// 没有 token,显示普通登录表单
return
}
// 3. 开始处理 SSO 登录
loading.value = true
try {
// 4. 解析 token 并解密密码
const parts = accessToken.split("$")
if (parts.length < 2) {
throw new Error("AccessToken 格式错误")
}
const passwordPart = parts[1].replace(/ /g, '+') // 替换空格为 +
// 使用 AES 解密
let bytes = CryptoJS.AES.decrypt(passwordPart, 'secret_key_123')
let decryptedPassword = bytes.toString(CryptoJS.enc.Utf8)
// 构造登录信息
const logininfo = {
accessToken: parts[0] + "$" + decryptedPassword
}
// 5. 调用 SSO 登录接口
userStore.ssoLogin(logininfo)
.then(() => {
// 6. 获取用户信息
return userStore.getInfo()
})
.then(() => {
// 7. 生成动态路由
return usePermissionStore().generateRoutes()
})
.then((accessRoutes: any[]) => {
// 8. 添加动态路由
accessRoutes.forEach((route: any) => {
if (!isHttp(route.path) && !router.hasRoute(route.name)) {
router.addRoute(route)
}
})
// 等待 addRoute 完成
return new Promise<void>((resolve) => {
setTimeout(resolve, 100)
})
})
.then(() => {
// 9. 跳转至内嵌页面
loading.value = false
router.push({ path: "/embed" })
})
.catch((err: any) => {
console.error("SSO Login Error", err)
loading.value = false
})
} catch (error) {
console.error("SSO Decryption Error", error)
loading.value = false
}
}
// 页面加载时执行
getCode()
getCookie()
getLoginByNameAndTokenJ()
</script>
SSO 登录流程:
- 从 URL
?accessToken=xxx中读取加密的 Token - 解析 Token(格式:
用户名$加密密码) - 使用 AES 算法(密钥
secret_key_123)解密密码 - 调用后端
ssologin接口换取系统 Token - 获取用户信息、生成动态路由
- 跳转至
/embed嵌入页面
1.3 路由配置
新建独立的 embedRoutes 数组,与系统原有菜单路由隔离:
javascript
import { createWebHistory, createRouter } from 'vue-router'
import Layout from '@/layout/index.vue'
import BlankLayout from '@/layout/embed/BlankLayout.vue'
// 公共路由
export const constantRoutes = [
// ... 原有路由
{
path: '/login',
component: () => import('@/views/login.vue'),
hidden: true
},
{
path: '/fpjklogin', // 新增 SSO 登录入口
component: () => import('@/views/fpjklogin.vue'),
hidden: true
},
// ... 其他路由
]
// 外部嵌入专用路由,独立数组
export const embedRoutes = [
{
path: '/embed',
component: BlankLayout, // 空白布局,不走默认 layout
redirect: '/embed/demo',
children: [
{
path: 'demo',
name: 'EmbedDemo',
component: () => import('@/views/qs/embed/demo.vue'),
meta: {
title: '外部嵌入页面',
hidden: true,
isEmbed: true // 自定义标记,用于权限判断
}
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes: [...constantRoutes, ...embedRoutes],
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition
return { top: 0 }
},
})
export default router
关键点:
- 使用
BlankLayout(空白布局)替代默认的Layout,避免显示侧边栏、顶部导航等 embedRoutes与constantRoutes合并加载
1.4 用户 Store(SSO 登录接口封装)
在 useUserStore 中新增 ssoLogin action:
javascript
import { login, logout, getInfo, ssologin } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
const useUserStore = defineStore('user', {
state: (): UserState => ({
token: getToken(),
id: '',
name: '',
nickName: '',
avatar: '',
roles: [],
permissions: []
}),
actions: {
// 普通登录
login(userInfo) {
return new Promise<void>((resolve, reject) => {
login(userInfo.username, userInfo.password, userInfo.code, userInfo.uuid).then(res => {
setToken(res.data.access_token)
this.token = res.data.access_token
resolve()
}).catch(error => reject(error))
})
},
// 新增:SSO 免登录
ssoLogin(loginInfo: { accessToken: string }) {
return new Promise<void>((resolve, reject) => {
ssologin(loginInfo).then((res: any) => {
const data = res.data
if (data && data.access_token) {
setToken(data.access_token)
this.token = data.access_token
resolve()
} else {
const token = res.token || data?.token
if (token) {
setToken(token)
this.token = token
resolve()
} else {
reject(new Error('SSO Login failed: No token received'))
}
}
}).catch((error: any) => reject(error))
})
},
// 获取用户信息
getInfo() {
// ... 原有逻辑
},
// 退出系统
logOut() {
// ... 原有逻辑
}
}
})
export default useUserStore
二、后端实现(RuoYi-Cloud)
2.1 网关白名单配置
在 Nacos 的 ruoyi-gateway-dev.yml 中新增 SSO 接口白名单:
yaml
# 不校验白名单
ignore:
whites:
- /auth/logout
- /auth/login
- /auth/ssologin # 新增 SSO 免登接口放行
- /auth/register
- /*/v2/api-docs
- /*/v3/api-docs
- /csrf
- /zlm/index/hook/**
- /zlm/cloudRecord/download/zip
2.2 SSO 登录控制器
在 AuthController 中新增 ssologin 接口:
java
@PostMapping("login")
public R<?> login(@RequestBody LoginBody form) {
// 用户登录
LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
// 获取登录 token
return R.ok(tokenService.createToken(userInfo));
}
/**
* SSO 免登录接口
*/
@PostMapping("ssologin")
public R<?> ssoLogin(@Valid @RequestBody SsoLoginDTO ssoLoginDTO) {
// 调用 SSO 登录服务,生成系统 token
LoginUser loginUser = ssoLoginService.ssoLogin(ssoLoginDTO.getAccessToken());
return R.ok(tokenService.createToken(loginUser));
}
2.3 SSO 登录服务
创建 SsoLoginService 处理 SSO 登录逻辑:
java
package com.ruoyi.auth.service;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.system.api.RemoteUserService;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.model.LoginUser;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class SsoLoginService {
@Resource
private TokenService tokenService;
@Autowired
private RemoteUserService remoteUserService;
/**
* SSO 登录核心逻辑
* @param accessToken 前端解密后的 token(格式:用户名$密码/凭证)
* @return 系统内有效的 access_token
*/
public LoginUser ssoLogin(String accessToken) {
// 1. 解析 accessToken(与前端约定格式:用户标识$凭证)
String[] tokenParts = accessToken.split("\\$");
String username = tokenParts[0]; // 提取用户名
// 2. 查询用户信息
R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER);
if (!R.isSuccess(userResult) || userResult.getData() == null) {
throw new ServiceException("未查询到该用户信息");
}
// 3. 返回用户信息(由 Controller 生成 token)
return userResult.getData();
}
}
三、RuoYi-Vue 内嵌实现
3.1 内嵌页面
在 RuoYi-Vue 项目中新建页面,通过 iframe 加载 RuoYi-Cloud 的 SSO 登录地址:
vue
<template>
<div class="app-container" style="padding:15px">
<iframe
id="test"
:src="url"
style="width:100%; height:800px; overflow:auto;"
></iframe>
</div>
</template>
<script>
import { getToken } from '@/utils/auth'
export default {
name: "Fpjk",
data() {
return {
url: ""
}
},
created() {
// 拼接 RuoYi-Cloud 的 SSO 登录地址
// 格式:用户名$密码(密码已在前端通过 AES 加密)
this.url = 'http://localhost:83/fpjklogin?accessToken=admin$admin123'
},
mounted() {}
}
</script>
关键点:
- iframe 的
src指向 RuoYi-Cloud 的/fpjklogin页面 - URL 参数
accessToken携带用户名和加密后的密码 - 实际生产环境中,密码应通过 AES 加密后再传递
四、整体实现流程
┌─────────────────────────────────────────────────────────────┐
│ 1. RuoYi-Vue 系统加载内嵌页面 │
│ ↓ │
│ 2. iframe 加载 http://localhost:83/fpjklogin?accessToken=...│
│ ↓ │
│ 3. RuoYi-Cloud 的 fpjklogin 页面解析 accessToken │
│ ↓ │
│ 4. 前端使用 AES 解密密码(密钥:secret_key_123) │
│ ↓ │
│ 5. 调用后端 /auth/ssologin 接口 │
│ ↓ │
│ 6. 后端 SsoLoginService 根据用户名查询用户信息 │
│ ↓ │
│ 7. 后端生成系统 access_token 并返回 │
│ ↓ │
│ 8. 前端保存 token,调用 getInfo 获取用户信息 │
│ ↓ │
│ 9. 生成动态路由,跳转至 /embed/demo 内嵌页面 │
│ ↓ │
│ 10. iframe 内显示完整的视频直播页面 │
└─────────────────────────────────────────────────────────────┘
五、关键配置总结
| 配置项 | 位置 | 说明 |
|---|---|---|
| SSO 登录页 | RuoYi-Cloud/.../views/fpjklogin.vue |
处理 Token 解析和登录逻辑 |
| 内嵌直播页 | RuoYi-Cloud/.../views/qs/embed/demo.vue |
包装 .embed-page 容器 |
| 嵌入路由 | RuoYi-Cloud/.../router/index.js |
embedRoutes 独立数组 |
| 空白布局 | RuoYi-Cloud/.../layout/embed/BlankLayout.vue |
不显示侧边栏和导航 |
| SSO 接口 | RuoYi-Cloud/.../controller/AuthController.java |
@PostMapping("ssologin") |
| SSO 服务 | RuoYi-Cloud/.../service/SsoLoginService.java |
根据用户名查询用户信息 |
| 网关白名单 | Nacos ruoyi-gateway-dev.yml |
/auth/ssologin 放行 |
| User Store | RuoYi-Cloud/.../store/modules/user.js |
新增 ssoLogin action |
| 内嵌入口 | RuoYi-Vue/.../views/.../Fpjk.vue |
iframe 加载 RuoYi-Cloud |
六、注意事项
- Token 安全性 :生产环境中,
accessToken应使用强加密算法(如 AES-256),并通过 HTTPS 传输 - Token 过期处理:需要处理 SSO Token 过期后的重新登录逻辑
- 跨域问题:iframe 嵌入需确保 RuoYi-Cloud 配置了正确的 CORS 策略
- 用户权限 :SSO 登录后仍需调用
getInfo和generateRoutes确保权限路由正常加载 - 密钥管理 :
secret_key_123仅为演示密钥,实际项目应使用配置文件或密钥管理服务
七、扩展建议
- 支持多种 Token 格式(JWT、OAuth2 等)
- 增加 Token 签名验证机制,防止伪造
- 实现统一的 SSO 认证中心,支持多系统单点登录
- 优化 iframe 通信机制,支持父子页面数据交互