增加SSO单点登录
This commit is contained in:
parent
f16ea74abe
commit
2d019f1bf9
@ -1,16 +1,19 @@
|
||||
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
|
||||
scene: number
|
||||
}
|
||||
|
||||
export interface SmsLoginVO {
|
||||
mobile: string
|
||||
code: string
|
||||
@ -71,3 +74,44 @@ export const getCodeApi = (data) => {
|
||||
export const reqCheckApi = (data) => {
|
||||
return request.postOriginal({ url: 'system/captcha/check', data })
|
||||
}
|
||||
|
||||
// ========== OAUTH 2.0 相关 ==========
|
||||
|
||||
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: any,
|
||||
uncheckedScopes: any
|
||||
) {
|
||||
// 构建 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'
|
||||
})
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import axios, {
|
||||
AxiosError,
|
||||
AxiosInstance,
|
||||
AxiosRequestHeaders,
|
||||
AxiosResponse,
|
||||
AxiosError,
|
||||
InternalAxiosRequestConfig
|
||||
} from 'axios'
|
||||
|
||||
@ -230,7 +230,7 @@ const handleAuthorized = () => {
|
||||
wsCache.clear()
|
||||
removeToken()
|
||||
isRelogin.show = false
|
||||
window.location.href = '/'
|
||||
window.location.href = '/login?redirect=/sso?' + window.location.href.split('?')[1]
|
||||
})
|
||||
}
|
||||
return Promise.reject(t('sys.api.timeoutMessage'))
|
||||
|
@ -129,6 +129,12 @@ export default {
|
||||
btnMobile: '手机登录',
|
||||
btnQRCode: '二维码登录',
|
||||
qrcode: '扫描二维码登录',
|
||||
sso: {
|
||||
user: {
|
||||
read: '访问你的个人信息',
|
||||
write: '修改你的个人信息'
|
||||
}
|
||||
},
|
||||
btnRegister: '注册',
|
||||
SmsSendMsg: '验证码已发送'
|
||||
},
|
||||
@ -353,6 +359,7 @@ export default {
|
||||
login: {
|
||||
backSignIn: '返回',
|
||||
signInFormTitle: '登录',
|
||||
ssoFormTitle: '三方授权',
|
||||
mobileSignInFormTitle: '手机登录',
|
||||
qrSignInFormTitle: '二维码登录',
|
||||
signUpFormTitle: '注册',
|
||||
|
@ -1,11 +1,11 @@
|
||||
import type { App } from 'vue'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import remainingRouter from './modules/remaining'
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(), // createWebHashHistory URL带#,createWebHistory URL不带#
|
||||
history: createWebHistory(), // createWebHashHistory URL带#,createWebHistory URL不带#
|
||||
strict: true,
|
||||
routes: remainingRouter as RouteRecordRaw[],
|
||||
scrollBehavior: () => ({ left: 0, top: 0 })
|
||||
|
@ -185,6 +185,16 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||
noTagsView: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/sso',
|
||||
component: () => import('@/views/Login/Login.vue'),
|
||||
name: 'SSOLogin',
|
||||
meta: {
|
||||
hidden: true,
|
||||
title: t('router.login'),
|
||||
noTagsView: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
component: () => import('@/views/Error/403.vue'),
|
||||
|
14
src/types/auto-components.d.ts
vendored
14
src/types/auto-components.d.ts
vendored
@ -23,13 +23,12 @@ declare module '@vue/runtime-core' {
|
||||
DictTag: typeof import('./../components/DictTag/src/DictTag.vue')['default']
|
||||
Echart: typeof import('./../components/Echart/src/Echart.vue')['default']
|
||||
Editor: typeof import('./../components/Editor/src/Editor.vue')['default']
|
||||
ElAutoResizer: typeof import('element-plus/es')['ElAutoResizer']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
@ -54,34 +53,23 @@ declare module '@vue/runtime-core' {
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTableV2: typeof import('element-plus/es')['ElTableV2']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
Error: typeof import('./../components/Error/src/Error.vue')['default']
|
||||
FlowCondition: typeof import('./../components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue')['default']
|
||||
|
@ -52,6 +52,11 @@
|
||||
<QrCodeForm class="p-20px h-auto m-auto <xl:(rounded-3xl light:bg-white)" />
|
||||
<!-- 注册 -->
|
||||
<RegisterForm class="p-20px h-auto m-auto <xl:(rounded-3xl light:bg-white)" />
|
||||
<!-- 三方登录 v-if触发组件初始化 -->
|
||||
<SSOLoginVue
|
||||
v-if="isSSO"
|
||||
class="p-20px h-auto m-auto <xl:(rounded-3xl light:bg-white)"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
@ -65,12 +70,25 @@ import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
|
||||
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
|
||||
import { LoginForm, MobileForm, RegisterForm, QrCodeForm } from './components'
|
||||
|
||||
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
|
||||
import { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = getPrefixCls('login')
|
||||
// =======SSO======
|
||||
const isSSO = ref(false)
|
||||
const router = useRouter()
|
||||
// 监听当前路由
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
(route: RouteLocationNormalizedLoaded) => {
|
||||
if (route.name === 'SSOLogin') isSSO.value = true
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -137,7 +137,7 @@ import { useIcon } from '@/hooks/web/useIcon'
|
||||
import * as authUtil from '@/utils/auth'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import * as LoginApi from '@/api/login'
|
||||
import { LoginStateEnum, useLoginState, useFormValid } from './useLogin'
|
||||
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
@ -240,7 +240,12 @@ const handleLogin = async (params) => {
|
||||
if (!redirect.value) {
|
||||
redirect.value = '/'
|
||||
}
|
||||
// 判断是否为SSO登录
|
||||
if (redirect.value.indexOf('sso') !== -1) {
|
||||
window.location.href = window.location.href.replace('/login?redirect=', '')
|
||||
} else {
|
||||
push({ path: redirect.value || permissionStore.addRouters[0].path })
|
||||
}
|
||||
} catch {
|
||||
loginLoading.value = false
|
||||
} finally {
|
||||
@ -274,6 +279,7 @@ const doSocialLogin = async (type: number) => {
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
(route: RouteLocationNormalizedLoaded) => {
|
||||
if (route.name === 'SSOLogin') setLoginState(LoginStateEnum.SSO)
|
||||
redirect.value = route?.query?.redirect as string
|
||||
},
|
||||
{
|
||||
@ -291,6 +297,7 @@ onMounted(() => {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.login-code {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
|
@ -16,7 +16,8 @@ const getFormTitle = computed(() => {
|
||||
[LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
|
||||
[LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
|
||||
[LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'),
|
||||
[LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle')
|
||||
[LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'),
|
||||
[LoginStateEnum.SSO]: t('sys.login.ssoFormTitle')
|
||||
}
|
||||
return titleObj[unref(getLoginState)]
|
||||
})
|
||||
|
177
src/views/Login/components/SSOLogin.vue
Normal file
177
src/views/Login/components/SSOLogin.vue
Normal file
@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<!-- 表单 -->
|
||||
<div class="form-cont">
|
||||
<el-tabs class="form" style="float: none" value="uname">
|
||||
<el-tab-pane :label="'三方授权(' + client.name + ')'" name="uname" />
|
||||
</el-tabs>
|
||||
<div>
|
||||
<el-form ref="ssoForm" :model="loginForm" class="login-form">
|
||||
<!-- 授权范围的选择 -->
|
||||
此第三方应用请求获得以下权限:
|
||||
<el-form-item prop="scopes">
|
||||
<el-checkbox-group v-model="loginForm.scopes">
|
||||
<el-checkbox
|
||||
v-for="scope in params.scopes"
|
||||
:label="scope"
|
||||
:key="scope"
|
||||
style="display: block; margin-bottom: -10px"
|
||||
>{{ formatScope(scope) }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<!-- 下方的登录按钮 -->
|
||||
<el-form-item style="width: 100%">
|
||||
<el-button
|
||||
:loading="loading"
|
||||
size="small"
|
||||
type="primary"
|
||||
style="width: 60%"
|
||||
@click.prevent="handleAuthorize(true)"
|
||||
>
|
||||
<span v-if="!loading">同意授权</span>
|
||||
<span v-else>授 权 中...</span>
|
||||
</el-button>
|
||||
<el-button size="small" style="width: 36%" @click.prevent="handleAuthorize(false)"
|
||||
>拒绝
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" name="SSOLogin" setup>
|
||||
import { authorize, getAuthorize } from '@/api/login'
|
||||
|
||||
const { t } = useI18n()
|
||||
const ssoForm = ref() // 表单Ref
|
||||
|
||||
type scopesType = string[]
|
||||
interface paramsType {
|
||||
responseType: string
|
||||
clientId: string
|
||||
redirectUri: string
|
||||
state: string
|
||||
scopes: scopesType
|
||||
}
|
||||
const loginForm = reactive<{ scopes: scopesType }>({
|
||||
scopes: [] // 已选中的 scope 数组
|
||||
})
|
||||
const params = reactive<paramsType>({
|
||||
// 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 都是一条字典数据。
|
||||
return t(`login.sso.${scope}`)
|
||||
}
|
||||
const route = useRoute()
|
||||
const init = () => {
|
||||
// 防止在没有登录的情况下循环弹窗
|
||||
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
|
||||
if (route.query.scope) {
|
||||
params.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
|
||||
})
|
||||
}
|
||||
|
||||
// 获取授权页的基本信息
|
||||
getAuthorize(params.clientId).then((res) => {
|
||||
console.log(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)
|
||||
}
|
||||
}
|
||||
// 生成已选中的 checkedScopes
|
||||
for (const scope of scopes) {
|
||||
if (scope.value) {
|
||||
loginForm.scopes.push(scope.key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
init()
|
||||
</script>
|
@ -3,5 +3,6 @@ import MobileForm from './MobileForm.vue'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import RegisterForm from './RegisterForm.vue'
|
||||
import QrCodeForm from './QrCodeForm.vue'
|
||||
import SSOLoginVue from './SSOLogin.vue'
|
||||
|
||||
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm }
|
||||
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue }
|
||||
|
@ -5,7 +5,8 @@ export enum LoginStateEnum {
|
||||
REGISTER,
|
||||
RESET_PASSWORD,
|
||||
MOBILE,
|
||||
QR_CODE
|
||||
QR_CODE,
|
||||
SSO
|
||||
}
|
||||
|
||||
const currentState = ref(LoginStateEnum.LOGIN)
|
||||
|
Loading…
Reference in New Issue
Block a user