【问题标题】:Combine Dynamic datasource routing with spring-data-rest将动态数据源路由与 spring-data-rest 结合起来
【发布时间】:2014-09-24 20:42:09
【问题描述】:

我正在使用动态数据源路由,如本博文所述: http://spring.io/blog/2007/01/23/dynamic-datasource-routing/

这很好用,但是当我将它与spring-data-rest 结合使用并浏览我生成的存储库时,我(正确地)得到一个异常,即我的查找键未定义(我没有设置默认值)。

在与数据库建立任何连接之前,我如何以及在何处挂钩 Spring 数据休息请求处理以基于“x”(用户授权、路径前缀或其他)设置查找键?

在代码方面,我的数据源配置几乎与顶部的博文相匹配,有一些基本的实体类、生成的存储库和 Spring Boot 将所有内容包装在一起。如果需要,我可以发布一些代码,但那里没什么可看的。

【问题讨论】:

  • 我可能错过了您的问题...示例中的 CustomerRoutingDataSource 不是在做您想要的工作吗?
  • 不是当你想将它与 spring data rest 生成的 rest api 结合起来时
  • 为什么不设置默认值?你也使用 Spring Security 吗?然后我可以提供一个简单的解决方案。
  • @ksokol 我没有设置默认值,因为不同的数据库涉及来自不同客户端的数据,不想混淆它们。我确实在使用 Spring 安全性,所以我想知道您对此有何看法。
  • @Tim 添加了我的想法作为答案。

标签: java spring-data spring-data-jpa spring-data-rest


【解决方案1】:

我的第一个想法是利用 Spring Security 的authentication 对象根据附加到身份验证的authorities 设置当前数据源。 当然,您也可以将查找键放入自定义UserDetails 对象甚至自定义身份验证对象中。为简洁起见,我将专注于基于权威的解决方案。 此解决方案需要一个有效的身份验证对象(匿名用户也可以有一个有效的身份验证)。根据您的 Spring Security 配置,更改权限/数据源可以在每个请求或会话的基础上完成。

我的第二个想法是使用 javax.servlet.Filter 在 Spring Data Rest 启动之前在线程局部变量中设置查找键。此解决方案独立于框架,可用于每个请求或会话。

使用 Spring Security 进行数据源路由

使用SecurityContextHolder 访问当前身份验证的权限。根据当局决定使用哪个数据源。 就像您的代码一样,我没有在 AbstractRoutingDataSource 上设置 defaultTargetDataSource。

public class CustomRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        Set<String> authorities = getAuthoritiesOfCurrentUser();
        if(authorities.contains("ROLE_TENANT1")) {
            return "TENANT1";
        }
        return "TENANT2";
    }

    private Set<String> getAuthoritiesOfCurrentUser() {
        if(SecurityContextHolder.getContext().getAuthentication() == null) {
            return Collections.emptySet();
        }
        Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
        return AuthorityUtils.authorityListToSet(authorities);
    }
}

在您的代码中,您必须将内存中的 UserDetailsService (inMemoryAuthentication) 替换为满足您需求的 UserDetailsS​​ervice。 它向您展示了用于数据源路由的两个具有不同角色的不同用户TENANT1TENANT2

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("user1").password("user1").roles("USER", "TENANT1")
            .and()
            .withUser("user2").password("user2").roles("USER", "TENANT2");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**")
            .authorizeRequests()
            .antMatchers("/**").hasRole("USER")
            .and()
            .httpBasic()
            .and().csrf().disable();
    }
}

这是一个完整的例子:https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-spring-security/spring-data

使用 javax.servlet.Filter 进行数据源路由

创建一个新的过滤器类并将其添加到您的web.xml 或分别使用AbstractAnnotationConfigDispatcherServletInitializer 注册。

public class TenantFilter implements Filter {

    private final Pattern pattern = Pattern.compile(";\\s*tenant\\s*=\\s*(\\w+)");

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String tenant = matchTenantSystemIDToken(httpRequest.getRequestURI());
        Tenant.setCurrentTenant(tenant);
        try {
            chain.doFilter(request, response);
        } finally {
            Tenant.clearCurrentTenant();
        }
    }

    private String matchTenantSystemIDToken(final String uri) {
        final Matcher matcher = pattern.matcher(uri);
        if (matcher.find()) {
            return matcher.group(1);
        }
        return null;
    }
}

Tenant 类是静态 ThreadLocal 的简单包装器。

public class Tenant {

    private static final ThreadLocal<String> TENANT = new ThreadLocal<>();

    public static void setCurrentTenant(String tenant) { TENANT.set(tenant); }

    public static String getCurrentTenant() { return TENANT.get(); }

    public static void clearCurrentTenant() { TENANT.remove(); }
}

就像您的代码一样,我没有在我的 AbstractRoutingDataSource 上设置 defaultTargetDataSource。

public class CustomRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        if(Tenant.getCurrentTenant() == null) {
            return "TENANT1";
        }
        return Tenant.getCurrentTenant().toUpperCase();
    }
}

现在您可以使用http://localhost:8080/sandbox/myEntities;tenant=tenant1 切换数据源。请注意,必须在每个请求上设置租户。或者,您可以将租户存储在HttpSession 中以供后续请求。

这是一个完整的例子:https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-url/spring-data

【讨论】:

  • 非常感谢您的详细回复。您的方法确实看起来可行,但遗憾的是,由于本周我的代码库的另一部分存在问题,我无法实现它。我希望尽快解决它,并在我确认它有效后将您的解决方案标记为答案。再次感谢!
  • 很抱歉我现在才开始使用它,但它就像一个魅力,非常感谢!我采用了基于过滤器的方法,因为租户和数据库处于多对多关系。
  • 我很高兴听到这个消息
  • @ksokol 我采用了基于过滤器的方法,它工作得很好!我只是想添加,而不是在过滤器中使用模式,您可以将租户作为参数发送(例如“?tenant=tenant1”),然后执行类似 Tenant.setCurrentTenant(httpRequest.getParameter("tenant"). ..它;我解决了就更清楚了。
  • @meriton-husaj 我知道你也可以使用查询参数。我更喜欢用分号来区分页面和应用程序范围的参数(想想'JSESSIONID')
猜你喜欢
  • 2021-03-15
  • 2012-06-22
  • 2018-07-06
  • 1970-01-01
  • 2018-10-28
  • 1970-01-01
  • 2017-05-15
  • 2018-01-24
  • 2016-10-26
相关资源
最近更新 更多