houzhongjian
2024-08-08 820397e43a0b64d35c6d31d2a55475061438593b
提交 | 用户 | 时间
820397 1 <template>
H 2   <div v-show="ssoVisible" class="form-cont">
3     <!-- 应用名 -->
4     <LoginFormTitle style="width: 100%" />
5     <el-tabs class="form" style="float: none" value="uname">
6       <el-tab-pane :label="client.name" name="uname" />
7     </el-tabs>
8     <div>
9       <el-form :model="formData" class="login-form">
10         <!-- 授权范围的选择 -->
11         此第三方应用请求获得以下权限:
12         <el-form-item prop="scopes">
13           <el-checkbox-group v-model="formData.scopes">
14             <el-checkbox
15               v-for="scope in queryParams.scopes"
16               :key="scope"
17               :label="scope"
18               style="display: block; margin-bottom: -10px"
19             >
20               {{ formatScope(scope) }}
21             </el-checkbox>
22           </el-checkbox-group>
23         </el-form-item>
24         <!-- 下方的登录按钮 -->
25         <el-form-item class="w-1/1">
26           <el-button
27             :loading="formLoading"
28             class="w-6/10"
29             type="primary"
30             @click.prevent="handleAuthorize(true)"
31           >
32             <span v-if="!formLoading">同意授权</span>
33             <span v-else>授 权 中...</span>
34           </el-button>
35           <el-button class="w-3/10" @click.prevent="handleAuthorize(false)">拒绝</el-button>
36         </el-form-item>
37       </el-form>
38     </div>
39   </div>
40 </template>
41 <script lang="ts" setup>
42 import LoginFormTitle from './LoginFormTitle.vue'
43 import * as OAuth2Api from '@/api/login/oauth2'
44 import { LoginStateEnum, useLoginState } from './useLogin'
45 import type { RouteLocationNormalizedLoaded } from 'vue-router'
46
47 defineOptions({ name: 'SSOLogin' })
48
49 const route = useRoute() // 路由
50 const { currentRoute } = useRouter() // 路由
51 const { getLoginState, setLoginState } = useLoginState()
52
53 const client = ref({
54   // 客户端信息
55   name: '',
56   logo: ''
57 })
58 interface queryType {
59   responseType: string
60   clientId: string
61   redirectUri: string
62   state: string
63   scopes: string[]
64 }
65 const queryParams = reactive<queryType>({
66   // URL 上的 client_id、scope 等参数
67   responseType: '',
68   clientId: '',
69   redirectUri: '',
70   state: '',
71   scopes: [] // 优先从 query 参数获取;如果未传递,从后端获取
72 })
73 const ssoVisible = computed(() => unref(getLoginState) === LoginStateEnum.SSO) // 是否展示 SSO 登录的表单
74 interface formType {
75   scopes: string[]
76 }
77 const formData = reactive<formType>({
78   scopes: [] // 已选中的 scope 数组
79 })
80 const formLoading = ref(false) // 表单是否提交中
81
82 /** 初始化授权信息 */
83 const init = async () => {
84   // 防止在没有登录的情况下循环弹窗
85   if (typeof route.query.client_id === 'undefined') return
86   // 解析参数
87   // 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.xxxx.cn&response_type=code&scope=user.read%20user.write
88   // 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.xxxx.cn&response_type=code&scope=user.read
89   queryParams.responseType = route.query.response_type as string
90   queryParams.clientId = route.query.client_id as string
91   queryParams.redirectUri = route.query.redirect_uri as string
92   queryParams.state = route.query.state as string
93   if (route.query.scope) {
94     queryParams.scopes = (route.query.scope as string).split(' ')
95   }
96
97   // 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
98   if (queryParams.scopes.length > 0) {
99     const data = await doAuthorize(true, queryParams.scopes, [])
100     if (data) {
101       location.href = data
102       return
103     }
104   }
105
106   // 获取授权页的基本信息
107   const data = await OAuth2Api.getAuthorize(queryParams.clientId)
108   client.value = data.client
109   // 解析 scope
110   let scopes
111   // 1.1 如果 params.scope 非空,则过滤下返回的 scopes
112   if (queryParams.scopes.length > 0) {
113     scopes = []
114     for (const scope of data.scopes) {
115       if (queryParams.scopes.indexOf(scope.key) >= 0) {
116         scopes.push(scope)
117       }
118     }
119     // 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它
120   } else {
121     scopes = data.scopes
122     for (const scope of scopes) {
123       queryParams.scopes.push(scope.key)
124     }
125   }
126   // 生成已选中的 checkedScopes
127   for (const scope of scopes) {
128     if (scope.value) {
129       formData.scopes.push(scope.key)
130     }
131   }
132 }
133
134 /** 处理授权的提交 */
135 const handleAuthorize = async (approved) => {
136   // 计算 checkedScopes + uncheckedScopes
137   let checkedScopes
138   let uncheckedScopes
139   if (approved) {
140     // 同意授权,按照用户的选择
141     checkedScopes = formData.scopes
142     uncheckedScopes = queryParams.scopes.filter((item) => checkedScopes.indexOf(item) === -1)
143   } else {
144     // 拒绝,则都是取消
145     checkedScopes = []
146     uncheckedScopes = queryParams.scopes
147   }
148   // 提交授权的请求
149   formLoading.value = true
150   try {
151     const data = await doAuthorize(false, checkedScopes, uncheckedScopes)
152     if (!data) {
153       return
154     }
155     location.href = data
156   } finally {
157     formLoading.value = false
158   }
159 }
160
161 /** 调用授权 API 接口 */
162 const doAuthorize = (autoApprove, checkedScopes, uncheckedScopes) => {
163   return OAuth2Api.authorize(
164     queryParams.responseType,
165     queryParams.clientId,
166     queryParams.redirectUri,
167     queryParams.state,
168     autoApprove,
169     checkedScopes,
170     uncheckedScopes
171   )
172 }
173
174 /** 格式化 scope 文本 */
175 const formatScope = (scope) => {
176   // 格式化 scope 授权范围,方便用户理解。
177   // 这里仅仅是一个 demo,可以考虑录入到字典数据中,例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。
178   switch (scope) {
179     case 'user.read':
180       return '访问你的个人信息'
181     case 'user.write':
182       return '修改你的个人信息'
183     default:
184       return scope
185   }
186 }
187
188 /** 监听当前路由为 SSOLogin 时,进行数据的初始化 */
189 watch(
190   () => currentRoute.value,
191   (route: RouteLocationNormalizedLoaded) => {
192     if (route.name === 'SSOLogin') {
193       setLoginState(LoginStateEnum.SSO)
194       init()
195     }
196   },
197   { immediate: true }
198 )
199 </script>