vue-sso-login-qcdl 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +235 -0
- package/dist/index.cjs.js +481 -0
- package/dist/index.esm.js +469 -0
- package/package.json +51 -0
- package/src/components/SsoCallback.vue +113 -0
- package/src/index.js +127 -0
- package/src/sso.js +161 -0
- package/src/utils/storage.js +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# @company/vue-sso-login
|
|
2
|
+
|
|
3
|
+
Vue SSO 单点登录组件,支持 Vue 2 和 Vue 3。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @company/vue-sso-login
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
### 1. 初始化 SSO 客户端
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
// main.js
|
|
17
|
+
import { createApp } from 'vue'
|
|
18
|
+
import axios from 'axios'
|
|
19
|
+
import { createSsoClient, createAxiosInterceptor } from '@company/vue-sso-login'
|
|
20
|
+
|
|
21
|
+
const app = createApp(App)
|
|
22
|
+
|
|
23
|
+
// 创建 axios 实例
|
|
24
|
+
const axiosInstance = axios.create({
|
|
25
|
+
baseURL: 'https://api.example.com'
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// 创建 SSO 客户端
|
|
29
|
+
const ssoClient = createSsoClient({
|
|
30
|
+
axios: axiosInstance,
|
|
31
|
+
getRedirectUrlApi: '/api/sso/redirect-url',
|
|
32
|
+
getTokenApi: '/api/sso/token',
|
|
33
|
+
logoutApi: '/api/sso/logout',
|
|
34
|
+
callbackPath: '/sso-callback',
|
|
35
|
+
homePath: '/dashboard',
|
|
36
|
+
loginPath: '/login'
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// 配置 axios 拦截器(自动添加 token)
|
|
40
|
+
createAxiosInterceptor(axiosInstance)
|
|
41
|
+
|
|
42
|
+
app.mount('#app')
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. 配置路由
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
// router/index.js
|
|
49
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
50
|
+
import { createSsoGuard, SsoCallback, useSso } from '@company/vue-sso-login'
|
|
51
|
+
|
|
52
|
+
const routes = [
|
|
53
|
+
{
|
|
54
|
+
path: '/sso-callback',
|
|
55
|
+
name: 'SsoCallback',
|
|
56
|
+
component: {
|
|
57
|
+
template: '<SsoCallback :sso-client="ssoClient" />',
|
|
58
|
+
setup() {
|
|
59
|
+
return { ssoClient: useSso() }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
path: '/dashboard',
|
|
65
|
+
name: 'Dashboard',
|
|
66
|
+
component: () => import('@/views/Dashboard.vue')
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const router = createRouter({
|
|
71
|
+
history: createWebHistory(),
|
|
72
|
+
routes
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// 添加路由守卫
|
|
76
|
+
router.beforeEach(createSsoGuard({
|
|
77
|
+
whiteList: ['/public', '/about'],
|
|
78
|
+
callbackPath: '/sso-callback'
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
export default router
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. 登录页面使用
|
|
85
|
+
|
|
86
|
+
```vue
|
|
87
|
+
<template>
|
|
88
|
+
<div class="login-page">
|
|
89
|
+
<button @click="handleSsoLogin" :disabled="loading">
|
|
90
|
+
{{ loading ? '跳转中...' : 'SSO 登录' }}
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<script setup>
|
|
96
|
+
import { ref } from 'vue'
|
|
97
|
+
import { useSso } from '@company/vue-sso-login'
|
|
98
|
+
|
|
99
|
+
const sso = useSso()
|
|
100
|
+
const loading = ref(false)
|
|
101
|
+
|
|
102
|
+
async function handleSsoLogin() {
|
|
103
|
+
loading.value = true
|
|
104
|
+
try {
|
|
105
|
+
await sso.redirectToSso()
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('SSO 登录失败:', error)
|
|
108
|
+
loading.value = false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
</script>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 4. 登出
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import { useSso } from '@company/vue-sso-login'
|
|
118
|
+
|
|
119
|
+
const sso = useSso()
|
|
120
|
+
|
|
121
|
+
// 登出并跳转登录页
|
|
122
|
+
await sso.logout()
|
|
123
|
+
|
|
124
|
+
// 登出但不跳转
|
|
125
|
+
await sso.logout(false)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## API 参考
|
|
129
|
+
|
|
130
|
+
### createSsoClient(options)
|
|
131
|
+
|
|
132
|
+
创建 SSO 客户端实例。
|
|
133
|
+
|
|
134
|
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
135
|
+
|------|------|------|--------|------|
|
|
136
|
+
| axios | AxiosInstance | 是 | - | axios 实例 |
|
|
137
|
+
| getRedirectUrlApi | string | 否 | '/sso/redirect-url' | 获取重定向地址接口 |
|
|
138
|
+
| getTokenApi | string | 否 | '/sso/token' | 获取 token 接口 |
|
|
139
|
+
| logoutApi | string | 否 | '/sso/logout' | 登出接口 |
|
|
140
|
+
| callbackPath | string | 否 | '/sso-callback' | 回调页面路径 |
|
|
141
|
+
| homePath | string | 否 | '/' | 首页路径 |
|
|
142
|
+
| loginPath | string | 否 | '/login' | 登录页路径 |
|
|
143
|
+
| storageOptions | object | 否 | {} | 存储配置 |
|
|
144
|
+
| headers | object | 否 | {} | 自定义请求头 |
|
|
145
|
+
|
|
146
|
+
### SsoClient 实例方法
|
|
147
|
+
|
|
148
|
+
| 方法 | 说明 |
|
|
149
|
+
|------|------|
|
|
150
|
+
| redirectToSso(params?) | 跳转到 SSO 登录页 |
|
|
151
|
+
| getTokenByCode(code, state?) | 使用授权码获取 token |
|
|
152
|
+
| handleCallback(route) | 处理回调页面逻辑 |
|
|
153
|
+
| isAuthenticated() | 检查是否已登录 |
|
|
154
|
+
| getToken() | 获取当前 token |
|
|
155
|
+
| getUserInfo() | 获取用户信息 |
|
|
156
|
+
| logout(redirectToLogin?) | 登出 |
|
|
157
|
+
|
|
158
|
+
### createSsoGuard(options)
|
|
159
|
+
|
|
160
|
+
创建路由守卫。
|
|
161
|
+
|
|
162
|
+
| 参数 | 类型 | 说明 |
|
|
163
|
+
|------|------|------|
|
|
164
|
+
| ssoClient | SsoClient | SSO 客户端实例 |
|
|
165
|
+
| whiteList | string[] | 白名单路径 |
|
|
166
|
+
| loginPath | string | 登录页路径 |
|
|
167
|
+
| callbackPath | string | 回调页路径 |
|
|
168
|
+
|
|
169
|
+
### SsoCallback 组件
|
|
170
|
+
|
|
171
|
+
中转页面组件,用于处理 SSO 回调。
|
|
172
|
+
|
|
173
|
+
| Props | 类型 | 必填 | 说明 |
|
|
174
|
+
|-------|------|------|------|
|
|
175
|
+
| ssoClient | SsoClient | 是 | SSO 客户端实例 |
|
|
176
|
+
| loadingText | string | 否 | 加载提示文字 |
|
|
177
|
+
| homePath | string | 否 | 首页路径 |
|
|
178
|
+
| autoRedirect | boolean | 否 | 是否自动跳转 |
|
|
179
|
+
|
|
180
|
+
| Events | 说明 |
|
|
181
|
+
|--------|------|
|
|
182
|
+
| success | 登录成功 |
|
|
183
|
+
| error | 登录失败 |
|
|
184
|
+
|
|
185
|
+
| Slots | 说明 |
|
|
186
|
+
|-------|------|
|
|
187
|
+
| loading | 自定义加载状态 |
|
|
188
|
+
| error | 自定义错误状态 |
|
|
189
|
+
|
|
190
|
+
## 接口约定
|
|
191
|
+
|
|
192
|
+
### 获取重定向地址接口
|
|
193
|
+
|
|
194
|
+
请求:
|
|
195
|
+
```
|
|
196
|
+
GET /api/sso/redirect-url?callback_url=xxx
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
响应:
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"code": 0,
|
|
203
|
+
"data": {
|
|
204
|
+
"redirect_url": "https://sso.example.com/login?..."
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 获取 Token 接口
|
|
210
|
+
|
|
211
|
+
请求:
|
|
212
|
+
```
|
|
213
|
+
POST /api/sso/token
|
|
214
|
+
{
|
|
215
|
+
"code": "授权码",
|
|
216
|
+
"state": "状态码",
|
|
217
|
+
"redirect_uri": "回调地址"
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
响应:
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"code": 0,
|
|
225
|
+
"data": {
|
|
226
|
+
"access_token": "xxx",
|
|
227
|
+
"refresh_token": "xxx",
|
|
228
|
+
"user_info": {}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var vue = require('vue');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Token 存储工具
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_TOKEN_KEY = 'sso_access_token';
|
|
11
|
+
const DEFAULT_REFRESH_TOKEN_KEY = 'sso_refresh_token';
|
|
12
|
+
const DEFAULT_USER_INFO_KEY = 'sso_user_info';
|
|
13
|
+
|
|
14
|
+
function createStorage(options = {}) {
|
|
15
|
+
const {
|
|
16
|
+
tokenKey = DEFAULT_TOKEN_KEY,
|
|
17
|
+
refreshTokenKey = DEFAULT_REFRESH_TOKEN_KEY,
|
|
18
|
+
userInfoKey = DEFAULT_USER_INFO_KEY,
|
|
19
|
+
storage = localStorage
|
|
20
|
+
} = options;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
// Token 操作
|
|
24
|
+
setToken(token) {
|
|
25
|
+
storage.setItem(tokenKey, token);
|
|
26
|
+
},
|
|
27
|
+
getToken() {
|
|
28
|
+
return storage.getItem(tokenKey)
|
|
29
|
+
},
|
|
30
|
+
removeToken() {
|
|
31
|
+
storage.removeItem(tokenKey);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Refresh Token 操作
|
|
35
|
+
setRefreshToken(token) {
|
|
36
|
+
storage.setItem(refreshTokenKey, token);
|
|
37
|
+
},
|
|
38
|
+
getRefreshToken() {
|
|
39
|
+
return storage.getItem(refreshTokenKey)
|
|
40
|
+
},
|
|
41
|
+
removeRefreshToken() {
|
|
42
|
+
storage.removeItem(refreshTokenKey);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// 用户信息操作
|
|
46
|
+
setUserInfo(info) {
|
|
47
|
+
storage.setItem(userInfoKey, JSON.stringify(info));
|
|
48
|
+
},
|
|
49
|
+
getUserInfo() {
|
|
50
|
+
const info = storage.getItem(userInfoKey);
|
|
51
|
+
return info ? JSON.parse(info) : null
|
|
52
|
+
},
|
|
53
|
+
removeUserInfo() {
|
|
54
|
+
storage.removeItem(userInfoKey);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// 清除所有
|
|
58
|
+
clearAll() {
|
|
59
|
+
storage.removeItem(tokenKey);
|
|
60
|
+
storage.removeItem(refreshTokenKey);
|
|
61
|
+
storage.removeItem(userInfoKey);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
createStorage();
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* SSO 核心逻辑
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
class SsoClient {
|
|
73
|
+
constructor(options = {}) {
|
|
74
|
+
this.options = {
|
|
75
|
+
// SSO 服务端接口地址
|
|
76
|
+
ssoServerUrl: '',
|
|
77
|
+
// 获取重定向地址的接口
|
|
78
|
+
getRedirectUrlApi: '/sso/redirect-config',
|
|
79
|
+
// 获取 token 的接口
|
|
80
|
+
getTokenApi: '/sso/exchange-token',
|
|
81
|
+
// 登出接口
|
|
82
|
+
logoutApi: '/sso/logout',
|
|
83
|
+
// 回调页面路径
|
|
84
|
+
callbackPath: '/sso-callback',
|
|
85
|
+
// 登录成功后跳转的首页
|
|
86
|
+
homePath: '/',
|
|
87
|
+
// 登录页路径
|
|
88
|
+
loginPath: '/login',
|
|
89
|
+
// axios 实例
|
|
90
|
+
axios: null,
|
|
91
|
+
// 存储配置
|
|
92
|
+
storageOptions: {},
|
|
93
|
+
// 自定义请求头
|
|
94
|
+
headers: {},
|
|
95
|
+
...options
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
this.storage = createStorage(this.options.storageOptions);
|
|
99
|
+
this.axios = this.options.axios;
|
|
100
|
+
|
|
101
|
+
if (!this.axios) {
|
|
102
|
+
throw new Error('[vue-sso-login] axios instance is required')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 获取 SSO 重定向地址并跳转
|
|
108
|
+
*/
|
|
109
|
+
async redirectToSso(params = {}) {
|
|
110
|
+
try {
|
|
111
|
+
// const callbackUrl = this._getCallbackUrl()
|
|
112
|
+
const response = await this.axios.get(this.options.getRedirectUrlApi);
|
|
113
|
+
|
|
114
|
+
const redirectUrl = response.data?.idpRedirectUrl;
|
|
115
|
+
if (redirectUrl) {
|
|
116
|
+
window.location.href = redirectUrl;
|
|
117
|
+
} else {
|
|
118
|
+
throw new Error('获取 SSO 重定向地址失败')
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('[vue-sso-login] redirectToSso error:', error);
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 使用授权码获取 token
|
|
128
|
+
*/
|
|
129
|
+
async getTokenByCode(code, state = '') {
|
|
130
|
+
try {
|
|
131
|
+
const response = await this.axios.post(this.options.getTokenApi, {
|
|
132
|
+
code,
|
|
133
|
+
state,
|
|
134
|
+
}, {
|
|
135
|
+
headers: this.options.headers
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const data = response.data?.data || response.data;
|
|
139
|
+
if (data?.access_token) {
|
|
140
|
+
this.storage.setToken(data.access_token);
|
|
141
|
+
if (data.refresh_token) {
|
|
142
|
+
this.storage.setRefreshToken(data.refresh_token);
|
|
143
|
+
}
|
|
144
|
+
if (data.user_info) {
|
|
145
|
+
this.storage.setUserInfo(data.user_info);
|
|
146
|
+
}
|
|
147
|
+
return data
|
|
148
|
+
} else {
|
|
149
|
+
throw new Error('获取 token 失败')
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('[vue-sso-login] getTokenByCode error:', error);
|
|
153
|
+
throw error
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 检查是否已登录
|
|
160
|
+
*/
|
|
161
|
+
isAuthenticated() {
|
|
162
|
+
return !!this.storage.getToken()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 获取当前 token
|
|
167
|
+
*/
|
|
168
|
+
getToken() {
|
|
169
|
+
return this.storage.getToken()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 获取用户信息
|
|
174
|
+
*/
|
|
175
|
+
getUserInfo() {
|
|
176
|
+
return this.storage.getUserInfo()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 登出
|
|
181
|
+
*/
|
|
182
|
+
async logout(redirectToLogin = true) {
|
|
183
|
+
try {
|
|
184
|
+
if (this.options.logoutApi) {
|
|
185
|
+
await this.axios.post(this.options.logoutApi, {}, {
|
|
186
|
+
headers: this.options.headers
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.warn('[vue-sso-login] logout api error:', error);
|
|
191
|
+
} finally {
|
|
192
|
+
this.storage.clearAll();
|
|
193
|
+
if (redirectToLogin) {
|
|
194
|
+
window.location.href = this.options.loginPath;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 处理回调页面逻辑
|
|
201
|
+
*/
|
|
202
|
+
async handleCallback(route) {
|
|
203
|
+
const code = route.query.code;
|
|
204
|
+
const state = route.query.state;
|
|
205
|
+
const error = route.query.error;
|
|
206
|
+
|
|
207
|
+
if (error) {
|
|
208
|
+
throw new Error(route.query.error_description || error)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!code) {
|
|
212
|
+
throw new Error('缺少授权码 code')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return await this.getTokenByCode(code, state)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 获取回调 URL
|
|
220
|
+
*/
|
|
221
|
+
_getCallbackUrl() {
|
|
222
|
+
const { protocol, host } = window.location;
|
|
223
|
+
return `${protocol}//${host}${this.options.callbackPath}`
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
var script = {
|
|
228
|
+
name: 'SsoCallback',
|
|
229
|
+
props: {
|
|
230
|
+
ssoClient: {
|
|
231
|
+
type: Object,
|
|
232
|
+
required: true
|
|
233
|
+
},
|
|
234
|
+
loadingText: {
|
|
235
|
+
type: String,
|
|
236
|
+
default: '正在登录中,请稍候...'
|
|
237
|
+
},
|
|
238
|
+
homePath: {
|
|
239
|
+
type: String,
|
|
240
|
+
default: '/'
|
|
241
|
+
},
|
|
242
|
+
autoRedirect: {
|
|
243
|
+
type: Boolean,
|
|
244
|
+
default: true
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
data() {
|
|
248
|
+
return {
|
|
249
|
+
loading: true,
|
|
250
|
+
error: null
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
async mounted() {
|
|
254
|
+
await this.handleSsoCallback();
|
|
255
|
+
},
|
|
256
|
+
methods: {
|
|
257
|
+
async handleSsoCallback() {
|
|
258
|
+
this.loading = true;
|
|
259
|
+
this.error = null;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const result = await this.ssoClient.handleCallback(this.$route);
|
|
263
|
+
this.$emit('success', result);
|
|
264
|
+
|
|
265
|
+
if (this.autoRedirect) {
|
|
266
|
+
const redirectPath = this.$route.query.redirect || this.homePath;
|
|
267
|
+
this.$router.replace(redirectPath);
|
|
268
|
+
}
|
|
269
|
+
} catch (err) {
|
|
270
|
+
this.error = err.message || '登录失败,请重试';
|
|
271
|
+
this.$emit('error', err);
|
|
272
|
+
} finally {
|
|
273
|
+
this.loading = false;
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
retry() {
|
|
277
|
+
this.ssoClient.redirectToSso();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const _hoisted_1 = { class: "sso-callback" };
|
|
283
|
+
const _hoisted_2 = {
|
|
284
|
+
key: 0,
|
|
285
|
+
class: "sso-callback__loading"
|
|
286
|
+
};
|
|
287
|
+
const _hoisted_3 = {
|
|
288
|
+
key: 1,
|
|
289
|
+
class: "sso-callback__error"
|
|
290
|
+
};
|
|
291
|
+
const _hoisted_4 = { class: "sso-callback__error-text" };
|
|
292
|
+
|
|
293
|
+
function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
294
|
+
return (vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [
|
|
295
|
+
($data.loading)
|
|
296
|
+
? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2, [
|
|
297
|
+
vue.renderSlot(_ctx.$slots, "loading", {}, () => [
|
|
298
|
+
_cache[1] || (_cache[1] = vue.createElementVNode("div", { class: "sso-callback__spinner" }, null, -1 /* CACHED */)),
|
|
299
|
+
vue.createElementVNode("p", null, vue.toDisplayString($props.loadingText), 1 /* TEXT */)
|
|
300
|
+
])
|
|
301
|
+
]))
|
|
302
|
+
: ($data.error)
|
|
303
|
+
? (vue.openBlock(), vue.createElementBlock("div", _hoisted_3, [
|
|
304
|
+
vue.renderSlot(_ctx.$slots, "error", { error: $data.error }, () => [
|
|
305
|
+
vue.createElementVNode("p", _hoisted_4, vue.toDisplayString($data.error), 1 /* TEXT */),
|
|
306
|
+
vue.createElementVNode("button", {
|
|
307
|
+
onClick: _cache[0] || (_cache[0] = (...args) => ($options.retry && $options.retry(...args))),
|
|
308
|
+
class: "sso-callback__retry-btn"
|
|
309
|
+
}, "重试")
|
|
310
|
+
])
|
|
311
|
+
]))
|
|
312
|
+
: vue.createCommentVNode("v-if", true)
|
|
313
|
+
]))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function styleInject(css, ref) {
|
|
317
|
+
if ( ref === void 0 ) ref = {};
|
|
318
|
+
var insertAt = ref.insertAt;
|
|
319
|
+
|
|
320
|
+
if (typeof document === 'undefined') { return; }
|
|
321
|
+
|
|
322
|
+
var head = document.head || document.getElementsByTagName('head')[0];
|
|
323
|
+
var style = document.createElement('style');
|
|
324
|
+
style.type = 'text/css';
|
|
325
|
+
|
|
326
|
+
if (insertAt === 'top') {
|
|
327
|
+
if (head.firstChild) {
|
|
328
|
+
head.insertBefore(style, head.firstChild);
|
|
329
|
+
} else {
|
|
330
|
+
head.appendChild(style);
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
head.appendChild(style);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (style.styleSheet) {
|
|
337
|
+
style.styleSheet.cssText = css;
|
|
338
|
+
} else {
|
|
339
|
+
style.appendChild(document.createTextNode(css));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
var css_248z = ".sso-callback[data-v-8ca5b118]{align-items:center;background:#f5f5f5;display:flex;justify-content:center;min-height:100vh}.sso-callback__loading[data-v-8ca5b118]{text-align:center}.sso-callback__spinner[data-v-8ca5b118]{animation:spin-8ca5b118 1s linear infinite;border:3px solid #e0e0e0;border-radius:50%;border-top-color:#1890ff;height:40px;margin:0 auto 16px;width:40px}@keyframes spin-8ca5b118{to{transform:rotate(1turn)}}.sso-callback__error[data-v-8ca5b118]{text-align:center}.sso-callback__error-text[data-v-8ca5b118]{color:#ff4d4f;margin-bottom:16px}.sso-callback__retry-btn[data-v-8ca5b118]{background:#1890ff;border:none;border-radius:4px;color:#fff;cursor:pointer;padding:8px 24px}";
|
|
344
|
+
styleInject(css_248z);
|
|
345
|
+
|
|
346
|
+
script.render = render;
|
|
347
|
+
script.__scopeId = "data-v-8ca5b118";
|
|
348
|
+
script.__file = "src/components/SsoCallback.vue";
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Vue SSO 单点登录组件
|
|
352
|
+
*/
|
|
353
|
+
|
|
354
|
+
// 全局 SSO 实例
|
|
355
|
+
let ssoInstance = null;
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 创建 SSO 实例
|
|
359
|
+
*/
|
|
360
|
+
function createSsoClient(options) {
|
|
361
|
+
ssoInstance = new SsoClient(options);
|
|
362
|
+
return ssoInstance
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 获取 SSO 实例
|
|
367
|
+
*/
|
|
368
|
+
function useSso() {
|
|
369
|
+
if (!ssoInstance) {
|
|
370
|
+
throw new Error('[vue-sso-login] SSO client not initialized. Call createSsoClient first.')
|
|
371
|
+
}
|
|
372
|
+
return ssoInstance
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* 创建路由守卫
|
|
377
|
+
*/
|
|
378
|
+
function createSsoGuard(options = {}) {
|
|
379
|
+
const {
|
|
380
|
+
ssoClient,
|
|
381
|
+
whiteList = [],
|
|
382
|
+
loginPath = '/login',
|
|
383
|
+
callbackPath = '/sso-callback'
|
|
384
|
+
} = options;
|
|
385
|
+
|
|
386
|
+
const client = ssoClient || ssoInstance;
|
|
387
|
+
|
|
388
|
+
return async (to, from, next) => {
|
|
389
|
+
// 白名单路径直接放行
|
|
390
|
+
if (whiteList.includes(to.path) || to.path === callbackPath) {
|
|
391
|
+
return next()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 已登录直接放行
|
|
395
|
+
if (client.isAuthenticated()) {
|
|
396
|
+
// 已登录访问登录页,跳转首页
|
|
397
|
+
if (to.path === loginPath) {
|
|
398
|
+
return next(client.options.homePath)
|
|
399
|
+
}
|
|
400
|
+
return next()
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 未登录,跳转 SSO 登录
|
|
404
|
+
try {
|
|
405
|
+
await client.redirectToSso({ redirect: to.fullPath });
|
|
406
|
+
} catch (error) {
|
|
407
|
+
console.error('[vue-sso-login] redirect error:', error);
|
|
408
|
+
next(loginPath);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* 创建 axios 请求拦截器
|
|
415
|
+
*/
|
|
416
|
+
function createAxiosInterceptor(axiosInstance, options = {}) {
|
|
417
|
+
const { tokenHeaderKey = 'Authorization', tokenPrefix = 'Bearer ' } = options;
|
|
418
|
+
const client = options.ssoClient || ssoInstance;
|
|
419
|
+
|
|
420
|
+
// 请求拦截器 - 自动添加 token
|
|
421
|
+
axiosInstance.interceptors.request.use(
|
|
422
|
+
(config) => {
|
|
423
|
+
const token = client.getToken();
|
|
424
|
+
if (token) {
|
|
425
|
+
config.headers[tokenHeaderKey] = `${tokenPrefix}${token}`;
|
|
426
|
+
}
|
|
427
|
+
return config
|
|
428
|
+
},
|
|
429
|
+
(error) => Promise.reject(error)
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
// 响应拦截器 - 处理 401
|
|
433
|
+
axiosInstance.interceptors.response.use(
|
|
434
|
+
(response) => response,
|
|
435
|
+
async (error) => {
|
|
436
|
+
if (error.response?.status === 401) {
|
|
437
|
+
client.storage.clearAll();
|
|
438
|
+
await client.redirectToSso();
|
|
439
|
+
}
|
|
440
|
+
return Promise.reject(error)
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Vue 插件安装
|
|
447
|
+
*/
|
|
448
|
+
function install(app, options) {
|
|
449
|
+
const client = createSsoClient(options);
|
|
450
|
+
|
|
451
|
+
// Vue 3
|
|
452
|
+
if (app.config?.globalProperties) {
|
|
453
|
+
app.config.globalProperties.$sso = client;
|
|
454
|
+
app.component('SsoCallback', script);
|
|
455
|
+
}
|
|
456
|
+
// Vue 2
|
|
457
|
+
else {
|
|
458
|
+
app.prototype.$sso = client;
|
|
459
|
+
app.component('SsoCallback', script);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
var index = {
|
|
464
|
+
install,
|
|
465
|
+
SsoClient,
|
|
466
|
+
SsoCallback: script,
|
|
467
|
+
createSsoClient,
|
|
468
|
+
useSso,
|
|
469
|
+
createSsoGuard,
|
|
470
|
+
createAxiosInterceptor
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
exports.SsoCallback = script;
|
|
474
|
+
exports.SsoClient = SsoClient;
|
|
475
|
+
exports.createAxiosInterceptor = createAxiosInterceptor;
|
|
476
|
+
exports.createSsoClient = createSsoClient;
|
|
477
|
+
exports.createSsoGuard = createSsoGuard;
|
|
478
|
+
exports.createStorage = createStorage;
|
|
479
|
+
exports.default = index;
|
|
480
|
+
exports.install = install;
|
|
481
|
+
exports.useSso = useSso;
|