我的第一个想法是利用 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) 替换为满足您需求的 UserDetailsService。
它向您展示了用于数据源路由的两个具有不同角色的不同用户TENANT1 和TENANT2。
@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