keycloak本地运行集成springboot+vue项目(demo)
下载和运行
Keycloak 26.3.3 released - Keycloak 首先下载keycloaka官方最新版
终端打开keycloak-26.3.3目录
运行:
bin/kc.sh start-dev

访问http://localhost:8080/ 创建用户 并登陆
打开Keycloak 管理控制台
将客户端页面设置为中文
点击 Realm settings 打开Internationalization开关 在Supported locales中选择简体中文 并设置为Default locales

点击save后 刷新页面 就能看到页面已经切换为中文

修改keycloak内置数据库
keycloak是默认自带一个 内置 H2 数据库的 但是为了能更清楚的看到keycloak需要什么样的表结构(便于理解) 所以我决定把keycloak使用的数据库改为我本地的mysql
首先需要在本地mysql中创建一个名为keycloak的数据库
然后进入keycloak文件夹的/conf 目录下
打开keycloak.conf
将数据源配置修改为下图(原文件这几行都是注释掉的 放开注释并修改)

重启keycloak
这时候刷新刚才本地新建的keycloak数据库
可以看到启动keycloak后创建了很多新表 说明数据源配置成功

简单demo(sringboot+vue)集成keycloak
keycloak客户端配置
进入keycloak的管理端 创建一个新领域给我们的demo用

命名为demo-realm

在demo-realm领域下 创建客户端

命名为demo-client
OpenID Connect : 用于 Web 应用、移动应用、SPA 等,支持 OAuth 2.0 和 OIDC 标准流程
点击下一步

保证标准流程启用

设置重定向路径
1️⃣ 根网址 (Root URL)
定义:客户端的主入口网址,一般是你前端应用或者后端应用的基础 URL
作用:Keycloak 用它来生成默认的重定向 URI 或者作为参考判断访问请求是否合法
2️⃣ 主页 URL (Home URL / Base URL)
定义:用户登录成功后默认跳转的页面 URL
作用:如果登录完成后没有指定 redirect_uri,Keycloak 会跳到这个主页 URL
3️⃣ 有效的重定向 URI (Valid Redirect URIs)
定义:OAuth2 / OIDC 登录完成后 Keycloak 允许回调的 URI 列表。
作用:安全检查,防止恶意站点重定向。Keycloak 只会重定向到这个白名单里的地址
4️⃣ 有效的注销后重定向 URI (Valid Post Logout Redirect URIs)
定义:用户退出登录(logout)后,Keycloak 允许跳转回的 URL 白名单
作用:用户登出 Keycloak 后,如果想回到前端页面或后端页面,需要在这里注册
5️⃣ 网络根源 (Web Origins)
定义:允许浏览器前端发起跨域请求的源列表(Origin)
作用:解决前端 SPA 和 Keycloak 跨域访问问题(CORS),尤其是 JS SDK 或 fetch/axios 调用 Keycloak 的接口时
6️⃣ 管理网址 (Admin URL)
定义:客户端管理后台 URL,用于 Keycloak 向客户端发送管理事件或通知
作用:主要用于某些服务端通知或自动化操作,一般前端 SPA 不需要配置

给领域创建用户(马上测试过程中登陆的用户)

填写相应信息

给demouser设置密码

创建demo项目
后端

package com.demo.sso;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true); // 允许 cookie
}
};
}
}
package com.demo.sso;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
)
.oauth2Login() // 启用 OAuth2 登录
.defaultSuccessUrl("http://localhost:5173/", true) // 登录成功后重定向前端主页
.and()
.logout()
.logoutSuccessUrl("http://localhost:5173/") // 登出后回到前端主页
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID");
return http.build();
}
}
package com.demo.sso;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
@RestController
public class UserController {
@GetMapping("/api/user")
public Map<String, Object> userInfo(@AuthenticationPrincipal OidcUser user) {
if (user == null) {
return Map.of("error", "not authenticated");
}
return Map.of(
"name", user.getFullName(),
"email", user.getEmail(),
"attributes", user.getAttributes()
);
}
// 自定义登出接口
@GetMapping("/api/logout")
public void logout(@AuthenticationPrincipal OidcUser oidcUser, HttpServletRequest request, HttpServletResponse response) throws IOException {
Logger logger = LoggerFactory.getLogger(getClass());
// 清理本地 Session
request.getSession().invalidate();
logger.info("Session 已清理");
if (oidcUser != null) {
// 获取当前用户的 ID Token
String idToken = oidcUser.getIdToken().getTokenValue();
logger.info("当前用户 ID Token: {}", idToken);
// 构造 Keycloak 自动登出 URL
String redirectUri = URLEncoder.encode("http://localhost:5173/", StandardCharsets.UTF_8);
String keycloakLogoutUrl = "http://localhost:8080/realms/demo-realm/protocol/openid-connect/logout"
+ "?id_token_hint=" + idToken
+ "&post_logout_redirect_uri=" + redirectUri;
logger.info("重定向到 Keycloak Logout URL: {}", keycloakLogoutUrl);
response.sendRedirect(keycloakLogoutUrl);
logger.info("sendRedirect 已调用");
} else {
logger.warn("未获取到 OidcUser,无法自动登出 Keycloak");
response.sendRedirect("http://localhost:5173/"); // 回首页
}
}
}
package com.demo.sso;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SsoKeycloakDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SsoKeycloakDemoApplication.class, args);
}
}
前端

App.vue
<template>
<router-view />
</template>
<script setup>
</script>
Home.vue
<template>
<div>
<h1>Keycloak SSO 演示</h1>
<!-- 已登录 -->
<div v-if="user">
<h2>用户信息</h2>
<pre>{{ user }}</pre>
<button @click="logout">退出登录</button>
</div>
<!-- 未登录 -->
<div v-else>
<p>您尚未登录,请点击下面按钮登录:</p>
<button @click="login">通过 Keycloak 登录</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
const user = ref(null)
// 检查用户是否已登录(只在 Home 页面加载时执行)
const fetchUser = async () => {
try {
const res = await axios.get('http://localhost:8081/api/user', { withCredentials: true })
if (!res.data.error) {
user.value = res.data
}
} catch (e) {
// 用户未登录,不会再重复发请求导致 CORS 错误
console.log('用户未登录')
}
}
// 点击登录:重定向到后端 OAuth2 授权端点
const login = () => {
window.location.href = 'http://localhost:8081/oauth2/authorization/keycloak'
}
// 点击登出:先后端登出,再重定向到 Keycloak logout 清除 SSO
const logout = async () => {
try {
// 调用自定义后端登出接口
window.location.href = 'http://localhost:8081/api/logout'
} catch (e) {
console.log('登出失败', e)
}
}
// 页面加载时尝试获取用户信息
onMounted(fetchUser)
</script>
main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
router.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from './Home.vue'
const routes = [
{ path: '/', component: Home }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
index.html
import { createRouter, createWebHistory } from 'vue-router'
import Home from './Home.vue'
const routes = [
{ path: '/', component: Home }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
package.json
{
"name": "vue-keycloak-demo",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"axios": "^1.11.0",
"oidc-client": "^1.11.5",
"vue": "^3.3.4",
"vue-router": "^4.2.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.3.2",
"vite": "^5.4.19"
}
}
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: { port: 5173 }
})
由于添加了登出后重定向 所以keycloak客户端中需要添加配置

运行并测试

点击登录按钮 页面会跳转到keycloak的登陆页面
用刚才在demo-realm中创建的用户登陆

点击登录后重定向回首页并显示登陆用户的信息

再点击退出登录 调用接口直接执行keycloak的登出后 重定向回首页

这里有一个需要注意的问题
一开始我的登出接口中重定向路径是用redirect_uri来拼接的

点击登出按钮后 跳转到下图页面
可以看到url就是后端拼接的路径

但是这个登出页面需要再次点击Logout按钮实现登出
点击按钮后url又跳转到logout-confirm了 此时就不是我后端拼接好的url了 也就没有重定向拼接了
所以虽然 成功实现了注销登陆 但是并没有跳转回主页

查了一下 说应该用post_logout_redirect_uri参数拼接登出后重定向路径
直接修改后端代码后 看浏览器 虽然跳转了我写的url 但是页面显示keycloak的报错 缺少id_token_hint

搜索后得知redirect_uri 和 post_logout_redirect_uri 运作的区别

所以将后端代码改成下图 获取id_token_hint并传入

最后的实现效果就像上面说的 点击注销登陆后直接执行登出 不用点击确认登出按钮 并且登出后返回首页
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)