You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Security多租户JWT令牌跨租户验证实现咨询

多租户JWT隔离验证实现方案

针对你的需求,我们可以通过在JWT中注入租户标识 + 资源服务器端验证租户匹配的方式实现租户隔离,以下是具体步骤和代码实现:


1. 认证服务器:将租户ID写入JWT令牌

首先需要在生成JWT时,把客户端所属的租户ID添加到令牌的Claims中,确保令牌携带租户身份信息。

1.1 映射配置文件属性

先把你配置的多租户客户端属性映射为Java实体类,方便后续注册客户端:

@Data
@ConfigurationProperties(prefix = "clientcredential")
public class MultiTenantClientProperties {
    private List<TenantClient> clients;

    @Data
    public static class TenantClient {
        private String tenant;
        private List<ClientDetail> clientdetails;

        @Data
        public static class ClientDetail {
            private String name;
            private String scope;
            private String clientId;
            private String clientSecret;
        }
    }
}

1.2 注册多租户客户端到内存仓库

在认证服务器配置中,将每个客户端关联对应的租户ID,并存入客户端属性:

@Configuration
@EnableAuthorizationServer
@EnableConfigurationProperties(MultiTenantClientProperties.class)
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    private final MultiTenantClientProperties clientProperties;
    private final AuthenticationManager authenticationManager;
    private final PasswordEncoder passwordEncoder;

    public AuthServerConfig(MultiTenantClientProperties clientProperties,
                            AuthenticationManager authenticationManager,
                            PasswordEncoder passwordEncoder) {
        this.clientProperties = clientProperties;
        this.authenticationManager = authenticationManager;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        InMemoryClientDetailsServiceBuilder builder = clients.inMemory();
        // 遍历配置中的租户和客户端,注册到内存仓库
        clientProperties.getClients().forEach(tenantClient -> {
            String tenantId = tenantClient.getTenant();
            tenantClient.getClientdetails().forEach(client -> {
                builder.withClient(client.getClientId())
                        .secret(passwordEncoder.encode(client.getClientSecret()))
                        .scopes(client.getScope().split(","))
                        .authorizedGrantTypes("client_credentials")
                        .accessTokenValiditySeconds(3600)
                        // 将租户ID存入客户端属性,后续写入JWT
                        .attributes(Map.of("tenant_id", tenantId));
            });
        });
    }

    // 自定义Token增强器,把租户ID写入JWT的额外Claims
    @Bean
    public TokenEnhancer tenantTokenEnhancer() {
        return (accessToken, authentication) -> {
            OAuth2Authentication oAuth2Auth = (OAuth2Authentication) authentication;
            ClientDetails clientDetails = oAuth2Auth.getOAuth2Request().getClientDetails();
            String tenantId = (String) clientDetails.getAttributes().get("tenant_id");
            
            if (tenantId != null) {
                ((DefaultOAuth2AccessToken) accessToken)
                        .getAdditionalInformation()
                        .put("tenant_id", tenantId);
            }
            return accessToken;
        };
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        enhancerChain.setTokenEnhancers(List.of(tenantTokenEnhancer(), new JwtAccessTokenConverter()));

        endpoints.authenticationManager(authenticationManager)
                .accessTokenConverter(new JwtAccessTokenConverter())
                .tokenEnhancer(enhancerChain);
    }
}

2. 资源服务器:验证租户匹配

在资源服务器端,提取请求路径中的租户ID,与JWT中的租户ID对比,不一致则拒绝请求。

2.1 自定义租户验证过滤器

编写一个过滤器,拦截目标端点并验证租户匹配:

@Component
public class TenantValidationFilter extends OncePerRequestFilter {

    private final JwtDecoder jwtDecoder;

    public TenantValidationFilter(JwtDecoder jwtDecoder) {
        this.jwtDecoder = jwtDecoder;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        // 匹配需要验证租户的端点:/api/v1/save/{tenant}
        String requestUri = request.getRequestURI();
        Matcher tenantMatcher = Pattern.compile("/api/v1/save/(\\w+)").matcher(requestUri);
        
        if (tenantMatcher.find()) {
            String pathTenant = tenantMatcher.group(1);
            String authHeader = request.getHeader("Authorization");

            // 校验Token格式
            if (authHeader == null || !authHeader.startsWith("Bearer ")) {
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "无效或缺失的身份凭证");
                return;
            }

            try {
                String token = authHeader.substring(7);
                Jwt jwt = jwtDecoder.decode(token);
                String tokenTenant = jwt.getClaim("tenant_id");

                // 对比路径租户与Token租户
                if (!pathTenant.equals(tokenTenant)) {
                    response.sendError(HttpServletResponse.SC_FORBIDDEN, "令牌租户与请求租户不匹配");
                    return;
                }
            } catch (JwtException e) {
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "无效的JWT令牌");
                return;
            }
        }

        filterChain.doFilter(request, response);
    }
}

2.2 配置资源服务器过滤器链

将自定义过滤器加入Spring Security的过滤流程:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    private final TenantValidationFilter tenantValidationFilter;

    public ResourceServerConfig(TenantValidationFilter tenantValidationFilter) {
        this.tenantValidationFilter = tenantValidationFilter;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(tenantValidationFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/api/v1/save/**").authenticated()
                .anyRequest().permitAll();
    }

    // 配置JWT解码器,与认证服务器的签名方式保持一致
    @Bean
    public JwtDecoder jwtDecoder() {
        // 若用非对称加密,配置JWKS地址
        return NimbusJwtDecoder.withJwkSetUri("http://your-auth-server/.well-known/jwks.json").build();
        
        // 若用对称加密,替换为以下代码:
        // SecretKey secretKey = new SecretKeySpec("your-shared-secret".getBytes(), "HS256");
        // return NimbusJwtDecoder.withSecretKey(secretKey).build();
    }
}

备选方案:方法级安全验证

如果更倾向于用注解实现验证,可以在Controller方法上添加@PreAuthorize注解:

@RestController
@RequestMapping("/api/v1/save")
public class SaveController {

    @GetMapping("/{tenant}")
    @PreAuthorize("#tenant == authentication.principal.attributes['tenant_id']")
    public ResponseEntity<String> save(@PathVariable String tenant) {
        // 业务逻辑实现
        return ResponseEntity.ok("租户" + tenant + "操作成功");
    }
}

需要开启方法级安全支持:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}

同时需要自定义JWT认证转换器,将租户ID存入Authentication的属性中:

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
    authoritiesConverter.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
    converter.setAuthenticationConverter(jwt -> {
        AbstractAuthenticationToken auth = converter.convert(jwt);
        auth.setDetails(jwt.getClaims());
        return auth;
    });
    return converter;
}

在资源服务器配置中启用该转换器:

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    resources.jwtAuthenticationConverter(jwtAuthenticationConverter());
}

内容的提问来源于stack exchange,提问作者Karan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 10:23:12