Ver código fonte

REVIEW 单点登录界面

YunaiV 1 ano atrás
pai
commit
276e82c5a8

+ 0 - 55
src/api/login/index.ts

@@ -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 getCode = (data) => {
 export const reqCheck = (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'
-  })
-}

+ 41 - 0
src/api/login/oauth2/index.ts

@@ -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)
+    }
+  })
+}

+ 0 - 14
src/api/login/types.ts

@@ -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
-}

+ 0 - 6
src/locales/zh-CN.ts

@@ -129,12 +129,6 @@ export default {
     btnMobile: '手机登录',
     btnQRCode: '二维码登录',
     qrcode: '扫描二维码登录',
-    sso: {
-      user: {
-        read: '访问你的个人信息',
-        write: '修改你的个人信息'
-      }
-    },
     btnRegister: '注册',
     SmsSendMsg: '验证码已发送'
   },

+ 1 - 0
src/types/auto-components.d.ts

@@ -21,6 +21,7 @@ declare module '@vue/runtime-core' {
     Descriptions: typeof import('./../components/Descriptions/src/Descriptions.vue')['default']
     Dialog: typeof import('./../components/Dialog/src/Dialog.vue')['default']
     DictTag: typeof import('./../components/DictTag/src/DictTag.vue')['default']
+    DocAlert: typeof import('./../components/DocAlert/index.vue')['default']
     Echart: typeof import('./../components/Echart/src/Echart.vue')['default']
     Editor: typeof import('./../components/Editor/src/Editor.vue')['default']
     ElBadge: typeof import('element-plus/es')['ElBadge']

+ 120 - 121
src/views/Login/components/SSOLogin.vue

@@ -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)
     }
-  })
+  }
+}
+
+/** 处理授权的提交 */
+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
+  }
 }
-// =======SSO======
-const { currentRoute } = useRouter()
-// 监听当前路由
+
+/** 调用授权 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>