xsh_1997 3 dias atrás
pai
commit
68cb174608

+ 1 - 1
ruoyi-screen/.env.development

@@ -15,5 +15,5 @@ VITE_WEATHER_APP_ID=36793262
15 15
 VITE_WEATHER_APP_SECRET=t6a6z0H8
16 16
 VITE_WEATHER_CITY=巴青
17 17
 
18
-# 大屏免密登录(RSA 加密 UUID,见 doc/大屏/免密登录/大屏免密登录技术方案.md
18
+# 大屏免密登录(获取公钥 + RSA 加密 UUID)
19 19
 VITE_SCREEN_AUTO_LOGIN=true

+ 72 - 16
ruoyi-screen/src/router/index.js

@@ -1,7 +1,15 @@
1 1
 import { createRouter, createWebHistory } from "vue-router"
2 2
 import ScreenLayout from "../layout/ScreenLayout.vue"
3 3
 import { getToken } from "../utils/auth"
4
-import { ensureScreenAutoLogin, isScreenAutoLoginEnabled } from "../utils/screenAutoLogin"
4
+import {
5
+  ensureScreenAutoLogin,
6
+  getUrlScreenLoginCipher,
7
+  SCREEN_LOGIN_CIPHER_ERROR,
8
+  SCREEN_LOGIN_UUID_ERROR,
9
+  stripScreenLoginTokenQuery,
10
+  stripScreenLoginUuidQuery,
11
+  tryScreenLoginWithCipher
12
+} from "../utils/screenAutoLogin"
5 13
 
6 14
 /**
7 15
  * 大屏路由表(路径与后端 /bigScreen/* 业务对应,便于联调)
@@ -88,7 +96,59 @@ const router = createRouter({
88 96
   routes
89 97
 })
90 98
 
91
-/** 未登录跳转登录页;已登录访问 /login 则进首页;启用免密时先尝试自动登录 */
99
+/** URL ?token= 密文登录;失败则提示公钥不对并跳转登录页 */
100
+async function tryLoginWithUrlToken(to, next) {
101
+  const cipher = getUrlScreenLoginCipher(to.query)
102
+  if (!cipher) {
103
+    return false
104
+  }
105
+  try {
106
+    await tryScreenLoginWithCipher(cipher)
107
+    const query = stripScreenLoginTokenQuery(to.query)
108
+    if (to.path === "/login") {
109
+      const redirect =
110
+        typeof to.query.redirect === "string" ? to.query.redirect : "/home"
111
+      next({ path: redirect, replace: true })
112
+      return true
113
+    }
114
+    next({ path: to.path, query, replace: true })
115
+    return true
116
+  } catch {
117
+    next({
118
+      path: "/login",
119
+      query: { screenLoginError: SCREEN_LOGIN_CIPHER_ERROR },
120
+      replace: true
121
+    })
122
+    return true
123
+  }
124
+}
125
+
126
+/** URL ?uuid= 免密登录;失败则提示并跳转登录页 */
127
+async function tryLoginWithUrlUuid(to, next) {
128
+  const { attempted, ok } = await ensureScreenAutoLogin(to.query)
129
+  if (!attempted) {
130
+    return false
131
+  }
132
+  if (ok) {
133
+    const query = stripScreenLoginUuidQuery(to.query)
134
+    if (to.path === "/login") {
135
+      const redirect =
136
+        typeof to.query.redirect === "string" ? to.query.redirect : "/home"
137
+      next({ path: redirect, replace: true })
138
+      return true
139
+    }
140
+    next({ path: to.path, query, replace: true })
141
+    return true
142
+  }
143
+  next({
144
+    path: "/login",
145
+    query: { screenLoginError: SCREEN_LOGIN_UUID_ERROR },
146
+    replace: true
147
+  })
148
+  return true
149
+}
150
+
151
+/** 未登录跳转登录页;URL ?token= / ?uuid= 优先 */
92 152
 router.beforeEach(async (to, _from, next) => {
93 153
   const token = getToken()
94 154
   if (to.path === "/login") {
@@ -96,26 +156,22 @@ router.beforeEach(async (to, _from, next) => {
96 156
       next({ path: "/home" })
97 157
       return
98 158
     }
99
-    if (isScreenAutoLoginEnabled()) {
100
-      const ok = await ensureScreenAutoLogin()
101
-      if (ok) {
102
-        const redirect =
103
-          typeof to.query.redirect === "string" ? to.query.redirect : "/home"
104
-        next({ path: redirect })
105
-        return
106
-      }
159
+    if (await tryLoginWithUrlToken(to, next)) {
160
+      return
161
+    }
162
+    if (await tryLoginWithUrlUuid(to, next)) {
163
+      return
107 164
     }
108 165
     next()
109 166
     return
110 167
   }
111 168
   const needAuth = to.matched.some((r) => r.meta.requiresAuth)
112 169
   if (needAuth && !getToken()) {
113
-    if (isScreenAutoLoginEnabled()) {
114
-      const ok = await ensureScreenAutoLogin()
115
-      if (ok) {
116
-        next()
117
-        return
118
-      }
170
+    if (await tryLoginWithUrlToken(to, next)) {
171
+      return
172
+    }
173
+    if (await tryLoginWithUrlUuid(to, next)) {
174
+      return
119 175
     }
120 176
     next({ path: "/login", query: { redirect: to.fullPath } })
121 177
     return

+ 116 - 30
ruoyi-screen/src/utils/screenAutoLogin.js

@@ -2,9 +2,16 @@ import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
2 2
 import { getScreenPublicKey, screenAutoLogin } from '@/api/login'
3 3
 import { getToken, setToken } from '@/utils/auth'
4 4
 
5
+/** URL 密文登录失败时展示给用户的提示 */
6
+export const SCREEN_LOGIN_CIPHER_ERROR = '公钥不对'
7
+
8
+/** URL uuid 免密登录失败时展示给用户的提示 */
9
+export const SCREEN_LOGIN_UUID_ERROR = '免密失败,请联系管理员获取免登ID'
10
+
5 11
 /** 同一会话内仅尝试一次免密登录,避免反复请求 */
6 12
 let autoLoginState = null
7 13
 let autoLoginPromise = null
14
+let autoLoginUuid = null
8 15
 
9 16
 export function isScreenAutoLoginEnabled() {
10 17
   return import.meta.env.VITE_SCREEN_AUTO_LOGIN === 'true'
@@ -13,58 +20,136 @@ export function isScreenAutoLoginEnabled() {
13 20
 export function resetScreenAutoLoginState() {
14 21
   autoLoginState = null
15 22
   autoLoginPromise = null
23
+  autoLoginUuid = null
24
+}
25
+
26
+/** 从路由 query 读取 RSA 密文(?token=xxxx) */
27
+export function getUrlScreenLoginCipher(query) {
28
+  const raw = query?.token
29
+  if (typeof raw !== 'string' || !raw.trim()) {
30
+    return ''
31
+  }
32
+  try {
33
+    return decodeURIComponent(raw.trim())
34
+  } catch {
35
+    return raw.trim()
36
+  }
37
+}
38
+
39
+/** 从路由 query 读取免登 UUID(?uuid=xxxx) */
40
+export function getUrlScreenLoginUuid(query) {
41
+  const raw = query?.uuid
42
+  if (typeof raw !== 'string' || !raw.trim()) {
43
+    return ''
44
+  }
45
+  try {
46
+    return decodeURIComponent(raw.trim())
47
+  } catch {
48
+    return raw.trim()
49
+  }
50
+}
51
+
52
+/** 去掉 URL 中的 token,避免密文残留在地址栏 */
53
+export function stripScreenLoginTokenQuery(query = {}) {
54
+  const next = { ...query }
55
+  delete next.token
56
+  return next
57
+}
58
+
59
+/** 去掉 URL 中的 uuid */
60
+export function stripScreenLoginUuidQuery(query = {}) {
61
+  const next = { ...query }
62
+  delete next.uuid
63
+  return next
16 64
 }
17 65
 
18 66
 /**
19
- * RSA 加密 UUID 换取 JWT(见 doc/大屏/免密登录/大屏免密登录技术方案.md)
67
+ * 使用 URL 传入的 RSA 密文换取 JWT
68
+ * @param {string} cipher
20 69
  * @returns {Promise<string>} JWT
21 70
  */
22
-export async function tryScreenAutoLogin() {
23
-  const keyRes = await getScreenPublicKey()
24
-  const publicKey = keyRes.data?.publicKey
25
-  if (!publicKey) {
26
-    throw new Error('未获取到 RSA 公钥')
71
+export async function tryScreenLoginWithCipher(cipher) {
72
+  if (!cipher) {
73
+    throw new Error(SCREEN_LOGIN_CIPHER_ERROR)
74
+  }
75
+  try {
76
+    const loginRes = await screenAutoLogin(cipher)
77
+    if (!loginRes?.token) {
78
+      throw new Error(SCREEN_LOGIN_CIPHER_ERROR)
79
+    }
80
+    setToken(loginRes.token)
81
+    autoLoginState = 'ok'
82
+    return loginRes.token
83
+  } catch {
84
+    throw new Error(SCREEN_LOGIN_CIPHER_ERROR)
27 85
   }
86
+}
28 87
 
29
-  const uuid = crypto.randomUUID()
30
-  const encryptor = new JSEncrypt()
31
-  encryptor.setPublicKey(publicKey)
32
-  const cipher = encryptor.encrypt(uuid)
33
-  if (!cipher) {
34
-    throw new Error('RSA 加密失败')
88
+/**
89
+ * 获取公钥 → RSA 加密 URL 中的 UUID → 换取 JWT
90
+ * @param {string} uuid
91
+ * @returns {Promise<string>} JWT
92
+ */
93
+export async function tryScreenAutoLogin(uuid) {
94
+  if (!uuid) {
95
+    throw new Error(SCREEN_LOGIN_UUID_ERROR)
35 96
   }
97
+  try {
98
+    const keyRes = await getScreenPublicKey()
99
+    const publicKey = keyRes.data?.publicKey
100
+    if (!publicKey) {
101
+      throw new Error(SCREEN_LOGIN_UUID_ERROR)
102
+    }
103
+
104
+    const encryptor = new JSEncrypt()
105
+    encryptor.setPublicKey(publicKey)
106
+    const cipher = encryptor.encrypt(uuid)
107
+    if (!cipher) {
108
+      throw new Error(SCREEN_LOGIN_UUID_ERROR)
109
+    }
36 110
 
37
-  const loginRes = await screenAutoLogin(cipher)
38
-  if (!loginRes.token) {
39
-    throw new Error('免密登录未返回 token')
111
+    const loginRes = await screenAutoLogin(cipher)
112
+    if (!loginRes?.token) {
113
+      throw new Error(SCREEN_LOGIN_UUID_ERROR)
114
+    }
115
+    setToken(loginRes.token)
116
+    return loginRes.token
117
+  } catch {
118
+    throw new Error(SCREEN_LOGIN_UUID_ERROR)
40 119
   }
41
-  setToken(loginRes.token)
42
-  return loginRes.token
43 120
 }
44 121
 
45 122
 /**
46
- * 路由守卫用:已登录直接通过;未启用免密则 false;否则尝试一次免密登录
47
- * @returns {Promise<boolean>}
123
+ * 路由守卫用:URL 含 uuid 时尝试免密登录
124
+ * @returns {Promise<{ attempted: boolean, ok: boolean }>}
48 125
  */
49
-export async function ensureScreenAutoLogin() {
126
+export async function ensureScreenAutoLogin(query) {
50 127
   if (getToken()) {
51
-    return true
128
+    return { attempted: false, ok: true }
52 129
   }
53 130
   if (!isScreenAutoLoginEnabled()) {
54
-    return false
131
+    return { attempted: false, ok: false }
55 132
   }
56
-  if (autoLoginState === 'fail') {
57
-    return false
133
+
134
+  const uuid = getUrlScreenLoginUuid(query)
135
+  if (!uuid) {
136
+    return { attempted: false, ok: false }
137
+  }
138
+
139
+  if (autoLoginState === 'fail' && autoLoginUuid === uuid) {
140
+    return { attempted: true, ok: false }
58 141
   }
59
-  if (autoLoginState === 'ok') {
60
-    return !!getToken()
142
+  if (autoLoginState === 'ok' && getToken()) {
143
+    return { attempted: false, ok: true }
61 144
   }
62
-  if (autoLoginState === 'pending' && autoLoginPromise) {
63
-    return autoLoginPromise
145
+  if (autoLoginState === 'pending' && autoLoginPromise && autoLoginUuid === uuid) {
146
+    const ok = await autoLoginPromise
147
+    return { attempted: true, ok }
64 148
   }
65 149
 
150
+  autoLoginUuid = uuid
66 151
   autoLoginState = 'pending'
67
-  autoLoginPromise = tryScreenAutoLogin()
152
+  autoLoginPromise = tryScreenAutoLogin(uuid)
68 153
     .then(() => {
69 154
       autoLoginState = 'ok'
70 155
       return true
@@ -74,5 +159,6 @@ export async function ensureScreenAutoLogin() {
74 159
       return false
75 160
     })
76 161
 
77
-  return autoLoginPromise
162
+  const ok = await autoLoginPromise
163
+  return { attempted: true, ok }
78 164
 }

+ 7 - 0
ruoyi-screen/src/views/login/index.vue

@@ -102,6 +102,13 @@ function onKeyupEnter() {
102 102
 }
103 103
 
104 104
 onMounted(() => {
105
+  const urlError = route.query.screenLoginError
106
+  if (typeof urlError === 'string' && urlError) {
107
+    errorMsg.value = urlError
108
+    const query = { ...route.query }
109
+    delete query.screenLoginError
110
+    router.replace({ path: '/login', query })
111
+  }
105 112
   loadRemembered()
106 113
   refreshCaptcha()
107 114
 })

+ 1 - 1
ruoyi-ui/src/layout/components/Sidebar/Logo.vue

@@ -80,7 +80,7 @@ export default {
80 80
       color: #fff;
81 81
       font-weight: 600;
82 82
       line-height: 50px;
83
-      font-size: 11px;
83
+      font-size: 14px;
84 84
       font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
85 85
       vertical-align: middle;
86 86
     }