fix: 接入cas单点登录

x-20250106
chengmingrui 2025-09-23 18:16:59 +08:00
parent ad9a90e252
commit a4d103790d
11 changed files with 399 additions and 250 deletions

View File

@ -37,7 +37,7 @@ onMounted(() => {
}, 1000 * 2) }, 1000 * 2)
}) })
const showMainApp = computed(() => { const showMainApp = computed(() => {
return ['/login', '/sso', '/lockme', '/redirect', '/404', '/license'].includes(route.path) return ['/login', '/caslogin', '/sso', '/lockme', '/redirect', '/404', '/license'].includes(route.path)
}) })
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -1,4 +1,4 @@
@media screen and(-ms-high-contrast:active), @media screen and (-ms-high-contrast:active),
(-ms-high-contrast:none) { (-ms-high-contrast:none) {
.el-table__header, .el-table__header,
.el-table__body { .el-table__body {

View File

@ -1,169 +1,176 @@
<template> <template>
<a-dropdown> <a-dropdown>
<div class="user-content"> <div class="user-content">
<img :src="userData.portrait" class="head-portrait" /> <img :src="userData.portrait" class="head-portrait" />
<span class="user-name">{{ userData.name }}</span> <span class="user-name">{{ userData.name }}</span>
<DownOutlined /> <DownOutlined />
</div> </div>
<template #overlay> <template #overlay>
<a-menu class="user-dropdown"> <a-menu class="user-dropdown">
<a-menu-item @click="openInfoDialog" class="menu-item"> <UserOutlined /> 个人信息 </a-menu-item> <a-menu-item @click="openInfoDialog" class="menu-item"> <UserOutlined /> 个人信息 </a-menu-item>
<a-menu-item @click="openPwdDialog" class="menu-item"> <LockOutlined /> 修改密码 </a-menu-item> <a-menu-item @click="openPwdDialog" class="menu-item"> <LockOutlined /> 修改密码 </a-menu-item>
<a-menu-item @click="logoutSystem()" class="menu-item"> <PoweroffOutlined /> 退出系统 </a-menu-item> <a-menu-item @click="logoutSystem()" class="menu-item"> <PoweroffOutlined /> 退出系统 </a-menu-item>
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
<a-modal title="修改密码" :maskClosable="false" width="600px" v-if="pwdDialogVisible" v-model:visible="pwdDialogVisible" @ok="modifyPwdSubmit" :confirmLoading="loading"> <a-modal title="修改密码" :maskClosable="false" width="600px" v-if="pwdDialogVisible" v-model:visible="pwdDialogVisible" @ok="modifyPwdSubmit" :confirmLoading="loading">
<a-form :model="pwdData" ref="pwdRef" :colon="true" :labelCol="{ span: 4 }"> <a-form :model="pwdData" ref="pwdRef" :colon="true" :labelCol="{ span: 4 }">
<a-form-item label="原密码" name="oldPassword" :rules="[required]"> <a-form-item label="原密码" name="oldPassword" :rules="[required]">
<a-input-password v-model:value="pwdData.oldPassword" auto-complete="off"></a-input-password> <a-input-password v-model:value="pwdData.oldPassword" auto-complete="off"></a-input-password>
</a-form-item> </a-form-item>
<a-form-item label="新密码" name="newPassword" :rules="pwdRule"> <a-form-item label="新密码" name="newPassword" :rules="pwdRule">
<a-input-password v-model:value="pwdData.newPassword" auto-complete="off"></a-input-password> <a-input-password v-model:value="pwdData.newPassword" auto-complete="off"></a-input-password>
</a-form-item> </a-form-item>
<a-form-item label="确认密码" name="confirmPassword" :rules="pwdRule"> <a-form-item label="确认密码" name="confirmPassword" :rules="pwdRule">
<a-input-password v-model:value="pwdData.confirmPassword" auto-complete="off"></a-input-password> <a-input-password v-model:value="pwdData.confirmPassword" auto-complete="off"></a-input-password>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
<InfoDialog ref="infoRef" :data="userData"></InfoDialog> <InfoDialog ref="infoRef" :data="userData"></InfoDialog>
</template> </template>
<script lang="ts"> <script lang="ts">
import crypto from 'utils/crypto.js' import crypto from 'utils/crypto.js'
import { changePassword } from '@/services/manager' import { changePassword } from '@/services/manager'
import { logout } from 'services' import { logout } from 'services'
import InfoDialog from './InfoDialog.vue' import { getCasConfig } from '@/store/utils'
import { computed, defineComponent, ref, createVNode } from 'vue' import InfoDialog from './InfoDialog.vue'
import { message, Modal } from 'ant-design-vue' import { computed, defineComponent, ref, createVNode } from 'vue'
import { useStore } from 'vuex' import { message, Modal } from 'ant-design-vue'
import { required, complexPassword } from '@/validate' import { useStore } from 'vuex'
import { UserOutlined, PoweroffOutlined, LockOutlined, ExclamationCircleOutlined, DownOutlined } from '@ant-design/icons-vue' import { required, complexPassword } from '@/validate'
import { UserOutlined, PoweroffOutlined, LockOutlined, ExclamationCircleOutlined, DownOutlined } from '@ant-design/icons-vue'
export default defineComponent({
components: { InfoDialog, UserOutlined, PoweroffOutlined, LockOutlined, DownOutlined }, export default defineComponent({
setup() { components: { InfoDialog, UserOutlined, PoweroffOutlined, LockOutlined, DownOutlined },
const store = useStore() setup() {
// const store = useStore()
const pwdData = ref({ //
oldPassword: '', const pwdData = ref({
newPassword: '', oldPassword: '',
confirmPassword: '' newPassword: '',
}) confirmPassword: ''
const pwdDialogVisible = ref(false) })
function openPwdDialog() { const pwdDialogVisible = ref(false)
pwdDialogVisible.value = true function openPwdDialog() {
pwdData.value = { pwdDialogVisible.value = true
oldPassword: '', pwdData.value = {
newPassword: '', oldPassword: '',
confirmPassword: '' newPassword: '',
} confirmPassword: ''
} }
function checkPassword() { }
const { newPassword, oldPassword, confirmPassword } = pwdData.value function checkPassword() {
if (newPassword === oldPassword) { const { newPassword, oldPassword, confirmPassword } = pwdData.value
message.error('新密码不能与原密码相同') if (newPassword === oldPassword) {
return false message.error('新密码不能与原密码相同')
} return false
if (confirmPassword !== newPassword) { }
message.error('确认密码与新密码不一致') if (confirmPassword !== newPassword) {
return false message.error('确认密码与新密码不一致')
} return false
}
return true
} return true
const pwdRef = ref() }
const loading = ref(false) const pwdRef = ref()
async function modifyPwdSubmit() { const loading = ref(false)
try { async function modifyPwdSubmit() {
const values = await pwdRef.value.validate() try {
if (!checkPassword()) return const values = await pwdRef.value.validate()
loading.value = true if (!checkPassword()) return
const res = await changePassword(userData.value.id, { loading.value = true
password: crypto.encrypt(values.newPassword), const res = await changePassword(userData.value.id, {
oldPassword: crypto.encrypt(values.oldPassword) password: crypto.encrypt(values.newPassword),
}) oldPassword: crypto.encrypt(values.oldPassword)
if (res.success) { })
pwdDialogVisible.value = false if (res.success) {
message.success(res.message) pwdDialogVisible.value = false
store.dispatch('permission/ResetRoutes') message.success(res.message)
} store.dispatch('permission/ResetRoutes')
} catch (error) { }
console.log(error) } catch (error) {
} console.log(error)
loading.value = false }
} loading.value = false
const userData = computed(() => store.getters.userData || {}) }
const pwdRule = computed(() => { const userData = computed(() => store.getters.userData || {})
const rule = store.state.app.systemConfig.pwdStrength const pwdRule = computed(() => {
if (rule === 'required') return [required] const rule = store.state.app.systemConfig.pwdStrength
return [required, complexPassword] if (rule === 'required') return [required]
}) return [required, complexPassword]
// })
function logoutSystem() { //
Modal.confirm({ function logoutSystem() {
title: '提示', Modal.confirm({
icon: createVNode(ExclamationCircleOutlined), title: '提示',
content: '您确定要退出该系统吗?', icon: createVNode(ExclamationCircleOutlined),
async onOk() { content: '您确定要退出该系统吗?',
const res = await logout() async onOk() {
if (res.success) { const { isCASLogin, casLogoutPath } = await getCasConfig()
store.dispatch('permission/ResetRoutes') if (isCASLogin) {
} location.href = casLogoutPath
} } else {
}) const res = await logout()
} if (res.success) {
// await store.dispatch('permission/ResetRoutes', false)
const infoRef = ref() location.href = '/login'
function openInfoDialog() { }
infoRef.value.open() }
} }
return { })
loading, }
pwdData, //
pwdDialogVisible, const infoRef = ref()
pwdRef, function openInfoDialog() {
pwdRule, infoRef.value.open()
openPwdDialog, }
modifyPwdSubmit, return {
logoutSystem, loading,
infoRef, pwdData,
openInfoDialog, pwdDialogVisible,
userData, pwdRef,
required pwdRule,
} openPwdDialog,
} modifyPwdSubmit,
}) logoutSystem,
</script> infoRef,
<style lang="scss" scoped> openInfoDialog,
.user-content { userData,
margin-right: 10px; required
display: flex; }
align-items: center; }
cursor: pointer; })
.head-portrait { </script>
display: inline-block; <style lang="scss" scoped>
width: 24px; .user-content {
height: 24px; margin-right: 10px;
border-radius: 50%; display: flex;
} align-items: center;
} cursor: pointer;
.user-name { .head-portrait {
max-width: 140px; display: inline-block;
white-space: nowrap; width: 24px;
overflow: hidden; height: 24px;
text-overflow: ellipsis; border-radius: 50%;
font-size: 14px; }
margin: 0 2px 0 5px; }
} .user-name {
.user-dropdown { max-width: 140px;
background: #2c2e3b; white-space: nowrap;
border-color: #2c2e3b; overflow: hidden;
::v-deep(.ant-dropdown-menu-item) { text-overflow: ellipsis;
color: #ccc; font-size: 14px;
&:hover { margin: 0 2px 0 5px;
background: #2d8cf0 !important; }
color: #fff; .user-dropdown {
} background: #2c2e3b;
} border-color: #2c2e3b;
} ::v-deep(.ant-dropdown-menu-item) {
</style> color: #ccc;
&:hover {
background: #2d8cf0 !important;
color: #fff;
}
}
}
</style>

View File

@ -1,52 +1,81 @@
/** /**
* Created by HaijunZhang on 2018/11/12. * Created by HaijunZhang on 2018/11/12.
*/ */
import store from './store' import store from './store'
import router from './router' import router from './router'
import { getToken, setToken } from 'utils/auth' import { getToken, setToken } from 'utils/auth'
import { getQuery } from 'utils' import { getQuery } from 'utils'
import { isEmpty, assign } from 'lodash-es' import { isEmpty, assign } from 'lodash-es'
import { getCasConfig, goLogin } from './store/utils'
const { token } = getQuery(location.hash)
if (token) { const redirectToCasLogin = (to, next, ticket, casLoginPath) => {
setToken(token) if (!ticket) {
} location.href = casLoginPath
return
const whiteList = ['/login', '/404', '/401', '/license', '/sso'] }
router.beforeEach(async (to, from, next) => { if (to.path === '/caslogin') {
if (isEmpty(history.state.current)) { next()
assign(history.state, { current: from.fullPath }) return
} }
if (getToken()) { next(`/caslogin?ticket=${ticket}`)
// 判断用户是否处于登录状态 }
if (to.path === '/login') {
// 如果已经登录重定向到主页 const { token } = getQuery(location.hash)
await store.dispatch('permission/ResetRoutes', false) if (token) {
next('/login') setToken(token)
} else { }
// 为null的场景 刷新页面或者新开窗口;
const addRoutes = store.getters.addRoutes const whiteList = ['/login', '/caslogin', '/404', '/401', '/license', '/sso']
if (addRoutes) { router.beforeEach(async (to, from, next) => {
next() const ticket = to.query.ticket || new URLSearchParams(location.search).get('ticket')
} else { if (isEmpty(history.state.current)) {
try { assign(history.state, { current: from.fullPath })
await store.dispatch('permission/GenerateRoutes') }
store.dispatch('GetUserInfo') if (getToken()) {
next({ ...to, replace: true }) // 判断用户是否处于登录状态
} catch (error) { if (to.path === '/login' || to.path === '/caslogin') {
// remove token and go to login page to re-login // 如果已经登录重定向到主页
await store.dispatch('permission/ResetRoutes', false) await store.dispatch('permission/ResetRoutes', false)
next('/login') const { isCASLogin, casLoginPath } = await getCasConfig()
} if (!isCASLogin) {
} next('/login')
} } else {
} else { redirectToCasLogin(to, next, ticket, casLoginPath)
// 用户没有登录 }
if (whiteList.includes(to.path)) { } else {
// 在白名单里直接跳转 // 为null的场景 刷新页面或者新开窗口;
next() const addRoutes = store.getters.addRoutes
} else { if (addRoutes) {
next('/login') next()
} } else {
} try {
}) await store.dispatch('permission/GenerateRoutes')
store.dispatch('GetUserInfo')
next({ ...to, replace: true })
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('permission/ResetRoutes', false)
const { isCASLogin, casLoginPath } = await getCasConfig()
if (!isCASLogin) {
next('/login')
} else {
redirectToCasLogin(to, next, ticket, casLoginPath)
}
}
}
}
} else {
// 用户没有登录
if (whiteList.includes(to.path)) {
// 在白名单里直接跳转
const { isCASLogin, casLoginPath } = await getCasConfig()
if (!isCASLogin) {
next()
} else {
redirectToCasLogin(to, next, ticket, casLoginPath)
}
} else {
goLogin()
}
}
})

View File

@ -5,7 +5,12 @@ const routes: RouteRecordRaw[] = [
{ {
name: 'Login', name: 'Login',
path: '/login', path: '/login',
component: () => import('views/login/login.vue') component: () => import('views/login/userLogin.vue')
},
{
name: 'CASLogin',
path: '/caslogin',
component: () => import('views/login/casLogin.vue')
}, },
{ {
path: '/sso', path: '/sso',

View File

@ -4,6 +4,10 @@
import request from 'utils/request' import request from 'utils/request'
import { wrapperParams } from 'utils' import { wrapperParams } from 'utils'
export function loginByCas(params) {
return request.post('/sms/v1/users/login/cas', wrapperParams(params))
}
export function login(params) { export function login(params) {
return request.post('/sms/v1/users/login', params) return request.post('/sms/v1/users/login', params)
} }

View File

@ -7,7 +7,7 @@ import { getUserPermissions } from 'services'
import BlankView from '@/layouts/blank.vue' import BlankView from '@/layouts/blank.vue'
import router, { resetRouter } from '@/router' import router, { resetRouter } from '@/router'
import { menuKey, enablePermissionStorage } from '@/config' import { menuKey, enablePermissionStorage } from '@/config'
import { urlToList } from '../utils' import { urlToList, goLogin } from '../utils'
import actionStore from '@/core/actions' import actionStore from '@/core/actions'
const resultRoutes = [] const resultRoutes = []
@ -167,7 +167,7 @@ const actions = {
// 重置标签信息 // 重置标签信息
dispatch('tagsView/delAllViews', null, { root: true }) dispatch('tagsView/delAllViews', null, { root: true })
removeToken() removeToken()
if (redirectToLogin) window.location.href = '/login' if (redirectToLogin) goLogin()
resolve() resolve()
}) })
} }

View File

@ -1,14 +1,45 @@
export const setBrowser = (icon, title) => { import { getSystemConfigs } from 'services'
const head = document.getElementsByTagName('head')[0] import store from '@/store'
const linkTag = document.createElement('link') export const setBrowser = (icon, title) => {
linkTag.href = icon const head = document.getElementsByTagName('head')[0]
linkTag.setAttribute('rel', 'shortcut icon') const linkTag = document.createElement('link')
linkTag.setAttribute('type', 'image/x-icon') linkTag.href = icon
head.appendChild(linkTag) linkTag.setAttribute('rel', 'shortcut icon')
document.title = title linkTag.setAttribute('type', 'image/x-icon')
} head.appendChild(linkTag)
// /system/configs/page -> [ '/system','/system/configs','/system/configs/page'] document.title = title
export const urlToList = (url) => { }
const urllist = url.split('/').filter((i) => i) // /system/configs/page -> [ '/system','/system/configs','/system/configs/page']
return urllist.map((urlItem, index) => `/${urllist.slice(0, index + 1).join('/')}`) export const urlToList = (url) => {
} const urllist = url.split('/').filter((i) => i)
return urllist.map((urlItem, index) => `/${urllist.slice(0, index + 1).join('/')}`)
}
export const getPageConfigs = async () => {
const res = await getSystemConfigs({ category: '界面配置' })
if (res.success) {
return res.data
}
throw res.data
}
export const getCasConfig = async () => {
const Store = store
let pageConfig = Store?.getters?.pageConfig || {}
if (!Object.keys(pageConfig).length) {
pageConfig = (await getPageConfigs()) || {}
}
const { cas, casLoginUrl, casLoginoutUrl } = pageConfig
const isCASLogin = (cas === 'true' || cas === true) && !!casLoginUrl
const casLoginPath = `${casLoginUrl}?service=${location.origin}/caslogin`
const casLogoutPath = `${casLoginoutUrl}?service=${location.origin}/caslogin`
return { isCASLogin, cas, casLoginPath, casLogoutPath }
}
export const goLogin = async (forceCasRedirect = false) => {
const { isCASLogin, casLoginPath } = await getCasConfig()
let path = '/login'
// forceCasRedirect avoids reusing stale CAS tickets during logout or failed auth attempts
const ticket = forceCasRedirect ? null : new URLSearchParams(location.search).get('ticket')
if (isCASLogin) {
path = ticket ? `/caslogin?ticket=${ticket}` : casLoginPath
}
window.location.href = path
}

View File

@ -16,6 +16,8 @@ import 'nprogress/nprogress.css'
import { notification } from 'ant-design-vue' import { notification } from 'ant-design-vue'
import { getToken } from 'utils/auth' import { getToken } from 'utils/auth'
import store from '@/store' import store from '@/store'
import { getCasConfig, goLogin } from '@/store/utils'
import { logout } from '@/services'
const codeMessage = { const codeMessage = {
200: '服务器成功返回请求的数据。', 200: '服务器成功返回请求的数据。',
@ -92,9 +94,17 @@ axiosInstance.interceptors.response.use(
break break
case '401': case '401':
case '509': case '509':
store.dispatch('permission/ResetRoutes') store.dispatch('permission/ResetRoutes', false).then(() => {
break getCasConfig().then(({ isCASLogin, casLogoutPath }) => {
default: if (isCASLogin) {
location.href = casLogoutPath
} else {
logout().then((res) => {
if (res.success) goLogin()
})
}
})
})
} }
if (!options.ignoreError) { if (!options.ignoreError) {
notification.error({ notification.error({

View File

@ -0,0 +1,61 @@
<template>
<div class="casLogin">{{ msg }}</div>
</template>
<script>
import { loginByCas } from 'services'
import setLoginData from './tools'
import { removeToken } from 'utils/auth'
import { goLogin } from '@/store/utils'
export default {
data() {
return {
msg: '',
url: window.location.href
}
},
created() {
this.init()
},
methods: {
init() {
let ticket = this.getUrlKey('ticket')
if (!ticket || ticket == '') {
const { redirect } = this.$route.query
const path = redirect ? redirect.split('/#')[1] : '/sms-web/resource_dashboard'
this.$router.replace({ path })
return
}
const service = `${window.location.origin}/caslogin`
this.login(ticket, service)
},
getUrlKey(name) {
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(this.url) || ['', ''])[1].replace(/\+/g, '%20')) || null
},
handleLoginAfter(data) {
setLoginData(data)
const { redirect } = this.$route.query
const path = redirect ? redirect.split('/#')[1] : '/sms-web/resource_dashboard'
this.$router.replace(path)
localStorage.removeItem('lockData')
},
login(ticket, service) {
loginByCas({ ticket, service })
.then((res) => {
if (res.success) {
this.handleLoginAfter(res.data)
this.msg = res.message
} else {
console.log(res.message, 'res.message')
goLogin(true)
}
})
.catch(() => {
this.msg = '登录失败'
removeToken()
goLogin(true)
})
}
}
}
</script>

View File

@ -59,10 +59,10 @@ export default {
remember: false, remember: false,
loginForm: { loginForm: {
account: '', account: '',
password: '', password: ''
}, },
loading: false, loading: false,
capsTooltip: false, capsTooltip: false
}) })
const store = useStore() const store = useStore()
const configs = computed(() => store.getters.pageConfig) const configs = computed(() => store.getters.pageConfig)
@ -84,7 +84,7 @@ export default {
if (state.remember) { if (state.remember) {
const obj = { const obj = {
account: state.loginForm.account, account: state.loginForm.account,
password: encrypt(state.loginForm.password), password: encrypt(state.loginForm.password)
} }
localStorage.setItem('cmcLoginData', JSON.stringify(obj)) localStorage.setItem('cmcLoginData', JSON.stringify(obj))
} else { } else {
@ -104,12 +104,14 @@ export default {
const res = await login({ const res = await login({
account, account,
password: encrypt(password), password: encrypt(password),
isManager: true, isManager: true
}) })
if (res.success) { if (res.success) {
goLogin(res.data) goLogin(res.data)
} }
} catch (error) {} } catch (error) {
console.log(error)
}
state.loading = false state.loading = false
} }
@ -131,9 +133,9 @@ export default {
configs, configs,
loginFormRef, loginFormRef,
handleLogin, handleLogin,
checkCapslock, checkCapslock
} }
}, }
} }
</script> </script>