Spring MVC Java CRM系统多数据库登录实现方案求助
Spring Boot + Spring Security 多租户(分公司独立数据库)登录实现方案
核心思路
通过租户元数据库存储所有公司的数据库连接信息,用户登录时先从元数据库获取对应公司的数据源配置,动态切换数据源后再验证用户身份,实现每个公司独立数据库的登录逻辑。
1. 定义租户数据源实体与元数据库
首先需要一个单独的元数据库,用来存储所有公司的数据库连接信息:
元数据库表结构
CREATE TABLE tenant_data_sources ( id BIGINT AUTO_INCREMENT PRIMARY KEY, company_id VARCHAR(50) UNIQUE NOT NULL COMMENT '公司唯一标识', db_url VARCHAR(255) NOT NULL COMMENT '数据库连接地址', db_username VARCHAR(50) NOT NULL COMMENT '数据库用户名', db_password VARCHAR(255) NOT NULL COMMENT '数据库密码', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' );
租户数据源实体类
@Entity @Table(name = "tenant_data_sources") public class TenantDataSource { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "company_id", unique = true, nullable = false) private String companyId; @Column(name = "db_url", nullable = false) private String dbUrl; @Column(name = "db_username", nullable = false) private String dbUsername; @Column(name = "db_password", nullable = false) private String dbPassword; // 省略getter、setter }
元数据库Repository
public interface TenantDataSourceRepository extends JpaRepository<TenantDataSource, Long> { TenantDataSource findByCompanyId(String companyId); }
2. 实现动态数据源切换
继承Spring的AbstractRoutingDataSource,通过ThreadLocal存储当前租户标识,实现动态路由到对应公司的数据库:
public class DynamicDataSource extends AbstractRoutingDataSource { // 存储已加载的租户数据源,避免重复创建 private final Map<Object, Object> targetDataSources = new ConcurrentHashMap<>(); // 当前租户标识线程变量 private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>(); public static void setCurrentTenant(String tenantId) { CURRENT_TENANT.set(tenantId); } public static void clearCurrentTenant() { CURRENT_TENANT.remove(); } @Override protected Object determineCurrentLookupKey() { return CURRENT_TENANT.get(); } // 动态添加租户数据源 public void addTargetDataSource(String tenantId, DataSource dataSource) { targetDataSources.put(tenantId, dataSource); super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); } }
动态数据源配置类
@Configuration public class DataSourceConfig { // 元数据库配置(application.yml中配置) @Value("${spring.datasource.meta.url}") private String metaDbUrl; @Value("${spring.datasource.meta.username}") private String metaDbUsername; @Value("${spring.datasource.meta.password}") private String metaDbPassword; // 元数据库数据源 @Bean public DataSource metaDataSource() { return DataSourceBuilder.create() .url(metaDbUrl) .username(metaDbUsername) .password(metaDbPassword) .driverClassName("com.mysql.cj.jdbc.Driver") .build(); } // 动态数据源(作为默认数据源) @Bean public DynamicDataSource dynamicDataSource() { DynamicDataSource dynamicDS = new DynamicDataSource(); // 设置默认数据源为元数据库(可选,避免无租户时报错) dynamicDS.setDefaultTargetDataSource(metaDataSource()); return dynamicDS; } // 配置JdbcTemplate使用动态数据源 @Bean public JdbcTemplate jdbcTemplate() { return new JdbcTemplate(dynamicDataSource()); } }
3. 自定义UserDetailsService实现多租户登录验证
重写UserDetailsService,在用户登录时解析租户标识,切换到对应数据库验证用户:
@Service public class TenantUserDetailsService implements UserDetailsService { @Autowired private TenantDataSourceRepository tenantRepo; @Autowired private DynamicDataSource dynamicDataSource; @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String loginIdentifier) throws UsernameNotFoundException { // 假设登录标识格式为「companyId:username」,也可通过请求参数单独传递companyId String[] parts = loginIdentifier.split(":"); if (parts.length != 2) { throw new UsernameNotFoundException("登录格式错误,请输入「公司ID:用户名」"); } String companyId = parts[0]; String username = parts[1]; // 从元数据库获取公司数据源配置 TenantDataSource tenantDS = tenantRepo.findByCompanyId(companyId); if (tenantDS == null) { throw new UsernameNotFoundException("公司不存在"); } // 复用已加载的数据源,不存在则创建 if (!dynamicDataSource.getTargetDataSources().containsKey(companyId)) { DataSource companyDS = DataSourceBuilder.create() .url(tenantDS.getDbUrl()) .username(tenantDS.getDbUsername()) .password(tenantDS.getDbPassword()) .driverClassName("com.mysql.cj.jdbc.Driver") .build(); dynamicDataSource.addTargetDataSource(companyId, companyDS); } // 切换到当前租户的数据源 DynamicDataSource.setCurrentTenant(companyId); // 从公司数据库查询用户信息(这里用JdbcTemplate,也可替换为JPA) JdbcTemplate jdbcTemplate = new JdbcTemplate((DataSource) dynamicDataSource.getTargetDataSources().get(companyId)); String userSql = "SELECT username, password, enabled, role FROM users WHERE username = ?"; try { return jdbcTemplate.queryForObject(userSql, new Object[]{username}, (rs, rowNum) -> User.withUsername(rs.getString("username")) .password(rs.getString("password")) .disabled(!rs.getBoolean("enabled")) .authorities(rs.getString("role")) .passwordEncoder(passwordEncoder::matches) .build() ); } catch (EmptyResultDataAccessException e) { throw new UsernameNotFoundException("用户不存在"); } } }
4. Spring Security配置调整
配置自定义的UserDetailsService,并处理登录/登出逻辑:
@Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private TenantUserDetailsService userDetailsService; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/login").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .usernameParameter("loginIdentifier") // 对应前端登录输入框的name属性 .defaultSuccessUrl("/dashboard") .failureUrl("/login?error") ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login") .addLogoutHandler((request, response, authentication) -> { // 登出时清除租户标识 DynamicDataSource.clearCurrentTenant(); }) ) .userDetailsService(userDetailsService); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
5. 请求结束后清除租户标识
添加过滤器,在请求完成后清除ThreadLocal中的租户标识,避免内存泄漏:
@Component public class TenantClearFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { filterChain.doFilter(request, response); } finally { DynamicDataSource.clearCurrentTenant(); } } }
关键注意事项
- 数据源缓存:
DynamicDataSource中维护的targetDataSources会缓存已加载的数据源,避免每次登录重复创建连接池。 - ThreadLocal清理:必须在请求结束后清除租户标识,否则会导致线程复用时代码逻辑错误。
- 异常处理:需针对租户不存在、数据库连接失败、用户不存在等场景添加友好的错误提示。
- JPA适配:如果使用JPA,需要配置动态
EntityManagerFactory,或者为每个租户单独创建EntityManager,相比JdbcTemplate复杂度更高,建议登录验证阶段先用JdbcTemplate实现。
内容的提问来源于stack exchange,提问作者user20432214
相关产品推荐
相关产品推荐

