最近我在使用Vben admin模板做一些内容,对于登录不太想自己维护一个用户中心,考虑到未来的扩展,还是想有一个SSO,于是采用keycloak来做统一的SSO。

这样的话意味着要在Vben里面实现keycloak的集成,由于Vben是Vue3版本,所以需要做一些修改,整体的工作内容如下:

安装keycloak的npm 依赖:

npm i @dsb-norge/vue-keycloak-js --save

新增文件:src/keyCloak.ts:

import type { App } from 'vue';
import keycloak from '@dsb-norge/vue-keycloak-js';

export function setupKeyCloak(app: App<Element>, bootstrap) {
app.use(keycloak, {
init: {
onLoad: 'login-required',
checkLoginIframe: true, //防止登陆后重复刷新
},
config: {
url: 'http://60.215.255.22:8088/auth', // 你的keycloak地址
realm: 'OM',
clientId: 'om-cli',
},
onReady: (keycloak) => {
app.config.globalProperties.$keycloak = keycloak;
console.log(keycloak);
keycloak.loadUserProfile().success((data) => {
console.log(data);
bootstrap(keycloak.token);
});
},
});
}

修改:src/main.ts:

import 'virtual:windi-base.css';
import 'virtual:windi-components.css';
import '/@/design/index.less';
import 'virtual:windi-utilities.css';
// Register icon sprite
import 'virtual:svg-icons-register';
import App from './App.vue';
import { createApp } from 'vue';
import { initAppConfigStore } from '/@/logics/initAppConfig';
import { setupErrorHandle } from '/@/logics/error-handle';
import { router, setupRouter } from '/@/router';
import { setupRouterGuard } from '/@/router/guard';
import { setupStore } from '/@/store';
import { setupGlobDirectives } from '/@/directives';
import { setupI18n } from '/@/locales/setupI18n';
import { registerGlobComp } from '/@/components/registerGlobComp';
import { setupKeyCloak } from '/@/keyCloak';

// 配置 keycloak
import { useUserStore } from '/@/store/modules/user';
const app = createApp(App);
setupKeyCloak(app, bootstrap);

async function bootstrap(token) {
// const app = createApp(App);

// Configure store
// 配置 store
setupStore(app);

// Initialize internal system configuration
// 初始化内部系统配置
initAppConfigStore();

// Register global components
// 注册全局组件
registerGlobComp(app);

// Multilingual configuration
// 多语言配置
// Asynchronous case: language files may be obtained from the server side
// 异步案例:语言文件可能从服务器端获取
await setupI18n(app);

// Configure routing
// 配置路由
setupRouter(app);

// router-guard
// 路由守卫
setupRouterGuard(router);

// Register global directive
// 注册全局指令
setupGlobDirectives(app);

// Configure global error handling
// 配置全局错误处理
setupErrorHandle(app);

// https://next.router.vuejs.org/api/#isready
// await router.isReady();

// 登陆方法修改
const userStore = useUserStore();
userStore.loginX(token);

app.mount('#app');
}

添加loginX:src/store/modules/user.ts:
添加logoutX:src/store/modules/user.ts:
修改confirmLoginOut:src/store/modules/user.ts:

async loginX(token): Promise<GetUserInfoModel | null> {
try {
this.setToken(token);
return this.afterLoginAction(true);
} catch (error) {
return Promise.reject(error);
}
},
async logoutX(goLogin = false) {
this.setToken(undefined);
this.setUserInfo(null);
console.log('退出成功');
goLogin && router.replace(PageEnum.BASE_HOME);
// goLogin && router.push(PageEnum.BASE_LOGIN);
},

/**
* @description: Confirm before logging out
*/
confirmLoginOut(globalProperties) {
const { createConfirm } = useMessage();
const { t } = useI18n();
createConfirm({
iconType: 'warning',
title: () => h('span', t('sys.app.logoutTip')),
content: () => h('span', t('sys.app.logoutMessage')),
onOk: async () => {
await this.logoutX(true);
await globalProperties.$keycloak.logout();
},
});
},

修改界面的退出登录:src/layouts/default/header/components/user-dropdown/index.vue:

<template>
<Dropdown placement="bottomLeft" :overlayClassName="`${prefixCls}-dropdown-overlay`">
<span :class="[prefixCls, `${prefixCls}--${theme}`]" class="flex">
<img :class="`${prefixCls}__header`" :src="getUserInfo.avatar" />
<span :class="`${prefixCls}__info hidden md:block`">
<span :class="`${prefixCls}__name `" class="truncate">
{{ getUserInfo.realName }}
</span>
</span>
</span>

<template #overlay>
<Menu @click="handleMenuClick">
<MenuItem
key="doc"
:text="t('layout.header.dropdownItemDoc')"
icon="ion:document-text-outline"
v-if="getShowDoc"
/>
<MenuDivider v-if="getShowDoc" />
<MenuItem
v-if="getUseLockPage"
key="lock"
:text="t('layout.header.tooltipLock')"
icon="ion:lock-closed-outline"
/>
<MenuItem
key="logout"
:text="t('layout.header.dropdownItemLoginOut')"
icon="ion:power-outline"
/>
</Menu>
</template>
</Dropdown>
<LockAction @register="register" />
</template>
<script lang="ts">
// components
import { Dropdown, Menu } from 'ant-design-vue';
import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';

import { defineComponent, computed, getCurrentInstance } from 'vue';

import { DOC_URL } from '/@/settings/siteSetting';

import { useUserStore } from '/@/store/modules/user';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useI18n } from '/@/hooks/web/useI18n';
import { useDesign } from '/@/hooks/web/useDesign';
import { useModal } from '/@/components/Modal';

import headerImg from '/@/assets/images/header.jpg';
import { propTypes } from '/@/utils/propTypes';
import { openWindow } from '/@/utils';

import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';

type MenuEvent = 'logout' | 'doc' | 'lock';

export default defineComponent({
name: 'UserDropdown',
components: {
Dropdown,
Menu,
MenuItem: createAsyncComponent(() => import('./DropMenuItem.vue')),
MenuDivider: Menu.Divider,
LockAction: createAsyncComponent(() => import('../lock/LockModal.vue')),
},
props: {
theme: propTypes.oneOf(['dark', 'light']),
},
setup() {
const internalInstance = getCurrentInstance();
const { prefixCls } = useDesign('header-user-dropdown');
const { t } = useI18n();
const { getShowDoc, getUseLockPage } = useHeaderSetting();
const userStore = useUserStore();

const getUserInfo = computed(() => {
const { realName = '', avatar, desc } = userStore.getUserInfo || {};
return { realName, avatar: avatar || headerImg, desc };
});

const [register, { openModal }] = useModal();

function handleLock() {
openModal(true);
}

// d
function handleLoginOut() {
userStore.confirmLoginOut(internalInstance?.appContext.config.globalProperties);
}

// open doc
function openDoc() {
openWindow(DOC_URL);
}

function handleMenuClick(e: MenuInfo) {
switch (e.key as MenuEvent) {
case 'logout':
handleLoginOut();
break;
case 'doc':
openDoc();
break;
case 'lock':
handleLock();
break;
}
}

return {
prefixCls,
t,
getUserInfo,
handleMenuClick,
getShowDoc,
register,
getUseLockPage,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-header-user-dropdown';

.@{prefix-cls} {
height: @header-height;
padding: 0 0 0 10px;
padding-right: 10px;
overflow: hidden;
font-size: 12px;
cursor: pointer;
align-items: center;

img {
width: 24px;
height: 24px;
margin-right: 12px;
}

&__header {
border-radius: 50%;
}

&__name {
font-size: 14px;
}

&--dark {
&:hover {
background-color: @header-dark-bg-hover-color;
}
}

&--light {
&:hover {
background-color: @header-light-bg-hover-color;
}

.@{prefix-cls}__name {
color: @text-color-base;
}

.@{prefix-cls}__desc {
color: @header-light-desc-color;
}
}

&-dropdown-overlay {
.ant-dropdown-menu-item {
min-width: 160px;
}
}
}
</style>

这样的话,就能在vben里面使用keycloak做统一的sso了。

version: '3'

volumes:
mysql_data:
driver: local

services:
mysql:
image: mysql:5.7
volumes:
- mysql_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: keycloak
MYSQL_USER: keycloak
MYSQL_PASSWORD: password
keycloak:
image: quay.io/keycloak/keycloak:legacy
environment:
DB_VENDOR: MYSQL
DB_ADDR: mysql
DB_DATABASE: keycloak
DB_USER: keycloak
DB_PASSWORD: password
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: Pa55w0rd
# Uncomment the line below if you want to specify JDBC parameters. The parameter below is just an example, and it shouldn't be used in production without knowledge. It is highly recommended that you read the MySQL JDBC driver documentation in order to use it.
#JDBC_PARAMS: "connectTimeout=30000"
ports:
- 8080:8080
depends_on:
- mysql

我是使用如上的方式启动了keycloak。

response 的结构体是这样的:

{
"id": "8a7e6c4d-84f1-4103-9488-29c6f187cf68",
"username": "fcbai",
"firstName": "fcbai",
"lastName": "fcbai",
"email": "fcbai@fcbai.com",
"emailVerified": false,
"userProfileMetadata": {
"attributes": [
{
"name": "username",
"displayName": "${username}",
"required": true,
"readOnly": true,
"validators": {}
},
{
"name": "email",
"displayName": "${email}",
"required": true,
"readOnly": false,
"validators": {
"email": {
"ignore.empty.value": true
}
}
},
{
"name": "firstName",
"displayName": "${firstName}",
"required": true,
"readOnly": false,
"validators": {}
},
{
"name": "lastName",
"displayName": "${lastName}",
"required": true,
"readOnly": false,
"validators": {}
}
]
},
"attributes": {}
}

可以参考下。


扫码手机观看或分享: