You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot OAuth 2.0:自定义授权服务器及租户ID认证问题

在Spring Boot自定义授权服务器中支持租户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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 04:38:17