OAuth2 集成
OAuth2 是一个开放标准,允许用户授权第三方应用访问其在其他服务提供者上的信息,而无需将用户名和密码提供给第三方应用。Spring Security 提供了完整的 OAuth2 支持,包括作为客户端和资源服务器的配置。本章将详细介绍 OAuth2 的核心概念和在 Spring Security 中的集成实现。
OAuth2 基础
核心概念
OAuth2 定义了四个核心角色:
资源所有者(Resource Owner):能够授予对受保护资源访问权限的实体,通常就是用户。
客户端(Client):代表资源所有者发起请求的应用程序。
授权服务器(Authorization Server):验证资源所有者并颁发访问令牌的服务器。
资源服务器(Resource Server):托管受保护资源的服务器,能够接受并响应使用访问令牌的受保护资源请求。
授权流程
OAuth2 定义了四种授权类型:
授权码模式(Authorization Code):最安全、最常用的模式,适合服务器端应用。用户被重定向到授权服务器进行认证,授权后重定向回客户端并携带授权码,客户端再用授权码换取令牌。
隐式模式(Implicit):已不推荐使用,直接在重定向中返回令牌,安全性较低。
密码模式(Resource Owner Password Credentials):用户直接将密码提供给客户端,仅适用于高度信任的应用,已不推荐。
客户端凭证模式(Client Credentials):客户端使用自己的凭证获取令牌,适合服务间通信。
OAuth2 登录流程
用户 → 客户端应用 → 授权服务器(如 Google、GitHub)
↓
授权服务器认证用户
↓
用户同意授权
↓
授权服务器返回授权码
↓
客户端用授权码换取 Access Token
↓
客户端使用 Token 访问资源服务器
Spring Security OAuth2 客户端
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
配置 OAuth2 提供者
在 application.yml 中配置 OAuth2 提供者:
spring:
security:
oauth2:
client:
registration:
# GitHub 登录
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope: read:user, user:email
# Google 登录
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: profile, email
# 自定义 OAuth2 提供者
custom:
client-id: ${CUSTOM_CLIENT_ID}
client-secret: ${CUSTOM_CLIENT_SECRET}
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope: read, write
provider:
custom:
authorization-uri: https://auth.example.com/oauth/authorize
token-uri: https://auth.example.com/oauth/token
user-info-uri: https://auth.example.com/userinfo
user-name-attribute: id
基本安全配置
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/error", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home")
.failureUrl("/login?error")
);
return http.build();
}
}
自定义登录页面
@GetMapping("/login")
public String login() {
return "login";
}
<!-- login.html -->
<div class="oauth2-login">
<a th:href="@{/oauth2/authorization/github}">
<img src="/images/github-icon.png" alt="GitHub 登录" />
使用 GitHub 登录
</a>
<a th:href="@{/oauth2/authorization/google}">
<img src="/images/google-icon.png" alt="Google 登录" />
使用 Google 登录
</a>
</div>
获取用户信息
OAuth2 登录成功后,可以获取用户信息:
@Controller
public class UserController {
@GetMapping("/home")
public String home(@AuthenticationPrincipal OAuth2User principal, Model model) {
model.addAttribute("name", principal.getAttribute("name"));
model.addAttribute("email", principal.getAttribute("email"));
model.addAttribute("avatar", principal.getAttribute("avatar_url"));
return "home";
}
// 或者使用 OAuth2AuthenticationToken
@GetMapping("/profile")
public String profile(OAuth2AuthenticationToken authentication, Model model) {
OAuth2User user = authentication.getPrincipal();
String clientRegistrationId = authentication.getAuthorizedClientRegistrationId();
model.addAttribute("user", user.getAttributes());
model.addAttribute("provider", clientRegistrationId);
return "profile";
}
}
自定义用户信息服务
当需要将 OAuth2 用户映射到本地用户时,可以自定义 OAuth2UserService:
@Component
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate;
public CustomOAuth2UserService(UserRepository userRepository) {
this.userRepository = userRepository;
this.delegate = new DefaultOAuth2UserService();
}
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 1. 从 OAuth2 提供者获取用户信息
OAuth2User oauth2User = delegate.loadUser(userRequest);
// 2. 提取关键信息
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
// 3. 根据提供者处理用户信息
OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(
registrationId,
oauth2User.getAttributes()
);
// 4. 查找或创建本地用户
User user = userRepository.findByEmail(userInfo.getEmail())
.orElseGet(() -> createNewUser(userInfo, registrationId));
// 5. 返回自定义的 OAuth2User
return new CustomOAuth2User(
oauth2User.getAttributes(),
user,
userNameAttributeName
);
}
private User createNewUser(OAuth2UserInfo userInfo, String provider) {
User user = new User();
user.setEmail(userInfo.getEmail());
user.setName(userInfo.getName());
user.setImageUrl(userInfo.getImageUrl());
user.setProvider(provider);
user.setProviderId(userInfo.getId());
return userRepository.save(user);
}
}
配置自定义用户服务
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler)
);
return http.build();
}
OAuth2 资源服务器
资源服务器负责验证 Access Token 并保护 API 资源。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
JWT 资源服务器配置
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com
# 或者直接指定公钥
# public-key-location: classpath:public-key.pem
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/user/**").hasAuthority("SCOPE_profile")
.requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);
return http.build();
}
private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(new CustomJwtGrantedAuthoritiesConverter());
return converter;
}
}
自定义权限提取
从 JWT 中提取权限信息:
public class CustomJwtGrantedAuthoritiesConverter
implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 从 scope 提取权限
List<String> scopes = jwt.getClaimAsStringList("scope");
if (scopes != null) {
scopes.forEach(scope ->
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope))
);
}
// 从自定义 claims 提取角色
List<String> roles = jwt.getClaimAsStringList("roles");
if (roles != null) {
roles.forEach(role ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + role))
);
}
return authorities;
}
}
访问受保护资源
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/user/profile")
public Map<String, Object> profile(@AuthenticationPrincipal Jwt jwt) {
Map<String, Object> result = new HashMap<>();
result.put("subject", jwt.getSubject());
result.put("claims", jwt.getClaims());
return result;
}
@GetMapping("/admin/users")
public List<User> getUsers() {
return userService.findAll();
}
}
多认证方式组合
在实际应用中,经常需要同时支持多种认证方式。
JWT + OAuth2 组合
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/oauth2/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.redirectionEndpoint(redirection -> redirection
.baseUri("/oauth2/callback/*")
)
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
return http.build();
}
API 端点区分
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// API 安全配置:使用 JWT
@Bean
@Order(1)
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
// Web 安全配置:使用 OAuth2 登录
@Bean
@Order(2)
public SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.oauth2Login(Customizer.withDefaults());
return http.build();
}
}
使用 Access Token 调用外部 API
当应用需要作为 OAuth2 客户端调用外部 API 时:
配置 WebClient
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Filter =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Filter.setDefaultClientRegistrationId("custom");
return WebClient.builder()
.apply(oauth2Filter.oauth2Configuration())
.build();
}
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.build();
AuthorizedClientServiceOAuth2AuthorizedClientManager manager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientService
);
manager.setAuthorizedClientProvider(authorizedClientProvider);
return manager;
}
}
使用 WebClient 调用 API
@Service
public class ExternalApiService {
private final WebClient webClient;
public ExternalApiService(WebClient webClient) {
this.webClient = webClient;
}
public Mono<UserInfo> getUserInfo() {
return webClient.get()
.uri("https://api.example.com/user")
.retrieve()
.bodyToMono(UserInfo.class);
}
}
小结
本章详细介绍了 OAuth2 集成的实现:
- OAuth2 定义了资源所有者、客户端、授权服务器、资源服务器四个角色
- Spring Security 支持作为 OAuth2 客户端集成第三方登录(如 GitHub、Google)
- 自定义用户服务可以将 OAuth2 用户映射到本地用户系统
- 资源服务器配置保护 API 端点,验证 Access Token
- 多认证方式组合满足复杂场景需求
- WebClient 支持使用 OAuth2 访问令牌调用外部 API
OAuth2 是现代应用集成的重要协议,理解其原理和 Spring Security 的实现对于构建安全的分布式系统至关重要。下一章将学习会话管理的高级配置。