Spring Boot OAuth 2.0:自定义授权服务器及租户ID认证问题
Got it, let's break down how to add tenant ID support to your Spring Boot authorization server step by step. The default password grant only handles username/password, but we can extend it to include tenant ID without too much hassle. Here's a practical, tested approach:
1. 扩展认证Token以携带租户ID
首先,我们需要让认证流程能传递租户ID。默认的UsernamePasswordAuthenticationToken没有这个字段,所以我们自定义一个子类:
public class TenantUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken { private final String tenantId; // 未认证时的构造器 public TenantUsernamePasswordAuthenticationToken(Object principal, Object credentials, String tenantId) { super(principal, credentials); this.tenantId = tenantId; } // 认证成功后的构造器 public TenantUsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String tenantId) { super(principal, credentials, authorities); this.tenantId = tenantId; } public String getTenantId() { return tenantId; } }
2. 创建包含租户ID的自定义UserDetails
我们需要在认证成功后的Principal中存储租户ID,所以自定义UserDetails实现:
public class TenantUserDetails implements UserDetails { private final String username; private final String password; private final String tenantId; private final Collection<? extends GrantedAuthority> authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled; // 全参构造器 public TenantUserDetails(String username, String password, String tenantId, Collection<? extends GrantedAuthority> authorities, boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired, boolean enabled) { this.username = username; this.password = password; this.tenantId = tenantId; this.authorities = authorities; this.accountNonExpired = accountNonExpired; this.accountNonLocked = accountNonLocked; this.credentialsNonExpired = credentialsNonExpired; this.enabled = enabled; } // 实现UserDetails的所有方法,以及tenantId的getter @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } public String getTenantId() { return tenantId; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public boolean isAccountNonExpired() { return accountNonExpired; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; } @Override public boolean isEnabled() { return enabled; } }
3. 实现支持租户ID的UserDetailsService
接下来,我们需要根据用户名+租户ID查询用户,所以扩展UserDetailsService:
@Service public class TenantUserDetailsService implements UserDetailsService { // 注入你的用户数据访问层(比如JPA Repository) private final UserRepository userRepository; public TenantUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } // 重写默认方法,引导使用租户ID查询的方法 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { throw new UnsupportedOperationException("Use loadUserByUsernameAndTenantId instead"); } // 自定义方法:根据用户名和租户ID加载用户 public UserDetails loadUserByUsernameAndTenantId(String username, String tenantId) throws UsernameNotFoundException { User dbUser = userRepository.findByUsernameAndTenantId(username, tenantId) .orElseThrow(() -> new UsernameNotFoundException( String.format("User not found for username: %s, tenantId: %s", username, tenantId))); // 转换为TenantUserDetails返回 return new TenantUserDetails( dbUser.getUsername(), dbUser.getPassword(), dbUser.getTenantId(), dbUser.getAuthorities(), true, true, true, true ); } }
4. 自定义AuthenticationProvider处理租户认证
我们需要替换默认的DaoAuthenticationProvider,让它能处理带租户ID的认证请求:
@Component public class TenantAuthenticationProvider extends DaoAuthenticationProvider { private final TenantUserDetailsService tenantUserDetailsService; public TenantAuthenticationProvider(TenantUserDetailsService tenantUserDetailsService) { this.tenantUserDetailsService = tenantUserDetailsService; setUserDetailsService(tenantUserDetailsService); // 配置密码编码器(和你的系统保持一致) setPasswordEncoder(new BCryptPasswordEncoder()); } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { TenantUsernamePasswordAuthenticationToken authToken = (TenantUsernamePasswordAuthenticationToken) authentication; String username = authToken.getName(); String password = (String) authToken.getCredentials(); String tenantId = authToken.getTenantId(); // 根据用户名+租户ID加载用户 TenantUserDetails userDetails = (TenantUserDetails) tenantUserDetailsService.loadUserByUsernameAndTenantId(username, tenantId); // 验证密码(复用父类的逻辑) additionalAuthenticationChecks(userDetails, authToken); // 返回认证成功的Token,包含租户ID return new TenantUsernamePasswordAuthenticationToken( userDetails, password, userDetails.getAuthorities(), tenantId ); } // 指定只处理我们自定义的Token类型 @Override public boolean supports(Class<?> authentication) { return TenantUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } }
5. 配置AuthenticationManager使用自定义Provider
在Spring Security配置中,让AuthenticationManager使用我们的租户认证Provider:
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final TenantAuthenticationProvider tenantAuthenticationProvider; public WebSecurityConfig(TenantAuthenticationProvider tenantAuthenticationProvider) { this.tenantAuthenticationProvider = tenantAuthenticationProvider; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(tenantAuthenticationProvider); } // 暴露AuthenticationManager给授权服务器使用 @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
6. 更新授权服务器配置
把自定义的AuthenticationManager注入到授权服务器中,替换默认的认证逻辑:
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { private final AuthenticationManager authenticationManager; private final PasswordEncoder passwordEncoder; public AuthorizationServerConfig(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder) { this.authenticationManager = authenticationManager; this.passwordEncoder = passwordEncoder; } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager); // 这里可以添加token存储、token增强等其他配置 } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 配置你的客户端信息(示例用内存存储,生产建议用数据库) clients.inMemory() .withClient("your-client-id") .secret(passwordEncoder.encode("your-client-secret")) .authorizedGrantTypes("password") .scopes("read", "write"); } }
7. 处理请求中的租户ID参数
最后,我们需要从请求中提取tenantId参数,构造自定义的认证Token。可以通过添加一个Filter来实现:
@Component public class TenantAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 只处理密码模式的token请求 if ("/oauth/token".equals(request.getRequestURI()) && "password".equals(request.getParameter("grant_type"))) { String username = request.getParameter("username"); String password = request.getParameter("password"); String tenantId = request.getParameter("tenantId"); if (username != null && password != null && tenantId != null) { // 构造自定义Token并放入SecurityContext TenantUsernamePasswordAuthenticationToken authToken = new TenantUsernamePasswordAuthenticationToken(username, password, tenantId); SecurityContextHolder.getContext().setAuthentication(authToken); } } filterChain.doFilter(request, response); } }
记得把这个Filter加到Spring Security的过滤链中,放在UsernamePasswordAuthenticationFilter之前:
// 在WebSecurityConfig中添加 @Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(tenantAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // 其他安全配置... .authorizeRequests() .anyRequest().authenticated(); }
8. 获取认证后的租户ID
认证成功后,你可以通过以下方式获取包含租户ID的Principal:
TenantUserDetails userDetails = (TenantUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); String tenantId = userDetails.getTenantId();
内容的提问来源于stack exchange,提问作者ishrk1

