java - 用户登录后如何使用 AbstractDataSource 切换 Schema
问题描述
我的问题:在用户登录后,在 StackOverFlow 之后坚持实施模式更改。
描述:我正在使用下面的课程。但是,我不知道如何使用它。我正在阅读每个教程,但我被卡住了。我期待的结果是:
1- Spring 使用默认 URL 进行初始化,以便用户可以登录。
2- 成功登录后,它会更改为基于UserDetails
类的架构。
我正在关注 Stack Overflow 的解决方案:Change database schema during runtime based on logged in user
我正在使用的 Spring 版本是
> : Spring Boot :: (v2.3.3.RELEASE)
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.sql.Connection;
import java.sql.ConnectionBuilder;
import java.sql.SQLException;
import java.util.concurrent.TimeUnit;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.AbstractDataSource;
public class UserSchemaAwareRoutingDataSource extends AbstractDataSource {
@Autowired
UsuarioProvider customUserDetails;
@Autowired
Environment env;
private LoadingCache<String, DataSource> dataSources = createCache();
public UserSchemaAwareRoutingDataSource() {
}
public UserSchemaAwareRoutingDataSource(UsuarioProvider customUserDetails, Environment env) {
this.customUserDetails = customUserDetails;
this.env = env;
}
private LoadingCache<String, DataSource> createCache() {
return CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(
new CacheLoader<String, DataSource>() {
public DataSource load(String key) throws Exception {
return buildDataSourceForSchema(key);
}
});
}
private DataSource buildDataSourceForSchema(String schema) {
System.out.println("schema:" + schema);
String url = "jdbc:mysql://REDACTED.com/" + schema;
String username = env.getRequiredProperty("spring.datasource.username");
String password = env.getRequiredProperty("spring.datasource.password");
System.out.println("Flag A");
DataSource build = (DataSource) DataSourceBuilder.create()
.driverClassName(env.getRequiredProperty("spring.datasource.driverClassName"))
.username(username)
.password(password)
.url(url)
.build();
System.out.println("Flag B");
return build;
}
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
private DataSource determineTargetDataSource() {
try {
Usuario usuario = customUserDetails.customUserDetails();
//
String db_schema = usuario.getTunnel().getDb_schema();
//
String schema = db_schema;
return dataSources.get(schema);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
@Override
public ConnectionBuilder createConnectionBuilder() throws SQLException {
return super.createConnectionBuilder();
}
}
参考: https ://spring.io/blog/2007/01/23/dynamic-datasource-routing/
如何使用 JDBC 在 Spring 中创建动态连接(数据源)
编辑(评论中需要其他信息):
我有 1 个数据库。该数据库有n
许多模式。每个模式都属于一家公司。一位用户属于一家公司。登录逻辑如下:
- 用户输入用户名和密码。- 成功时,UserDetails
将包含此用户的“模式”的名称。基本上,该用户属于哪个公司/架构。
- 之后,它应该直接连接到该模式,以便用户可以使用他自己公司的数据。
我希望这尽可能地澄清。
编辑2:
@Component
public class UsuarioProvider {
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) // or just @RequestScope
public Usuario customUserDetails() {
return (Usuario) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}
public class UserSchemaAwareRoutingDataSource extends AbstractDataSource {
@Autowired
private UsuarioProvider usuarioProvider;
@Autowired // This references the primary datasource, because no qualifier is given
private DataSource companyDependentDataSource;
@Autowired
@Qualifier(value = "loginDataSource")
private DataSource loginDataSource;
@Autowired
Environment env;
private LoadingCache<String, DataSource> dataSources = createCache();
public UserSchemaAwareRoutingDataSource() {
}
private LoadingCache<String, DataSource> createCache() {
return CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(
new CacheLoader<String, DataSource>() {
public DataSource load(String key) throws Exception {
return buildDataSourceForSchema(key);
}
});
}
private DataSource buildDataSourceForSchema(String schema) {
System.out.println("schema:" + schema);
String url = "jdbc:mysql://REDACTED.com/" + schema;
String username = env.getRequiredProperty("spring.datasource.username");
String password = env.getRequiredProperty("spring.datasource.password");
System.out.println("Flag A");
DataSource build = (DataSource) DataSourceBuilder.create()
.driverClassName(env.getRequiredProperty("spring.datasource.driverClassName"))
.username(username)
.password(password)
.url(url)
.build();
System.out.println("Flag B");
return build;
}
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
private DataSource determineTargetDataSource() {
try {
System.out.println("Flag G");
Usuario usuario = usuarioProvider.customUserDetails(); // request scoped answer!
String db_schema = usuario.getTunnel().getDb_schema();
return dataSources.get(db_schema);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
@Override
public ConnectionBuilder createConnectionBuilder() throws SQLException {
return super.createConnectionBuilder();
}
}
我需要把这@Configuration
门课放在首位吗?我无法让 Spring Boot 了解此设置。我对如何告诉 Spring Boot loginDataSource;
url 是什么感到有些困惑。我使用application.properties
默认值登录。
解决方案
您的设置结合了两个不同数据源的经典情况。这是Baeldung-Blog-Post 如何配置 Spring Data JPA。
首先要注意的是,他们正在使用@Primary
. 这在帮助你的同时也阻碍了你。您只能拥有一个特定类型的主 bean。这给某些人带来了麻烦,因为他们试图通过将测试 spring bean 设为主要来“覆盖”spring bean。这导致有两个具有相同类型的主 bean。因此,在设置测试时要小心。
但是,如果您主要指的是一个 DataSource 而仅在少数情况下指代另一个,它也会使事情变得容易。这似乎是你的情况,所以让我们采用它。
您的 DataSource 配置可能看起来像
@Configuration
public class DataSourceConfiguration {
@Bean(name="loginDataSource")
public DataSource loginDataSource(Environment env) {
String url = env.getRequiredProperty("spring.logindatasource.url");
return DataSourceBuilder.create()
.driverClassName(env.getRequiredProperty("spring.logindatasource.driverClassName"))
[...]
.url(url)
.build();
}
@Bean(name="companyDependentDataSource")
@Primary // use with caution, I'd recommend to use name based autowiring. See @Qualifier
public DataSource companyDependentDataSource(Environment env) {
return new UserSchemaAwareRoutingDataSource(); // Autowiring is done afterwards by Spring
}
}
这两个数据源现在可以在您的存储库/DAO 中使用,或者您如何构建您的程序
@Autowired // This references the primary datasource, because no qualifier is given. UserSchemaAwareRoutingDataSource is its implementation
// @Qualifier("companyDependentDataSource") if @Primary is omitted
private DataSource companyDependentDataSource;
@Autowired
@Qualifier(name="loginDataSource") // reference by bean name
private DataSource loginDataSource
这是一个示例,如何使用DataSource
名称引用来配置 Spring Data JPA:
@Configuration
@EnableJpaRepositories(
basePackages = "<your entity package>",
entityManagerFactoryRef = "companyEntityManagerFactory",
transactionManagerRef = "companyTransactionManager"
)
public class CompanyPersistenceConfiguration {
@Autowired
@Qualifier("companyDependentDataSource")
private DataSource companyDependentDataSource;
@Bean(name="companyEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean companyEntityManagerFactory() {
LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
emf.setDataSource(companyDependentDataSource);
// ... see Baeldung Blog Post
return emf;
}
@Bean(name="companyTransactionManager")
public PlatformTransactionManager companyTransactionManager() {
JpaTransactionManager tm = new JpaTransactionManager();
tm.setEntityManagerFactory(companyEntityManagerFactory().getObject());
return tm;
}
}
正如您提到的我的 SO-answer中所述,有一个重要的假设
用于当前用户的当前模式名称可通过 Spring JSR-330 Provider 访问,例如
private javax.inject.Provider<User> user; String schema = user.get().getSchema();
. 理想情况下,这是一个基于 ThreadLocal 的代理。
这是使UserSchemaAwareRoutingDataSource
实现成为可能的技巧。Spring bean 大多是单例的,因此是无状态的。这也适用于 DataSources 的正常使用。它们被视为无状态的单例,并且对它们的引用在整个程序中传递。因此,我们需要找到一种方法来提供单个实例,该实例companyDependentDataSource
在用户基础上表现不同,无论如何。为了获得这种行为,我建议使用请求范围的 bean。
在 Web 应用程序中,您可以使用@Scope(REQUEST_SCOPE)
来创建此类对象。还有一个Bealdung Post谈论那个话题。像往常一样,@Bean
带注释的方法驻留在@Confiugration
带注释的类中。
@Configuration
public class UsuarioConfiguration {
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS) // or just @RequestScope
public Usuario usario() {
// based on your edit2
return (Usuario) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}
现在,您可以将此请求范围对象与单例DataSource 中的提供程序一起使用,以根据登录用户的行为有所不同:
@Autowired
private Usario usario; // this is now a request-scoped proxy which will create the corresponding bean (see UsuarioConfiguration.usario()
private DataSource determineTargetDataSource() {
try {
String db_schema = this.usuario.getTunnel().getDb_schema();
return dataSources.get(db_schema);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
我希望这可以帮助您理解 Spring 的请求范围概念。
所以你的登录过程看起来像
- 用户输入用户名和密码
- 一个普通的 spring bean,通过name引用 userDataSource ,正在检查登录并将用户信息放入 session/securitycontext/cookie/....
- 成功后,在下一个请求期间,
companyDependentDataSource
能够检索正确设置的Usario
对象 - 您现在可以使用此数据源来执行用户特定的操作。
要验证您DataSource
是否正常工作,您可以创建一个小型 Spring MVC 端点
@RestController
public class DataSourceVerificationController {
@Autowired
private Usario usario;
@Autowired
@Qualifier("companyDependentDataSource") // omit this annotation if you use @Primary
private DataSource companyDependentDataSource;
@GetRequest("/test")
public String test() throws Exception {
String schema = usario.getTunnel().getDb_schema()
Connection con = companyDependentDataSource.getConnection();
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("select name from Employee"); // just a random guess
rs.next();
String name = rs.getString("name")
rs.close();
stmt.close();
con.close();
return "name = '" + name + "', schema = '" + schema + "'";
}
}
使用您喜欢的浏览器进入您的登录页面,进行有效登录,然后调用 http://localhost:8080/test
推荐阅读
- laravel - 缺少路线 bus.edit 所需的参数
- node.js - 将nodejs程序的输出重定向到文件时编码错误(windows 10 powershell可能的问题)
- scala - 如果组的任何行具有 0 值,我将如何使用 ANY 条件进行过滤?
- java - "GeorgeNotFound" 的 MinecraftShocker 插件如果你有这个错误:jSSC-2.8_x86_64.dll+0xb5db,如何修复
- next.js - Next.js 和 Styled Components 在刷新时服务器和客户端之间不同步
- javascript - 在本地插件(Strapi)中使用 .env
- javascript - javascript: for void 在 for 循环的上下文中是什么意思?
- tensorflow - 如何在 python 3.7.9 中使用 Tensorflow 2.0 版本并使用置换功能?
- reactjs - React 应用程序“找不到 index.html 文件”
- r - 如何删除officer doc中自动添加的blanc页面?