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 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;