跳到主要内容

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 的实现对于构建安全的分布式系统至关重要。下一章将学习会话管理的高级配置。