如何用Micronaut实现Schema模式多租户?含认证及注册疑问
一、改造AuthenticationProvider支持租户感知
你的推测完全正确——当前认证逻辑没有关联租户信息,导致无论访问哪个子域名,都会默认使用public schema的用户数据。需要按以下步骤改造:
先验证租户解析器是否生效
先确认子域名租户解析是否正常工作,可添加一个请求拦截器打印当前租户ID:@InterceptorBean(TenantInterceptor.class) public class TenantLoggingInterceptor implements HttpRequestInterceptor { @Override public Publisher<MutableHttpRequest<?>> intercept(HttpRequest<?> request, HttpRequestInterceptorChain chain) { Optional<String> tenantId = TenantContext.currentTenantId(); System.out.println("当前租户ID: " + tenantId.orElse("未识别")); return chain.proceed(request); } }访问
subdomain.localhost时,如果控制台输出当前租户ID: subdomain,说明解析器生效;若未输出,需检查Micronaut版本是否支持该配置,或是否存在其他租户解析器冲突。修改认证逻辑关联租户
在AuthenticationProvider中获取当前租户ID,查询对应schema下的用户数据。如果使用JDBC操作,可动态拼接schema:@Singleton public class CustomAuthenticationProvider implements AuthenticationProvider { private final JdbcOperations jdbcOperations; public CustomAuthenticationProvider(JdbcOperations jdbcOperations) { this.jdbcOperations = jdbcOperations; } @Override public Publisher<AuthenticationResponse> authenticate(HttpRequest<?> request, AuthenticationRequest<?, ?> authRequest) { Optional<String> tenantId = TenantContext.currentTenantId(); if (tenantId.isEmpty()) { return Publishers.just(AuthenticationResponse.failure("租户未识别")); } // 动态指定租户schema查询用户 String sql = String.format("SELECT id, username, password FROM %s.users WHERE username = ?", tenantId.get()); return jdbcOperations.findOne(sql, User.class, authRequest.getIdentity()) .map(user -> { // 密码校验逻辑(示例用BCrypt) if (BCrypt.checkpw(new String((char[]) authRequest.getSecret()), user.getPassword())) { return AuthenticationResponse.success(user.getUsername(), Collections.singletonList("USER")); } else { return AuthenticationResponse.failure("密码错误"); } }) .switchIfEmpty(Publishers.just(AuthenticationResponse.failure("用户不存在"))); } }如果使用Micronaut Data Repository,可通过动态参数指定schema:
@Repository public interface UserRepository extends CrudRepository<User, Long> { @Query("SELECT u FROM :schema.users u WHERE u.username = :username") Optional<User> findByUsernameAndSchema(@Param("schema") String schema, @Param("username") String username); }然后在认证逻辑中调用
userRepository.findByUsernameAndSchema(tenantId.get(), (String) authRequest.getIdentity())即可。
二、未认证时创建租户Schema
新租户注册阶段无认证信息,TenantContext中没有租户ID,需手动处理Schema创建:
注册接口中手动执行Schema创建
在注册接口接收租户标识(如子域名),先执行创建Schema的SQL,再初始化表结构:@Controller("/register") public class TenantRegistrationController { private final JdbcOperations jdbcOperations; public TenantRegistrationController(JdbcOperations jdbcOperations) { this.jdbcOperations = jdbcOperations; } @Post public HttpResponse<String> registerTenant(@Body TenantRegistrationRequest request) { String tenantId = request.getSubdomain(); // 创建租户Schema jdbcOperations.execute(String.format("CREATE SCHEMA IF NOT EXISTS %s", tenantId)); // 初始化表结构 initializeTenantTables(tenantId); return HttpResponse.ok("租户注册成功"); } private void initializeTenantTables(String tenantId) { String createUserTableSql = String.format(""" CREATE TABLE IF NOT EXISTS %s.users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL ) """, tenantId); jdbcOperations.execute(createUserTableSql); } }用数据库迁移工具规范初始化
更可靠的方式是结合Flyway或Liquibase做Schema版本化管理,手动指定租户Schema执行迁移:private void initializeWithFlyway(String tenantId) { Flyway flyway = Flyway.configure() .dataSource(jdbcOperations.getDataSource()) .schemas(tenantId) .locations("db/migration/tenant") // 存放租户通用迁移脚本的路径 .load(); flyway.migrate(); }这种方式能确保所有租户的Schema结构一致,且支持后续的结构升级。
内容的提问来源于stack exchange,提问作者rich

