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终极方案

相关推荐
Victor3561 小时前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端
缘不易1 小时前
Springboot 整合JustAuth实现gitee授权登录
spring boot·后端·gitee
Kiri霧1 小时前
Range循环和切片
前端·后端·学习·golang
WizLC1 小时前
【Java】各种IO流知识详解
java·开发语言·后端·spring·intellij idea
Victor3562 小时前
Netty(19)Netty的性能优化手段有哪些?
后端
爬山算法2 小时前
Netty(15)Netty的线程模型是什么?它有哪些线程池类型?
java·后端
白宇横流学长2 小时前
基于SpringBoot实现的冬奥会科普平台设计与实现【源码+文档】
java·spring boot·后端
Python编程学习圈3 小时前
Asciinema - 终端日志记录神器,开发者的福音
后端
bing.shao3 小时前
Golang 高并发秒杀系统踩坑
开发语言·后端·golang
壹方秘境3 小时前
一款方便Java开发者在IDEA中抓包分析调试接口的插件
后端