SpringSecurity中如何接入单点登录
基于 Spring Boot 3.2 + Spring Security 6 + OAuth2 的 完整落地指南
含 授权服务器、资源服务器、客户端 三端代码,可直接运行
目录
- [背景:SSO vs OAuth2 vs Spring Security](#背景:SSO vs OAuth2 vs Spring Security "#%E8%83%8C%E6%99%AF")
- 整体架构
- 环境准备
- [搭建授权服务器(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")
- [搭建资源服务器(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")
- 搭建客户端(Client)
- [前后端分离下的 SSO](#前后端分离下的 SSO "#%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E4%B8%8B%E7%9A%84-sso")
- [常见安全问题 & 防护](#常见安全问题 & 防护 "#%E5%B8%B8%E8%A7%81%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98--%E9%98%B2%E6%8A%A4")
- 总结
背景: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. 启动验证
- 浏览器访问
http://localhost:8081/ - 自动跳转到授权服务器登录页
- 输入 alice / 123456
- 返回客户端首页,显示用户名与权限
前后端分离下的 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)流程
- 点击登录 → 跳转
http://localhost:8080/oauth2/authorize?client_id=client&response_type=code&scope=openid&redirect_uri=http://localhost:3000/callback - 回调 → 解析
?code=xxx - POST /oauth2/token 获取 access_token
- 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 的 终极方案!