Java框架快速入门: Spring Security+OAuth2之单点登录(SSO)实战

纲要

  • SSO 与 OAuth2 基础
    • 单点登录的含义:多个客户端共享同一认证状态
    • Spring Security 作为 OAuth2 客户端,Keycloak 作为授权服务器
  • Keycloak 客户端配置
    • 创建两个 Clientsso-client-1sso-client-2
    • 配置访问类型、重定向 URI、客户端密钥
  • Spring Security 客户端工程
    • 两个独立但结构相同的 Spring Boot 应用(端口 80818082
    • 依赖 spring-boot-starter-oauth2-client
  • 核心配置
    • application.yml 中注册 client-idclient-secretprovider 信息
    • 安全配置只需开启 oauth2Login()
  • 运行效果
    • 访问任意客户端受保护资源 → 重定向至 Keycloak 统一登录页
    • 在一个客户端登录后,另一个客户端无需再次认证即可访问
  • 技术要点
    • 授权码模式,基于 Session
    • registrationprovider 配置细节

SSO 单点登录流程

单点登录允许用户在多个独立的应用(客户端)之间共享登录状态。一旦用户在统一认证中心完成登录,其他已注册的客户端将不再要求重复认证。本博客演示如何使用 Spring Security OAuth2 Client 连接 Keycloak 实现该模式。
Keycloak (8080) Client2 (8082) Client1 (8081) Browser Keycloak (8080) Client2 (8082) Client1 (8081) Browser #mermaid-svg-5BNAOPdEypK1Vdfs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5BNAOPdEypK1Vdfs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5BNAOPdEypK1Vdfs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5BNAOPdEypK1Vdfs .error-icon{fill:#552222;}#mermaid-svg-5BNAOPdEypK1Vdfs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5BNAOPdEypK1Vdfs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5BNAOPdEypK1Vdfs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5BNAOPdEypK1Vdfs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5BNAOPdEypK1Vdfs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5BNAOPdEypK1Vdfs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5BNAOPdEypK1Vdfs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5BNAOPdEypK1Vdfs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5BNAOPdEypK1Vdfs .marker.cross{stroke:#333333;}#mermaid-svg-5BNAOPdEypK1Vdfs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5BNAOPdEypK1Vdfs p{margin:0;}#mermaid-svg-5BNAOPdEypK1Vdfs .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5BNAOPdEypK1Vdfs text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-5BNAOPdEypK1Vdfs .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5BNAOPdEypK1Vdfs .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-5BNAOPdEypK1Vdfs .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-5BNAOPdEypK1Vdfs .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-5BNAOPdEypK1Vdfs #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-5BNAOPdEypK1Vdfs .sequenceNumber{fill:white;}#mermaid-svg-5BNAOPdEypK1Vdfs #sequencenumber{fill:#333;}#mermaid-svg-5BNAOPdEypK1Vdfs #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-5BNAOPdEypK1Vdfs .messageText{fill:#333;stroke:none;}#mermaid-svg-5BNAOPdEypK1Vdfs .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5BNAOPdEypK1Vdfs .labelText,#mermaid-svg-5BNAOPdEypK1Vdfs .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-5BNAOPdEypK1Vdfs .loopText,#mermaid-svg-5BNAOPdEypK1Vdfs .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-5BNAOPdEypK1Vdfs .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5BNAOPdEypK1Vdfs .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-5BNAOPdEypK1Vdfs .noteText,#mermaid-svg-5BNAOPdEypK1Vdfs .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-5BNAOPdEypK1Vdfs .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5BNAOPdEypK1Vdfs .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5BNAOPdEypK1Vdfs .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5BNAOPdEypK1Vdfs .actorPopupMenu{position:absolute;}#mermaid-svg-5BNAOPdEypK1Vdfs .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-5BNAOPdEypK1Vdfs .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5BNAOPdEypK1Vdfs .actor-man circle,#mermaid-svg-5BNAOPdEypK1Vdfs line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-5BNAOPdEypK1Vdfs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 访问受保护资源 302 重定向至 Keycloak 登录 请求登录页面 返回登录表单 提交凭证 (testuser/12345678) 302 重定向回 Client1 (携带授权码) 发送授权码 用授权码换取令牌 返回 access_token 登录成功,返回受保护资源 访问受保护资源 302 重定向至 Keycloak 已有 Session,直接颁发授权码 302 重定向回 Client2 发送授权码 换取令牌 返回 access_token 登录成功,返回资源

Keycloak 客户端创建

在使用 Spring Security 连接 Keycloak 之前,需要在授权服务器上注册客户端。本示例使用 Keycloakdemo 域(Realm)。

创建 Client 步骤

  1. 登录 Keycloak 管理控制台,进入目标 Realm。
  2. 创建 Client,填写以下信息:
字段 Client 1 值 Client 2 值
Client ID sso-client-1 sso-client-2
Client Protocol openid-connect openid-connect
Access Type confidential confidential
Root URL http://localhost:8081 http://localhost:8082
Valid Redirect URIs /login/oauth2/code/* /login/oauth2/code/*
  1. Credentials 标签页中生成 Client Secret,并记录对应值(例如 client1-secretclient2-secret)。

注意Valid Redirect URIs 必须精确匹配,生产环境中避免使用通配符 *,但演示时可简化配置。

Spring Security 客户端工程

两个客户端项目结构完全一致,仅端口和 Client 信息不同。这里以 client1 为例展示完整代码。

项目结构

dir 复制代码
sso-client-1/
├── pom.xml
└── src/main/
    ├── java/com/example/sso/
    │   ├── SsoClient1Application.java
    │   └── controller/
    │       └── HomeController.java
    └── resources/
        ├── application.yml
        └── static/
            └── index.html

Maven 依赖 (pom.xml)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>sso-client-1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>sso-client-1</name>
    
    <properties>
        <java.version>11</java.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

应用主类

java 复制代码
package com.example.sso;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SsoClient1Application {
    public static void main(String[] args) {
        SpringApplication.run(SsoClient1Application.class, args);
    }
}

安全配置

Spring Security 5.1 之后,开启 OAuth2 客户端支持异常简洁。只需在配置中启用 oauth2Login() 即可自动完成授权码流程。

java 复制代码
package com.example.sso;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .antMatchers("/", "/index.html").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login();  // 核心:启用 OAuth2 登录
        return http.build();
    }
}

控制器

提供受保护的测试页面,用于验证登录状态。

java 复制代码
package com.example.sso.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.Map;

@RestController
public class HomeController {

    @GetMapping("/user")
    public Map<String, Object> user(@AuthenticationPrincipal OAuth2User principal) {
        return Collections.singletonMap("name", principal.getAttribute("preferred_username"));
    }

    @GetMapping("/content")
    public String content() {
        return "Welcome to Client 1's protected content!";
    }
}

静态首页

放置于 src/main/resources/static/index.html,提供简单的登录入口。

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Client 1</title>
</head>
<body>
    <h1>Client 1</h1>
    <a href="/content">登录并访问受保护内容</a>
</body>
</html>

Client 1 的 application.yml

yaml 复制代码
server:
  port: 8081

spring:
  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: sso-client-1
            client-secret: client1-secret
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: openid, profile, email
        provider:
          keycloak:
            issuer-uri: http://localhost:8080/realms/demo
            user-name-attribute: preferred_username

配置解释

  • registrationclient-idclient-secret 必须与 Keycloak 中创建的一致。
  • authorization-grant-typeauthorization_code,即授权码模式。
  • redirect-uri 使用模板自动拼接,无需硬编码。
  • providerissuer-uri 指向 Keycloak 的特定 Realm,Spring Security 会自动发现端点。
  • user-name-attribute 指定返回用户信息 JSON 中的用户名字段,此处为 preferred_username

Client 2 的差异化配置

Client 2 项目结构与 Client 1 完全一致,仅需修改端口和注册信息。

yaml 复制代码
server:
  port: 8082

spring:
  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: sso-client-2
            client-secret: client2-secret
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: openid, profile, email
        provider:
          keycloak:
            issuer-uri: http://localhost:8080/realms/demo
            user-name-attribute: preferred_username

将主类、安全配置、控制器中的类名和注释相应调整,但核心逻辑不变。两个应用启动后分别监听 80818082

运行与验证

  1. 确保 Keycloak 已启动并创建好对应的两个 Client。
  2. 分别启动 sso-client-1sso-client-2
  3. 访问 http://localhost:8081,点击登录链接,浏览器重定向至 http://localhost:8080 的 Keycloak 登录页。
  4. 输入凭据登录后,成功返回 Client 1 的受保护内容。
  5. 直接访问 http://localhost:8082,点击登录,浏览器短暂跳转后直接登录成功,无需再次输入密码。

单点登录生效的关键 :Keycloak 在第一次登录后与浏览器建立了 Session。当第二个客户端发起认证时,授权服务器检测到已有有效会话,直接颁发授权码,完成静默登录。

关键点总结

  • 在 Spring Security 中实现 OAuth2 SSO,只需引入 spring-boot-starter-oauth2-client,并配置 registrationprovider
  • 安全配置中调用 .oauth2Login() 即可激活整个授权码流程。
  • 多个客户端共享同一个授权服务器 Session,是实现单点登录的核心机制。
  • 所有配置信息与具体授权服务器解耦,符合 OAuth2 标准,因此替换 Keycloak 为其他提供商(如 Auth0、Okta)同样简单。

常见问题

Q:必须为每个客户端单独创建 Client 吗?

A:是的。OAuth2 协议要求每个客户端在授权服务器上独立注册,以确保安全隔离和回调 URI 的正确性。

Q:能否将授权服务器与资源服务器合一?

A:可以,但推荐分离部署。本例中 Keycloak 作为独立授权服务器,客户端只负责前端展示与用户资源访问。

Q:生产环境需要注意哪些?

A:重定向 URI 避免使用通配符,使用 HTTPS,妥善保管 Client Secret,并配置合理的 Token 有效期。

相关扩展

  • 自定义登录页面与权限
  • 结合 Spring Cloud Gateway 实现统一网关下的 SSO
  • 多租户、多 Realm 配置
  • 使用 JWT 进行资源服务器间的令牌传递
相关推荐
譕痕1 小时前
Idea 集成 智能体开发工具“千问(qwen)”
java·ide·spring·语言模型
聚搜云——JuSouClouD1 小时前
在阿里云代理商渠道买轻量服务器,带宽套餐支持自选吗?
服务器·阿里云·云计算
m0_634865402 小时前
2G2核服务器部署deep seekharness 困难?资源不够?DeepSeek Harness 部署复用笔记
运维·服务器·deepseek·harness
huainingning2 小时前
盈高安全准入设备与深信服实现单点登录对接配置
服务器·网络·安全
Anthony_2312 小时前
Nginx基础
服务器·nginx·http·https·edge浏览器·web
是枚小菜鸡儿吖2 小时前
Windows 和 iPhone 怎么互传文件?用 PairDrop 搭一个自己的浏览器传输入口
服务器·人工智能·大模型
步行cgn2 小时前
Spring 注入内部 Bean 详解
java·spring·rpc
网硕互联的小客服3 小时前
如何在Ubuntu系统上查看和刷新DNS缓存?操作方法与原理解析
运维·服务器·网络·ubuntu
聚搜云——JuSouClouD3 小时前
2026找阿里云代理商采购GPU服务器,有没有额外折扣?
服务器·阿里云·云计算