RuoYi-Cloud 免登录与页面内嵌实现

RuoYi-Cloud 免登录与页面内嵌实现文档

概述

本项目实现了两个核心功能:

  1. RuoYi-Cloud 免登录(SSO):通过 Token 方式跳过登录界面,自动获取系统访问凭证
  2. 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, #appmargin: 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 登录流程

  1. 从 URL ?accessToken=xxx 中读取加密的 Token
  2. 解析 Token(格式:用户名$加密密码
  3. 使用 AES 算法(密钥 secret_key_123)解密密码
  4. 调用后端 ssologin 接口换取系统 Token
  5. 获取用户信息、生成动态路由
  6. 跳转至 /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,避免显示侧边栏、顶部导航等
  • embedRoutesconstantRoutes 合并加载

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

六、注意事项

  1. Token 安全性 :生产环境中,accessToken 应使用强加密算法(如 AES-256),并通过 HTTPS 传输
  2. Token 过期处理:需要处理 SSO Token 过期后的重新登录逻辑
  3. 跨域问题:iframe 嵌入需确保 RuoYi-Cloud 配置了正确的 CORS 策略
  4. 用户权限 :SSO 登录后仍需调用 getInfogenerateRoutes 确保权限路由正常加载
  5. 密钥管理secret_key_123 仅为演示密钥,实际项目应使用配置文件或密钥管理服务

七、扩展建议

  • 支持多种 Token 格式(JWT、OAuth2 等)
  • 增加 Token 签名验证机制,防止伪造
  • 实现统一的 SSO 认证中心,支持多系统单点登录
  • 优化 iframe 通信机制,支持父子页面数据交互