这确实是一个很好的问题,我真的很想知道如何在新的授权服务器中以适当的方式做到这一点。在 Spring Resource Server 中有一个关于多租户的部分。我成功地做到了。
至于新的 Spring Authorization Server 多租户问题。我还为密码和客户端凭据授予类型完成了此操作。
但请注意,虽然它可以工作,但这是多么完美。我不知道,因为我只是为了学习目的而这样做的。这只是一个样本。当我为授权代码授予类型执行此操作时,我也会将其发布在我的 github 上。
我假设主数据库和租户数据库配置已经完成。我不能在这里提供整个代码,因为它有很多代码。我只会提供相关的sn-ps。但这只是示例
@Configuration
@Import({MasterDatabaseConfiguration.class, TenantDatabaseConfiguration.class})
public class DatabaseConfiguration {
}
我使用了单独的数据库。我所做的我在 AuthorizationServerConfiguration 中使用了类似以下的内容。
@Import({OAuth2RegisteredClientConfiguration.class})
public class AuthorizationServerConfiguration {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
....
http.addFilterBefore(new TenantFilter(), OAuth2AuthorizationRequestRedirectFilter.class);
SecurityFilterChain securityFilterChain = http.formLogin(Customizer.withDefaults()).build();
addCustomOAuth2ResourceOwnerPasswordAuthenticationProvider(http);
return securityFilterChain;
}
}
这是我的 TenantFilter 代码
public class TenantFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LogManager.getLogger();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestUrl = request.getRequestURL().toString();
if (!requestUrl.endsWith("/oauth2/jwks")) {
String tenantDatabaseName = request.getParameter("tenantDatabaseName");
if(StringUtils.hasText(tenantDatabaseName)) {
LOGGER.info("tenantDatabaseName request parameter is found");
TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
} else {
LOGGER.info("No tenantDatabaseName request parameter is found");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("{'error': 'No tenant request parameter supplied'}");
response.getWriter().flush();
return;
}
}
filterChain.doFilter(request, response);
}
public static String getFullURL(HttpServletRequest request) {
StringBuilder requestURL = new StringBuilder(request.getRequestURL().toString());
String queryString = request.getQueryString();
if (queryString == null) {
return requestURL.toString();
} else {
return requestURL.append('?').append(queryString).toString();
}
}
}
这是 TenantDBContextHolder 类
public class TenantDBContextHolder {
private static final ThreadLocal<String> TENANT_DB_CONTEXT_HOLDER = new ThreadLocal<>();
public static void setCurrentDb(String dbType) {
TENANT_DB_CONTEXT_HOLDER.set(dbType);
}
public static String getCurrentDb() {
return TENANT_DB_CONTEXT_HOLDER.get();
}
public static void clear() {
TENANT_DB_CONTEXT_HOLDER.remove();
}
}
然后在令牌定制器中。您可以使用以下内容
public class UsernamePasswordAuthenticationTokenJwtCustomizerHandler extends AbstractJwtCustomizerHandler {
....
@Override
protected void customizeJwt(JwtEncodingContext jwtEncodingContext) {
....
String tenantDatabaseName = TenantDBContextHolder.getCurrentDb();
if (StringUtils.hasText(tenantDatabaseName)) {
URL issuerURL = jwtClaimSetBuilder.build().getIssuer();
String issuer = issuerURL + "/" + tenantDatabaseName;
jwtClaimSetBuilder.claim(JwtClaimNames.ISS, issuer);
}
jwtClaimSetBuilder.claims(claims ->
userAttributes.entrySet().stream()
.forEach(entry -> claims.put(entry.getKey(), entry.getValue()))
);
}
}
现在我假设资源服务器也配置为多租户。这是链接Spring Security Resource Server Multitenancy。基本上你必须为多租户配置两个bean,如下所示
public class OAuth2ResourceServerConfiguration {
....
@Bean
public JWTProcessor<SecurityContext> jwtProcessor(JWTClaimsSetAwareJWSKeySelector<SecurityContext> keySelector) {
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector);
return jwtProcessor;
}
@Bean
public JwtDecoder jwtDecoder(JWTProcessor<SecurityContext> jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(JwtValidators.createDefault(), jwtValidator);
decoder.setJwtValidator(validator);
return decoder;
}
}
现在春季开两节课。您可以从中获取令牌中的租户标识符。
@Component
public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
private final TenantDataSourceRepository tenantDataSourceRepository;
private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();
....
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
String issuerURL = toTenant(token);
JwtIssuerValidator jwtIssuerValidator = validators.computeIfAbsent(issuerURL, this::fromTenant);
OAuth2TokenValidatorResult oauth2TokenValidatorResult = jwtIssuerValidator.validate(token);
String tenantDatabaseName = JwtService.getTenantDatabaseName(token);
TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
return oauth2TokenValidatorResult;
}
private String toTenant(Jwt jwt) {
return jwt.getIssuer().toString();
}
private JwtIssuerValidator fromTenant(String tenant) {
String issuerURL = tenant;
String tenantDatabaseName = JwtService.getTenantDatabaseName(issuerURL);
TenantDataSource tenantDataSource = tenantDataSourceRepository.findByDatabaseName(tenantDatabaseName);
if (tenantDataSource == null) {
throw new IllegalArgumentException("unknown tenant");
}
JwtIssuerValidator jwtIssuerValidator = new JwtIssuerValidator(issuerURL);
return jwtIssuerValidator;
}
}
相似地
@Component
public class TenantJWSKeySelector implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
....
@Override
public List<? extends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext) throws KeySourceException {
String tenant = toTenantDatabaseName(jwtClaimsSet);
JWSKeySelector<SecurityContext> jwtKeySelector = selectors.computeIfAbsent(tenant, this::fromTenant);
List<? extends Key> jwsKeys = jwtKeySelector.selectJWSKeys(jwsHeader, securityContext);
return jwsKeys;
}
}
现在授权代码授权类型授权类型流程怎么样。在这种情况下,我也得到了租户标识符。但是当它将我重定向到登录页面时,我丢失了租户标识符,因为我认为它会根据授权代码请求为登录页面创建一个新请求。无论如何,我不确定,因为我必须查看授权代码流的代码它实际上在做什么。因此,当我将我重定向到登录页面时,我的租户标识符正在丢失。
但是在密码授予类型和客户端凭据授予类型的情况下,没有重定向,因此我在稍后阶段获得租户标识符,并且可以成功使用它来放入我的令牌声明中。
然后在资源服务器上我得到颁发者 url。从颁发者 url 获取租户标识符。验证它。它连接到资源服务器上的租户数据库。
我是如何测试的。我用的是spring客户端。您可以自定义授权码流的请求。包含自定义参数的密码和客户端凭据。
谢谢。