Spring Security:已登录用户切换租户生成新AccessToken方案问询
多租户系统租户切换实现方案
核心思路
利用已登录用户的有效JWT作为身份凭证,向认证服务器发起租户切换请求,携带当前JWT和目标tenantId。认证服务器验证JWT有效性、用户与目标租户的关联权限后,生成包含新tenantId的新Access Token返回给客户端,无需用户再次输入密码。
具体实现步骤
1. 后端新增租户切换API(认证服务器端)
在认证服务器中新增接口处理租户切换请求:
@RestController @RequestMapping("/auth") public class TenantSwitchController { @Autowired private JwtDecoder jwtDecoder; @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenGenerator jwtTokenGenerator; // 自定义JWT生成器,需自行实现 @PostMapping("/switch-tenant") public ResponseEntity<AuthResponse> switchTenant( @RequestHeader("Authorization") String authHeader, @RequestParam String newTenantId) { // 1. 解析并验证当前JWT String token = authHeader.replace("Bearer ", ""); Jwt jwt = jwtDecoder.decode(token); String username = jwt.getSubject(); // 2. 加载用户详情,验证用户是否有权访问新租户 SpringSecurityUserWithAdditionalData user = (SpringSecurityUserWithAdditionalData) userDetailsService.loadUserByUsername(username); if (!user.getTenantIds().contains(newTenantId)) { // 假设用户实体维护了可访问租户ID集合 throw new AccessDeniedException("无权限访问该租户"); } // 3. 生成包含新tenantId的新JWT Map<String, Object> extraClaims = new HashMap<>(); extraClaims.put("tenantId", newTenantId); extraClaims.put(GlobalConstants.JWT_CLAIM_ROLA_LIST, user.getAuthorities()); // 保留原有权限 String newToken = jwtTokenGenerator.generateToken(user, extraClaims); // 4. 返回新Token return ResponseEntity.ok(new AuthResponse(newToken)); } }
2. 自定义JWT生成器实现
封装JWT生成逻辑:
@Component public class JwtTokenGenerator { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private long expirationMs; public String generateToken(UserDetails userDetails, Map<String, Object> extraClaims) { return Jwts.builder() .setClaims(extraClaims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + expirationMs)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } }
3. 调整Security配置,放行切换接口
在WebSecurityConfiguration的defaultSecurityFilterChain中,允许已认证用户访问切换接口:
// @formatter:off @Bean SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http ... .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/changeTenant").permitAll() .requestMatchers("/auth/switch-tenant").authenticated() // 仅允许已认证用户访问 .anyRequest().authenticated()) .and() .logout() .clearAuthentication(true) .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .logoutSuccessHandler(new CustomLogoutSuccessHandler()) .and() .formLogin() .loginPage("/login") .permitAll().and(); return http.build(); } // @formatter:on
4. 前端客户端实现
用户选择目标租户后,发起切换请求并更新本地Token:
async function switchTenant(newTenantId) { const currentToken = localStorage.getItem('accessToken'); const response = await fetch('/auth/switch-tenant', { method: 'POST', headers: { 'Authorization': `Bearer ${currentToken}`, 'Content-Type': 'application/x-www-form-urlencoded' }, body: `newTenantId=${newTenantId}` }); if (response.ok) { const data = await response.json(); localStorage.setItem('accessToken', data.token); // 刷新页面或重新加载用户权限、菜单等数据 window.location.reload(); } else { alert('切换租户失败:' + await response.text()); } }
5. 扩展用户实体与UserDetails
确保SpringSecurityUserWithAdditionalData包含用户可访问的租户ID集合:
public class SpringSecurityUserWithAdditionalData extends User { private Set<String> tenantIds; // 用户可访问的租户ID集合 // 构造函数、getter/setter public SpringSecurityUserWithAdditionalData(String username, String password, Collection<? extends GrantedAuthority> authorities, Set<String> tenantIds) { super(username, password, authorities); this.tenantIds = tenantIds; } public Set<String> getTenantIds() { return tenantIds; } }
关键注意事项
- Token有效性验证:必须校验当前JWT的签名、过期时间,拒绝非法请求。
- 租户权限校验:严格验证用户与目标租户的关联关系,防止越权访问。
- Token失效策略:原Token可选择继续有效或加入黑名单立即失效,需根据业务需求决定。
- 会话同步:切换租户后,前端需更新本地Token并重新加载租户相关的用户数据。
内容的提问来源于stack exchange,提问作者Igor Milovanović
相关产品推荐
相关产品推荐

