REVIEW 单点登录界面
This commit is contained in:
parent
7a2ccb8973
commit
68981b9153
@ -1,13 +1,6 @@
|
|||||||
import request from '@/config/axios'
|
import request from '@/config/axios'
|
||||||
import { getRefreshToken } from '@/utils/auth'
|
import { getRefreshToken } from '@/utils/auth'
|
||||||
import type { UserLoginVO } from './types'
|
import type { UserLoginVO } from './types'
|
||||||
import { service } from '@/config/axios/service'
|
|
||||||
|
|
||||||
export interface CodeImgResult {
|
|
||||||
captchaOnOff: boolean
|
|
||||||
img: string
|
|
||||||
uuid: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SmsCodeVO {
|
export interface SmsCodeVO {
|
||||||
mobile: string
|
mobile: string
|
||||||
@ -74,51 +67,3 @@ export const getCodeApi = (data) => {
|
|||||||
export const reqCheckApi = (data) => {
|
export const reqCheckApi = (data) => {
|
||||||
return request.postOriginal({ url: 'system/captcha/check', 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
src/api/login/oauth2/index.ts
Normal file
41
src/api/login/oauth2/index.ts
Normal file
@ -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
|
loginIp: string
|
||||||
loginDate: 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: '手机登录',
|
btnMobile: '手机登录',
|
||||||
btnQRCode: '二维码登录',
|
btnQRCode: '二维码登录',
|
||||||
qrcode: '扫描二维码登录',
|
qrcode: '扫描二维码登录',
|
||||||
sso: {
|
|
||||||
user: {
|
|
||||||
read: '访问你的个人信息',
|
|
||||||
write: '修改你的个人信息'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
btnRegister: '注册',
|
btnRegister: '注册',
|
||||||
SmsSendMsg: '验证码已发送'
|
SmsSendMsg: '验证码已发送'
|
||||||
},
|
},
|
||||||
|
@ -1,178 +1,178 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 表单 -->
|
<div v-show="ssoVisible" class="form-cont">
|
||||||
<div v-show="getShow" class="form-cont">
|
<!-- 应用名 -->
|
||||||
<!-- <LoginFormTitle style="width: 100%" />-->
|
<LoginFormTitle style="width: 100%" />
|
||||||
<el-tabs class="form" style="float: none" value="uname">
|
<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>
|
</el-tabs>
|
||||||
<div>
|
<div>
|
||||||
<el-form ref="ssoForm" :model="loginForm" class="login-form">
|
<el-form :model="formData" class="login-form">
|
||||||
<!-- 授权范围的选择 -->
|
<!-- 授权范围的选择 -->
|
||||||
此第三方应用请求获得以下权限:
|
此第三方应用请求获得以下权限:
|
||||||
<el-form-item prop="scopes">
|
<el-form-item prop="scopes">
|
||||||
<el-checkbox-group v-model="loginForm.scopes">
|
<el-checkbox-group v-model="formData.scopes">
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
v-for="scope in params.scopes"
|
v-for="scope in queryParams.scopes"
|
||||||
:key="scope"
|
:key="scope"
|
||||||
:label="scope"
|
:label="scope"
|
||||||
style="display: block; margin-bottom: -10px"
|
style="display: block; margin-bottom: -10px"
|
||||||
>{{ formatScope(scope) }}
|
>
|
||||||
|
{{ formatScope(scope) }}
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- 下方的登录按钮 -->
|
<!-- 下方的登录按钮 -->
|
||||||
<el-form-item style="width: 100%">
|
<el-form-item class="w-1/1">
|
||||||
<el-button
|
<el-button
|
||||||
:loading="loading"
|
:loading="formLoading"
|
||||||
size="small"
|
class="w-6/10"
|
||||||
style="width: 60%"
|
|
||||||
type="primary"
|
type="primary"
|
||||||
@click.prevent="handleAuthorize(true)"
|
@click.prevent="handleAuthorize(true)"
|
||||||
>
|
>
|
||||||
<span v-if="!loading">同意授权</span>
|
<span v-if="!formLoading">同意授权</span>
|
||||||
<span v-else>授 权 中...</span>
|
<span v-else>授 权 中...</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" style="width: 36%" @click.prevent="handleAuthorize(false)"
|
<el-button class="w-3/10" @click.prevent="handleAuthorize(false)">拒绝</el-button>
|
||||||
>拒绝
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" name="SSOLogin" setup>
|
<script lang="ts" name="SSOLogin" setup>
|
||||||
// import LoginFormTitle from './LoginFormTitle.vue' // TODO 艿艿你看看要不要这个表头
|
import LoginFormTitle from './LoginFormTitle.vue'
|
||||||
import { authorize, getAuthorize, paramsType, scopesType } from '@/api/login'
|
import * as OAuth2Api from '@/api/login/oauth2'
|
||||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||||
|
const route = useRoute() // 路由
|
||||||
const { t } = useI18n()
|
const { currentRoute } = useRouter() // 路由
|
||||||
const ssoForm = ref() // 表单Ref
|
|
||||||
const { getLoginState, setLoginState } = useLoginState()
|
const { getLoginState, setLoginState } = useLoginState()
|
||||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.SSO)
|
|
||||||
const loginForm = reactive<{ scopes: scopesType }>({
|
const client = ref({
|
||||||
scopes: [] // 已选中的 scope 数组
|
// 客户端信息
|
||||||
|
name: '',
|
||||||
|
logo: ''
|
||||||
})
|
})
|
||||||
const params = reactive<paramsType>({
|
const queryParams = reactive({
|
||||||
// URL 上的 client_id、scope 等参数
|
// URL 上的 client_id、scope 等参数
|
||||||
responseType: '',
|
responseType: '',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
redirectUri: '',
|
redirectUri: '',
|
||||||
state: '',
|
state: '',
|
||||||
scopes: [] // 优先从 query 参数获取;如果未传递,从后端获取
|
scopes: [] // 优先从 query 参数获取;如果未传递,从后端获取
|
||||||
}) // 表单Ref
|
|
||||||
const client = ref({
|
|
||||||
// 客户端信息
|
|
||||||
name: '',
|
|
||||||
logo: ''
|
|
||||||
})
|
})
|
||||||
const loading = ref(false)
|
const ssoVisible = computed(() => unref(getLoginState) === LoginStateEnum.SSO) // 是否展示 SSO 登录的表单
|
||||||
const handleAuthorize = (approved) => {
|
const formData = reactive({
|
||||||
ssoForm.value.validate((valid) => {
|
scopes: [] // 已选中的 scope 数组
|
||||||
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(() => {
|
const formLoading = ref(false) // 表单是否提交中
|
||||||
loading.value = false
|
|
||||||
})
|
/** 初始化授权信息 */
|
||||||
})
|
const init = async () => {
|
||||||
}
|
|
||||||
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 = () => {
|
|
||||||
// 防止在没有登录的情况下循环弹窗
|
// 防止在没有登录的情况下循环弹窗
|
||||||
if (typeof route.query.client_id === 'undefined') return
|
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%20user.write
|
||||||
// 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read
|
// 例如说【自动授权通过】: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
|
queryParams.responseType = route.query.response_type as string
|
||||||
params.clientId = route.query.client_id as string
|
queryParams.clientId = route.query.client_id as string
|
||||||
params.redirectUri = route.query.redirect_uri as string
|
queryParams.redirectUri = route.query.redirect_uri as string
|
||||||
params.state = route.query.state as string
|
queryParams.state = route.query.state as string
|
||||||
if (route.query.scope) {
|
if (route.query.scope) {
|
||||||
params.scopes = (route.query.scope as string).split(' ')
|
queryParams.scopes = (route.query.scope as string).split(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
|
// 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
|
||||||
if (params.scopes.length > 0) {
|
if (queryParams.scopes.length > 0) {
|
||||||
doAuthorize(true, params.scopes, []).then((res) => {
|
const data = await doAuthorize(true, queryParams.scopes, [])
|
||||||
if (!res) {
|
if (data) {
|
||||||
console.log('自动授权未通过!')
|
location.href = data
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
location.href = res.data
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取授权页的基本信息
|
// 获取授权页的基本信息
|
||||||
getAuthorize(params.clientId).then((res) => {
|
const data = await OAuth2Api.getAuthorize(queryParams.clientId)
|
||||||
client.value = res.client
|
client.value = data.client
|
||||||
// 解析 scope
|
// 解析 scope
|
||||||
let scopes
|
let scopes
|
||||||
// 1.1 如果 params.scope 非空,则过滤下返回的 scopes
|
// 1.1 如果 params.scope 非空,则过滤下返回的 scopes
|
||||||
if (params.scopes.length > 0) {
|
if (queryParams.scopes.length > 0) {
|
||||||
scopes = []
|
scopes = []
|
||||||
for (const scope of res.scopes) {
|
for (const scope of data.scopes) {
|
||||||
if (params.scopes.indexOf(scope.key) >= 0) {
|
if (queryParams.scopes.indexOf(scope.key) >= 0) {
|
||||||
scopes.push(scope)
|
scopes.push(scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它
|
// 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它
|
||||||
} else {
|
} else {
|
||||||
scopes = res.scopes
|
scopes = data.scopes
|
||||||
for (const scope of scopes) {
|
for (const scope of scopes) {
|
||||||
params.scopes.push(scope.key)
|
queryParams.scopes.push(scope.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 生成已选中的 checkedScopes
|
// 生成已选中的 checkedScopes
|
||||||
for (const scope of scopes) {
|
for (const scope of scopes) {
|
||||||
if (scope.value) {
|
if (scope.value) {
|
||||||
loginForm.scopes.push(scope.key)
|
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(
|
watch(
|
||||||
() => currentRoute.value,
|
() => currentRoute.value,
|
||||||
(route: RouteLocationNormalizedLoaded) => {
|
(route: RouteLocationNormalizedLoaded) => {
|
||||||
@ -183,5 +183,4 @@ watch(
|
|||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
init()
|
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
Reference in New Issue
Block a user