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

相关推荐
GetcharZp4 小时前
GitHub 49K+ Star!C++ 开发者必知的 JSON 神级库:从零到精通全指北
后端
xujinwei_gingko5 小时前
SpringBoot整合WebSocket
spring boot·后端·websocket
智码看视界5 小时前
现代Web开发基础:全栈工程师的起航点
前端·后端·c5全栈
程序员cxuan5 小时前
Claude Fable 5 来了
人工智能·后端·程序员
JS菌5 小时前
手写一个 AI Agent 全栈项目:从沙箱执行到子智能体的完整实现
前端·人工智能·后端
wang09076 小时前
自己动手写一个spring之IOC_2
java·后端·spring
ltl6 小时前
推理退化:为什么大模型会输出乱码、死循环和无意义文本
后端
ltl6 小时前
架构视图与文档:C4 模型从入门到实战
后端
IT_陈寒9 小时前
Redis持久化这个坑,我爬了一整天才出来
前端·人工智能·后端
无风听海9 小时前
多租户系统中的 OIDC:Discovery 端点与联合登录的深度实践
后端·python·flask