SpringSecurity中如何接入单点登录

349 阅读3分钟

SpringSecurity中如何接入单点登录

基于 Spring Boot 3.2 + Spring Security 6 + OAuth2完整落地指南
授权服务器、资源服务器、客户端 三端代码,可直接运行


目录

  1. 背景:SSO vs OAuth2 vs Spring Security
  2. 整体架构
  3. 环境准备
  4. 搭建授权服务器(Authorization Server)
  5. 搭建资源服务器(Resource Server)
  6. 搭建客户端(Client)
  7. 前后端分离下的 SSO
  8. 常见安全问题 & 防护
  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}

环境准备

组件版本
JDK17
Spring Boot3.2.5
Spring Security6.2
OAuth2 Authorization Server1.2.3

Maven 父 POM:

<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. 启动类

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

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

@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. 用户服务(内存用户)

@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. 启动验证

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

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


搭建资源服务器(Resource Server)

1. 配置

@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. 用户信息接口

@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)

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. 启动类 + 控制器

@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

@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)
CSRFSameSite=Strict + 状态码校验
重放攻击Redis 黑名单 + JWT exp
弱密钥自动生成 RSA256 密钥对

总结

Spring Security 6 + OAuth2 内置 Authorization Server
三端代码 < 300 行 即可跑通 生产级 SSO
掌握 授权码流程 + JWT + CORS = 前后端分离 SSO终极方案