下载和运行

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客户端中需要添加配置

运行并测试

访问http://localhost:5173/

点击登录按钮 页面会跳转到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并传入

最后的实现效果就像上面说的 点击注销登陆后直接执行登出 不用点击确认登出按钮 并且登出后返回首页

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐