REVIEW 单点登录界面
							parent
							
								
									7a2ccb8973
								
							
						
					
					
						commit
						68981b9153
					
				|  | @ -1,13 +1,6 @@ | |||
| import request from '@/config/axios' | ||||
| import { getRefreshToken } from '@/utils/auth' | ||||
| import type { UserLoginVO } from './types' | ||||
| import { service } from '@/config/axios/service' | ||||
| 
 | ||||
| export interface CodeImgResult { | ||||
|   captchaOnOff: boolean | ||||
|   img: string | ||||
|   uuid: string | ||||
| } | ||||
| 
 | ||||
| export interface SmsCodeVO { | ||||
|   mobile: string | ||||
|  | @ -74,51 +67,3 @@ export const getCodeApi = (data) => { | |||
| export const reqCheckApi = (data) => { | ||||
|   return request.postOriginal({ url: 'system/captcha/check', data }) | ||||
| } | ||||
| 
 | ||||
| // ========== OAUTH 2.0 相关 ==========
 | ||||
| export type scopesType = string[] | ||||
| export interface paramsType { | ||||
|   responseType: string | ||||
|   clientId: string | ||||
|   redirectUri: string | ||||
|   state: string | ||||
|   scopes: scopesType | ||||
| } | ||||
| export const getAuthorize = (clientId) => { | ||||
|   return request.get({ url: '/system/oauth2/authorize?clientId=' + clientId }) | ||||
| } | ||||
| 
 | ||||
| export function authorize( | ||||
|   responseType: string, | ||||
|   clientId: string, | ||||
|   redirectUri: string, | ||||
|   state: string, | ||||
|   autoApprove: boolean, | ||||
|   checkedScopes: scopesType, | ||||
|   uncheckedScopes: scopesType | ||||
| ) { | ||||
|   // 构建 scopes
 | ||||
|   const scopes = {} | ||||
|   for (const scope of checkedScopes) { | ||||
|     scopes[scope] = true | ||||
|   } | ||||
|   for (const scope of uncheckedScopes) { | ||||
|     scopes[scope] = false | ||||
|   } | ||||
|   // 发起请求
 | ||||
|   return service({ | ||||
|     url: '/system/oauth2/authorize', | ||||
|     headers: { | ||||
|       'Content-type': 'application/x-www-form-urlencoded' | ||||
|     }, | ||||
|     params: { | ||||
|       response_type: responseType, | ||||
|       client_id: clientId, | ||||
|       redirect_uri: redirectUri, | ||||
|       state: state, | ||||
|       auto_approve: autoApprove, | ||||
|       scope: JSON.stringify(scopes) | ||||
|     }, | ||||
|     method: 'post' | ||||
|   }) | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,41 @@ | |||
| import request from '@/config/axios' | ||||
| 
 | ||||
| // 获得授权信息
 | ||||
| export const getAuthorize = (clientId: string) => { | ||||
|   return request.get({ url: '/system/oauth2/authorize?clientId=' + clientId }) | ||||
| } | ||||
| 
 | ||||
| // 发起授权
 | ||||
| export const authorize = ( | ||||
|   responseType: string, | ||||
|   clientId: string, | ||||
|   redirectUri: string, | ||||
|   state: string, | ||||
|   autoApprove: boolean, | ||||
|   checkedScopes: string[], | ||||
|   uncheckedScopes: string[] | ||||
| ) => { | ||||
|   // 构建 scopes
 | ||||
|   const scopes = {} | ||||
|   for (const scope of checkedScopes) { | ||||
|     scopes[scope] = true | ||||
|   } | ||||
|   for (const scope of uncheckedScopes) { | ||||
|     scopes[scope] = false | ||||
|   } | ||||
|   // 发起请求
 | ||||
|   return request.post({ | ||||
|     url: '/system/oauth2/authorize', | ||||
|     headers: { | ||||
|       'Content-type': 'application/x-www-form-urlencoded' | ||||
|     }, | ||||
|     params: { | ||||
|       response_type: responseType, | ||||
|       client_id: clientId, | ||||
|       redirect_uri: redirectUri, | ||||
|       state: state, | ||||
|       auto_approve: autoApprove, | ||||
|       scope: JSON.stringify(scopes) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|  | @ -26,17 +26,3 @@ export type UserVO = { | |||
|   loginIp: string | ||||
|   loginDate: string | ||||
| } | ||||
| 
 | ||||
| export type UserInfoVO = { | ||||
|   permissions: [] | ||||
|   roles: [] | ||||
|   user: { | ||||
|     avatar: string | ||||
|     id: number | ||||
|     nickname: string | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export type TentantNameVO = { | ||||
|   name: string | ||||
| } | ||||
|  |  | |||
|  | @ -129,12 +129,6 @@ export default { | |||
|     btnMobile: '手机登录', | ||||
|     btnQRCode: '二维码登录', | ||||
|     qrcode: '扫描二维码登录', | ||||
|     sso: { | ||||
|       user: { | ||||
|         read: '访问你的个人信息', | ||||
|         write: '修改你的个人信息' | ||||
|       } | ||||
|     }, | ||||
|     btnRegister: '注册', | ||||
|     SmsSendMsg: '验证码已发送' | ||||
|   }, | ||||
|  |  | |||
|  | @ -1,178 +1,178 @@ | |||
| <template> | ||||
|   <!-- 表单 --> | ||||
|   <div v-show="getShow" class="form-cont"> | ||||
|     <!--    <LoginFormTitle style="width: 100%" />--> | ||||
|   <div v-show="ssoVisible" class="form-cont"> | ||||
|     <!-- 应用名 --> | ||||
|     <LoginFormTitle style="width: 100%" /> | ||||
|     <el-tabs class="form" style="float: none" value="uname"> | ||||
|       <el-tab-pane :label="'三方授权(' + client.name + ')'" name="uname" /> | ||||
|       <el-tab-pane :label="client.name" name="uname" /> | ||||
|     </el-tabs> | ||||
|     <div> | ||||
|       <el-form ref="ssoForm" :model="loginForm" class="login-form"> | ||||
|       <el-form :model="formData" class="login-form"> | ||||
|         <!-- 授权范围的选择 --> | ||||
|         此第三方应用请求获得以下权限: | ||||
|         <el-form-item prop="scopes"> | ||||
|           <el-checkbox-group v-model="loginForm.scopes"> | ||||
|           <el-checkbox-group v-model="formData.scopes"> | ||||
|             <el-checkbox | ||||
|               v-for="scope in params.scopes" | ||||
|               v-for="scope in queryParams.scopes" | ||||
|               :key="scope" | ||||
|               :label="scope" | ||||
|               style="display: block; margin-bottom: -10px" | ||||
|               >{{ formatScope(scope) }} | ||||
|             > | ||||
|               {{ formatScope(scope) }} | ||||
|             </el-checkbox> | ||||
|           </el-checkbox-group> | ||||
|         </el-form-item> | ||||
|         <!-- 下方的登录按钮 --> | ||||
|         <el-form-item style="width: 100%"> | ||||
|         <el-form-item class="w-1/1"> | ||||
|           <el-button | ||||
|             :loading="loading" | ||||
|             size="small" | ||||
|             style="width: 60%" | ||||
|             :loading="formLoading" | ||||
|             class="w-6/10" | ||||
|             type="primary" | ||||
|             @click.prevent="handleAuthorize(true)" | ||||
|           > | ||||
|             <span v-if="!loading">同意授权</span> | ||||
|             <span v-if="!formLoading">同意授权</span> | ||||
|             <span v-else>授 权 中...</span> | ||||
|           </el-button> | ||||
|           <el-button size="small" style="width: 36%" @click.prevent="handleAuthorize(false)" | ||||
|             >拒绝 | ||||
|           </el-button> | ||||
|           <el-button class="w-3/10" @click.prevent="handleAuthorize(false)">拒绝</el-button> | ||||
|         </el-form-item> | ||||
|       </el-form> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| <script lang="ts" name="SSOLogin" setup> | ||||
| // import LoginFormTitle from './LoginFormTitle.vue' // TODO 艿艿你看看要不要这个表头 | ||||
| import { authorize, getAuthorize, paramsType, scopesType } from '@/api/login' | ||||
| import LoginFormTitle from './LoginFormTitle.vue' | ||||
| import * as OAuth2Api from '@/api/login/oauth2' | ||||
| import { LoginStateEnum, useLoginState } from './useLogin' | ||||
| import type { RouteLocationNormalizedLoaded } from 'vue-router' | ||||
| 
 | ||||
| const { t } = useI18n() | ||||
| const ssoForm = ref() // 表单Ref | ||||
| const route = useRoute() // 路由 | ||||
| const { currentRoute } = useRouter() // 路由 | ||||
| const { getLoginState, setLoginState } = useLoginState() | ||||
| const getShow = computed(() => unref(getLoginState) === LoginStateEnum.SSO) | ||||
| const loginForm = reactive<{ scopes: scopesType }>({ | ||||
|   scopes: [] // 已选中的 scope 数组 | ||||
| 
 | ||||
| const client = ref({ | ||||
|   // 客户端信息 | ||||
|   name: '', | ||||
|   logo: '' | ||||
| }) | ||||
| const params = reactive<paramsType>({ | ||||
| const queryParams = reactive({ | ||||
|   // URL 上的 client_id、scope 等参数 | ||||
|   responseType: '', | ||||
|   clientId: '', | ||||
|   redirectUri: '', | ||||
|   state: '', | ||||
|   scopes: [] // 优先从 query 参数获取;如果未传递,从后端获取 | ||||
| }) // 表单Ref | ||||
| const client = ref({ | ||||
|   // 客户端信息 | ||||
|   name: '', | ||||
|   logo: '' | ||||
| }) | ||||
| const loading = ref(false) | ||||
| const handleAuthorize = (approved) => { | ||||
|   ssoForm.value.validate((valid) => { | ||||
|     if (!valid) { | ||||
|       return | ||||
|     } | ||||
|     loading.value = true | ||||
|     // 计算 checkedScopes + uncheckedScopes | ||||
|     let checkedScopes | ||||
|     let uncheckedScopes | ||||
|     if (approved) { | ||||
|       // 同意授权,按照用户的选择 | ||||
|       checkedScopes = loginForm.scopes | ||||
|       uncheckedScopes = params.scopes.filter((item) => checkedScopes.indexOf(item) === -1) | ||||
|     } else { | ||||
|       // 拒绝,则都是取消 | ||||
|       checkedScopes = [] | ||||
|       uncheckedScopes = params.scopes | ||||
|     } | ||||
|     // 提交授权的请求 | ||||
|     doAuthorize(false, checkedScopes, uncheckedScopes) | ||||
|       .then((res) => { | ||||
|         const href = res.data | ||||
|         if (!href) { | ||||
|           return | ||||
|         } | ||||
|         location.href = href | ||||
|       }) | ||||
|       .finally(() => { | ||||
|         loading.value = false | ||||
|       }) | ||||
|   }) | ||||
| } | ||||
| const doAuthorize = (autoApprove, checkedScopes, uncheckedScopes) => { | ||||
|   return authorize( | ||||
|     params.responseType, | ||||
|     params.clientId, | ||||
|     params.redirectUri, | ||||
|     params.state, | ||||
|     autoApprove, | ||||
|     checkedScopes, | ||||
|     uncheckedScopes | ||||
|   ) | ||||
| } | ||||
| const formatScope = (scope) => { | ||||
|   // 格式化 scope 授权范围,方便用户理解。 | ||||
|   // 这里仅仅是一个 demo,可以考虑录入到字典数据中,例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。 | ||||
|   // TODO 这个之做了中文部分 | ||||
|   return t(`login.sso.${scope}`) | ||||
| } | ||||
| const route = useRoute() | ||||
| const init = () => { | ||||
| const ssoVisible = computed(() => unref(getLoginState) === LoginStateEnum.SSO) // 是否展示 SSO 登录的表单 | ||||
| const formData = reactive({ | ||||
|   scopes: [] // 已选中的 scope 数组 | ||||
| }) | ||||
| const formLoading = ref(false) // 表单是否提交中 | ||||
| 
 | ||||
| /** 初始化授权信息 */ | ||||
| const init = async () => { | ||||
|   // 防止在没有登录的情况下循环弹窗 | ||||
|   if (typeof route.query.client_id === 'undefined') return | ||||
|   // 解析参数 | ||||
|   // 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write | ||||
|   // 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read | ||||
|   params.responseType = route.query.response_type as string | ||||
|   params.clientId = route.query.client_id as string | ||||
|   params.redirectUri = route.query.redirect_uri as string | ||||
|   params.state = route.query.state as string | ||||
|   queryParams.responseType = route.query.response_type as string | ||||
|   queryParams.clientId = route.query.client_id as string | ||||
|   queryParams.redirectUri = route.query.redirect_uri as string | ||||
|   queryParams.state = route.query.state as string | ||||
|   if (route.query.scope) { | ||||
|     params.scopes = (route.query.scope as string).split(' ') | ||||
|     queryParams.scopes = (route.query.scope as string).split(' ') | ||||
|   } | ||||
| 
 | ||||
|   // 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。 | ||||
|   if (params.scopes.length > 0) { | ||||
|     doAuthorize(true, params.scopes, []).then((res) => { | ||||
|       if (!res) { | ||||
|         console.log('自动授权未通过!') | ||||
|         return | ||||
|       } | ||||
|       location.href = res.data | ||||
|     }) | ||||
|   if (queryParams.scopes.length > 0) { | ||||
|     const data = await doAuthorize(true, queryParams.scopes, []) | ||||
|     if (data) { | ||||
|       location.href = data | ||||
|       return | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // 获取授权页的基本信息 | ||||
|   getAuthorize(params.clientId).then((res) => { | ||||
|     client.value = res.client | ||||
|     // 解析 scope | ||||
|     let scopes | ||||
|     // 1.1 如果 params.scope 非空,则过滤下返回的 scopes | ||||
|     if (params.scopes.length > 0) { | ||||
|       scopes = [] | ||||
|       for (const scope of res.scopes) { | ||||
|         if (params.scopes.indexOf(scope.key) >= 0) { | ||||
|           scopes.push(scope) | ||||
|         } | ||||
|       } | ||||
|       // 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它 | ||||
|     } else { | ||||
|       scopes = res.scopes | ||||
|       for (const scope of scopes) { | ||||
|         params.scopes.push(scope.key) | ||||
|   const data = await OAuth2Api.getAuthorize(queryParams.clientId) | ||||
|   client.value = data.client | ||||
|   // 解析 scope | ||||
|   let scopes | ||||
|   // 1.1 如果 params.scope 非空,则过滤下返回的 scopes | ||||
|   if (queryParams.scopes.length > 0) { | ||||
|     scopes = [] | ||||
|     for (const scope of data.scopes) { | ||||
|       if (queryParams.scopes.indexOf(scope.key) >= 0) { | ||||
|         scopes.push(scope) | ||||
|       } | ||||
|     } | ||||
|     // 生成已选中的 checkedScopes | ||||
|     // 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它 | ||||
|   } else { | ||||
|     scopes = data.scopes | ||||
|     for (const scope of scopes) { | ||||
|       if (scope.value) { | ||||
|         loginForm.scopes.push(scope.key) | ||||
|       } | ||||
|       queryParams.scopes.push(scope.key) | ||||
|     } | ||||
|   }) | ||||
|   } | ||||
|   // 生成已选中的 checkedScopes | ||||
|   for (const scope of scopes) { | ||||
|     if (scope.value) { | ||||
|       formData.scopes.push(scope.key) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| // =======SSO====== | ||||
| const { currentRoute } = useRouter() | ||||
| // 监听当前路由 | ||||
| 
 | ||||
| /** 处理授权的提交 */ | ||||
| const handleAuthorize = async (approved) => { | ||||
|   // 计算 checkedScopes + uncheckedScopes | ||||
|   let checkedScopes | ||||
|   let uncheckedScopes | ||||
|   if (approved) { | ||||
|     // 同意授权,按照用户的选择 | ||||
|     checkedScopes = formData.scopes | ||||
|     uncheckedScopes = queryParams.scopes.filter((item) => checkedScopes.indexOf(item) === -1) | ||||
|   } else { | ||||
|     // 拒绝,则都是取消 | ||||
|     checkedScopes = [] | ||||
|     uncheckedScopes = queryParams.scopes | ||||
|   } | ||||
|   // 提交授权的请求 | ||||
|   formLoading.value = true | ||||
|   try { | ||||
|     const data = await doAuthorize(false, checkedScopes, uncheckedScopes) | ||||
|     if (!data) { | ||||
|       return | ||||
|     } | ||||
|     location.href = data | ||||
|   } finally { | ||||
|     formLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 调用授权 API 接口 */ | ||||
| const doAuthorize = (autoApprove, checkedScopes, uncheckedScopes) => { | ||||
|   return OAuth2Api.authorize( | ||||
|     queryParams.responseType, | ||||
|     queryParams.clientId, | ||||
|     queryParams.redirectUri, | ||||
|     queryParams.state, | ||||
|     autoApprove, | ||||
|     checkedScopes, | ||||
|     uncheckedScopes | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| /** 格式化 scope 文本 */ | ||||
| const formatScope = (scope) => { | ||||
|   // 格式化 scope 授权范围,方便用户理解。 | ||||
|   // 这里仅仅是一个 demo,可以考虑录入到字典数据中,例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。 | ||||
|   switch (scope) { | ||||
|     case 'user.read': | ||||
|       return '访问你的个人信息' | ||||
|     case 'user.write': | ||||
|       return '修改你的个人信息' | ||||
|     default: | ||||
|       return scope | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 监听当前路由为 SSOLogin 时,进行数据的初始化 */ | ||||
| watch( | ||||
|   () => currentRoute.value, | ||||
|   (route: RouteLocationNormalizedLoaded) => { | ||||
|  | @ -183,5 +183,4 @@ watch( | |||
|   }, | ||||
|   { immediate: true } | ||||
| ) | ||||
| init() | ||||
| </script> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 shizhong
						shizhong