在Spring Security中整合OAuth2并保留自定义CustomUser对象
问题
我们正将Spring Security应用从用户名/密码认证迁移至OAuth2,已成功在安全链中添加OAuth2登录,但安全上下文的用户对象出现问题。
当前配置如下:
用户对象
public class CustomUser implements UserDetails { private Long id; private String username; private Set<CustomRole> roles; // 更多字段、getter、setter等 }
public class UserService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { try { return getUserByLoginname(username); } catch (NotFoundException e) { throw new UsernameNotFoundException(""); } } public CustomUser getUserByLoginname(String identifier) throws NotFoundException { // 从数据库加载用户 } }
我们在多处通过以下代码获取登录用户:
@GetMapping(value = "/XXX") public void XXX() { CustomUser loggedInUser = (CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); // ... }
现在我们希望整合OAuth2的同时保留CustomUser模型,仅用OAuth2完成认证,不从OAuth流程获取权限或角色,而是在自有数据库中维护用户角色。已在安全配置中加入oauth2Login:
@Bean protected SecurityFilterChain configure(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception { http .cors(c -> corsConfigurationSource()) .authorizeHttpRequests(requests -> requests .requestMatchers(AntPathRequestMatcher.antMatcher("/XXX"), AntPathRequestMatcher.antMatcher("/XXXXX")) .permitAll() .anyRequest().authenticated()) .oauth2Login(o -> o.authorizationEndpoint(end -> end .authorizationRequestResolver(new CustomAuthorizationRequestResolver(clientRegistrationRepository)))) .logout(l -> l.logoutSuccessUrl("/logoutsite").permitAll()) .sessionManagement(management -> management.maximumSessions(-1).sessionRegistry(sessionRegistry())) .headers(headers -> headers.referrerPolicy(policy -> policy.policy(ReferrerPolicy.NO_REFERRER))) .csrf(csrf -> csrf.disable()); return http.build(); }
认证已成功,但安全上下文中的对象是DefaultOidcUser而非CustomUser。曾考虑在UsernamePasswordAuthenticationFilter后添加过滤器替换用户对象,但觉得不够优雅。尝试过OAuthUserService或OidcUserService等方法,仍得到OAuth相关对象。
核心需求:如何仅用OAuth2完成认证,将自定义CustomUser对象载入安全上下文,同时忽略OAuth的所有权限和授权信息?
解决方案
通过自定义OidcUserService(如果使用非OIDC类型的OAuth2提供商,可替换为OAuth2UserService),在OAuth2认证完成后替换安全上下文的用户对象为CustomUser,具体实现如下:
1. 自定义OidcUserService实现
创建自定义服务,在OAuth2用户信息获取完成后,调用自有UserService加载数据库中的CustomUser,并构建新的认证对象覆盖安全上下文:
@Service public class CustomOidcUserService extends OidcUserService { private final UserService userService; public CustomOidcUserService(UserService userService) { this.userService = userService; } @Override public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { // 先获取OAuth2返回的用户信息 OidcUser oidcUser = super.loadUser(userRequest); // 从OAuth2用户信息中提取唯一标识(比如email或sub字段,根据提供商配置调整) String userIdentifier = oidcUser.getEmail(); // 或oidcUser.getSubject() // 加载自有数据库中的CustomUser CustomUser customUser; try { customUser = userService.getUserByLoginname(userIdentifier); } catch (NotFoundException e) { // 可在此处理首次登录自动注册逻辑,根据业务需求调整 throw new OAuth2AuthenticationException("用户不存在"); } // 用CustomUser的角色生成权限,忽略OAuth返回的权限 Collection<GrantedAuthority> authorities = customUser.getRoles() .stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName())) .collect(Collectors.toList()); // 构建新的认证对象并替换安全上下文 Authentication authentication = new UsernamePasswordAuthenticationToken( customUser, null, // OAuth2认证无需凭证,设为null authorities ); SecurityContextHolder.getContext().setAuthentication(authentication); return oidcUser; } }
2. 在安全配置中绑定自定义服务
修改SecurityFilterChain配置,将自定义的OidcUserService绑定到oauth2Login的用户信息端点:
@Bean protected SecurityFilterChain configure(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository, CustomOidcUserService customOidcUserService) throws Exception { http .cors(c -> corsConfigurationSource()) .authorizeHttpRequests(requests -> requests .requestMatchers(AntPathRequestMatcher.antMatcher("/XXX"), AntPathRequestMatcher.antMatcher("/XXXXX")) .permitAll() .anyRequest().authenticated()) .oauth2Login(o -> o .authorizationEndpoint(end -> end .authorizationRequestResolver(new CustomAuthorizationRequestResolver(clientRegistrationRepository))) .userInfoEndpoint(userInfo -> userInfo .oidcUserService(customOidcUserService)) // 绑定自定义服务 ) .logout(l -> l.logoutSuccessUrl("/logoutsite").permitAll()) .sessionManagement(management -> management.maximumSessions(-1).sessionRegistry(sessionRegistry())) .headers(headers -> headers.referrerPolicy(policy -> policy.policy(ReferrerPolicy.NO_REFERRER))) .csrf(csrf -> csrf.disable()); return http.build(); }
关键说明
- 标识映射:确保从OAuth2返回的用户标识与自有数据库的用户登录名匹配,这是关联OAuth认证和自有用户的核心。
- 权限控制:完全使用
CustomUser中的角色生成权限,彻底忽略OAuth流程返回的任何权限信息。 - 兼容性:原有业务中获取
CustomUser的代码无需修改,安全上下文已替换为目标对象。
内容的提问来源于stack exchange,提问作者user27226098

