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
相关产品推荐
相关产品推荐

