SpringSecurity中如何接入单点登录

SpringSecurity中如何接入单点登录

基于 Spring Boot 3.2 + Spring Security 6 + OAuth2完整落地指南

授权服务器、资源服务器、客户端 三端代码,可直接运行


目录

  1. [背景:SSO vs OAuth2 vs Spring Security](#背景:SSO vs OAuth2 vs Spring Security "#%E8%83%8C%E6%99%AF")
  2. 整体架构
  3. 环境准备
  4. [搭建授权服务器(Authorization Server)](#搭建授权服务器(Authorization Server) "#%E6%90%AD%E5%BB%BA%E6%8E%88%E6%9D%83%E6%9C%8D%E5%8A%A1%E5%99%A8")
  5. [搭建资源服务器(Resource Server)](#搭建资源服务器(Resource Server) "#%E6%90%AD%E5%BB%BA%E8%B5%84%E6%BA%90%E6%9C%8D%E5%8A%A1%E5%99%A8")
  6. 搭建客户端(Client)
  7. [前后端分离下的 SSO](#前后端分离下的 SSO "#%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E4%B8%8B%E7%9A%84-sso")
  8. [常见安全问题 & 防护](#常见安全问题 & 防护 "#%E5%B8%B8%E8%A7%81%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98--%E9%98%B2%E6%8A%A4")
  9. 总结

背景:SSO vs OAuth2 vs Spring Security

  • SSO(Single Sign-On):一次登录,全网通行。
  • OAuth2 :授权协议,授权码模式 最适合实现 SSO。
  • Spring Security 6 :内置 OAuth2 Authorization Server零配置 即可启动。

整体架构

sequenceDiagram participant Browser participant Client participant AuthServer participant ResourceServer Browser->>Client: GET /index Client->>Browser: 302 → /oauth2/authorize Browser->>AuthServer: 登录页 Browser->>AuthServer: POST /login AuthServer->>Browser: 302 → /oauth2/authorize?client_id=client&response_type=code Browser->>Client: 回调 /login?code=xxx Client->>AuthServer: POST /oauth2/token AuthServer->>Client: {access_token, id_token} Client->>ResourceServer: GET /user Bearer access_token ResourceServer->>Client: 200 {sub: alice}

环境准备

组件 版本
JDK 17
Spring Boot 3.2.5
Spring Security 6.2
OAuth2 Authorization Server 1.2.3

Maven 父 POM:

xml 复制代码
<properties>
    <java.version>17</java.version>
    <spring-boot.version>3.2.5</spring-boot.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-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-authorization-server</artifactId>
        <version>1.2.3</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-resource-server</artifactId>
    </dependency>
</dependencies>

搭建授权服务器(Authorization Server)

1. 启动类

java 复制代码
@SpringBootApplication
public class AuthServerApp {
    public static void main(String[] args) {
        SpringApplication.run(AuthServerApp.class, args);
    }
}

2. Security 配置(授权码 + JWT)

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain authChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(a -> a
                .requestMatchers("/login", "/oauth2/**", "/.well-known/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(f -> f.loginPage("/login").permitAll())
            .oauth2AuthorizationServer(o -> o
                .authorizationEndpoint(ae -> ae.uri("/oauth2/authorize"))
                .tokenEndpoint(te -> te.uri("/oauth2/token"))
                .oidc(oidc -> oidc.userInfoEndpoint(u -> u.uri("/userinfo")))
            );
        return http.build();
    }

    @Bean
    public RegisteredClientRepository clientRepository() {
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("client")
                .clientSecret("{noop}secret")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUri("http://localhost:8081/login/oauth2/code/client")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .build();
        return new InMemoryRegisteredClientRepository(client);
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = generateRsa();
        return (jwkSelector, context) -> jwkSelector.select(new JWKSet(rsaKey));
    }

    private static RSAKey generateRsa() {
        KeyPair keyPair = KeyGeneratorUtils.generateKeyPair();
        return new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
                .privateKey(keyPair.getPrivate())
                .keyID(UUID.randomUUID().toString())
                .build();
    }
}

3. 用户服务(内存用户)

java 复制代码
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return User.withUsername(username)
                   .password("{noop}123456")
                   .authorities("ROLE_USER")
                   .build();
    }
}

4. 启动验证

bash 复制代码
curl -X POST http://localhost:8080/login \
     -d "username=alice&password=123456" -c cookie.txt

返回 302 到 /oauth2/authorize,登录成功。


搭建资源服务器(Resource Server)

1. 配置

java 复制代码
@Configuration
@EnableWebSecurity
public class ResourceConfig {

    @Bean
    public SecurityFilterChain resourceChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(a -> a
                .requestMatchers("/userinfo").authenticated()
            )
            .oauth2ResourceServer(o -> o
                .jwt(j -> j.jwkSetUri("http://localhost:8080/oauth2/jwks"))
            );
        return http.build();
    }
}

2. 用户信息接口

java 复制代码
@RestController
public class UserInfoController {

    @GetMapping("/userinfo")
    public Map<String, Object> userInfo(OAuth2AuthenticationToken token) {
        return Map.of(
                "sub", token.getName(),
                "authorities", token.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList()
        );
    }
}

搭建客户端(Client)

1. 配置(application.yml)

yaml 复制代码
server:
  port: 8081
spring:
  security:
    oauth2:
      client:
        registration:
          client:
            client-id: client
            client-secret: secret
            scope: openid,profile
            redirect-uri: "http://localhost:8081/login/oauth2/code/{registrationId}"
            authorization-grant-type: authorization_code
        provider:
          client:
            issuer-uri: http://localhost:8080

2. 启动类 + 控制器

java 复制代码
@SpringBootApplication
public class ClientApp {
    public static void main(String[] args) {
        SpringApplication.run(ClientApp.class, args);
    }
}

@RestController
public class DemoController {

    @GetMapping("/")
    public Map<String, Object> index(OAuth2AuthenticationToken token) {
        return Map.of("user", token.getName(),
                      "authority", token.getAuthorities());
    }
}

3. 启动验证

  1. 浏览器访问 http://localhost:8081/
  2. 自动跳转到授权服务器登录页
  3. 输入 alice / 123456
  4. 返回客户端首页,显示用户名与权限

前后端分离下的 SSO

1. 授权服务器增加 CORS

java 复制代码
@Bean
public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowedOrigins("http://localhost:3000")
                    .allowedMethods("*")
                    .allowedHeaders("*");
        }
    };
}

2. 前端(React/Vue)流程

  1. 点击登录 → 跳转 http://localhost:8080/oauth2/authorize?client_id=client&response_type=code&scope=openid&redirect_uri=http://localhost:3000/callback
  2. 回调 → 解析 ?code=xxx
  3. POST /oauth2/token 获取 access_token
  4. Header Authorization: Bearer access_token 访问资源

常见安全问题 & 防护

攻击 防护
Token 泄露 HTTPS + 短有效期(<1h)
CSRF SameSite=Strict + 状态码校验
重放攻击 Redis 黑名单 + JWT exp
弱密钥 自动生成 RSA256 密钥对

总结

Spring Security 6 + OAuth2 内置 Authorization Server
三端代码 < 300 行 即可跑通 生产级 SSO

掌握 授权码流程 + JWT + CORS = 前后端分离 SSO终极方案

相关推荐
雄大3 分钟前
使用 QWebChannel 实现 JS 与 C++ 双向通信(超详细 + 踩坑总结 + Demo)
后端
计算机学姐5 分钟前
基于SpringBoot的汉服租赁系统【颜色尺码套装+个性化推荐算法+数据可视化统计】
java·vue.js·spring boot·后端·mysql·信息可视化·推荐算法
回家路上绕了弯5 分钟前
定期归档历史数据实战指南:从方案设计到落地优化
分布式·后端
+VX:Fegn08956 分钟前
计算机毕业设计|基于springboot + vue建筑材料管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
掘金者阿豪7 分钟前
Redis `WRONGTYPE` 错误的原因及解决方法
后端
天天摸鱼的java工程师10 分钟前
线程池深度解析:核心参数 + 拒绝策略 + 动态调整实战
java·后端
小杨同学4917 分钟前
C 语言实战:动态规划求解最长公共子串(连续),附完整实现与优化
后端
Cache技术分享20 分钟前
290. Java Stream API - 从文本文件的行创建 Stream
前端·后端
用户9483570165120 分钟前
拒绝 try-catch:如何设计全局通用的异常拦截体系?
后端
golang学习记23 分钟前
Go 1.22 隐藏彩蛋:cmp.Or —— 让“默认值”写起来像呼吸一样自然!
后端