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

Spring Authorization Server多租户OAuth2授权码流自定义认证方案咨询

基于Spring Boot 3 + Spring Authorization Server + JDK 21的多租户OAuth2自定义流程实现

业务需求

构建多租户Web服务,需实现以下OAuth2授权码流逻辑:

  • 第三方客户端(如Team Tailor)向Auth2服务发起授权码请求
  • Auth2服务通过Client ID识别租户,重定向用户到租户端完成认证(SSO/账号密码登录)
  • 租户认证成功后返回JWT,Auth2服务解析该JWT的Claims(如username、用户档案ID)
  • 将Claims传递给Spring Authorization Server,由其完成后续流程并为第三方客户端签发最终JWT(禁止手动生成令牌)

核心问题与解决方案

1. 拦截授权码请求并提取Client ID,重定向至租户登录页

通过自定义过滤器拦截/oauth2/authorize端点的未认证请求,提取Client ID并触发租户重定向:

自定义租户授权请求过滤器

public class TenantAuthorizationRequestFilter extends OncePerRequestFilter {
    private final ClientRegistrationRepository clientRepo;
    private final TenantService tenantService; // 自定义服务,根据Client ID获取租户登录URL
    private final RedisTemplate<String, String> redisTemplate;

    public TenantAuthorizationRequestFilter(ClientRegistrationRepository clientRepo, TenantService tenantService, RedisTemplate<String, String> redisTemplate) {
        this.clientRepo = clientRepo;
        this.tenantService = tenantService;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String uri = request.getRequestURI();
        // 仅处理未认证的授权码请求
        if ("/oauth2/authorize".equals(uri) && request.getUserPrincipal() == null) {
            String clientId = request.getParameter("client_id");
            String state = request.getParameter("state");
            String redirectUri = request.getParameter("redirect_uri");

            if (clientId != null && state != null) {
                // 校验客户端合法性
                ClientRegistration client = clientRepo.findByRegistrationId(clientId);
                if (client != null) {
                    // 保存授权请求上下文,用于回调时恢复
                    String contextKey = "auth_ctx:" + state;
                    redisTemplate.opsForValue().set(contextKey, 
                        new ObjectMapper().writeValueAsString(Map.of("clientId", clientId, "redirectUri", redirectUri)),
                        30, TimeUnit.MINUTES);
                    // 重定向到租户登录页,携带state
                    String tenantLoginUrl = tenantService.getTenantLoginUrl(clientId) + "?state=" + state;
                    response.sendRedirect(tenantLoginUrl);
                    return;
                }
            }
        }
        chain.doFilter(request, response);
    }
}

注册过滤器到Security链

将过滤器放在OAuth2AuthorizationRequestRedirectFilter之前,确保优先拦截请求:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, TenantAuthorizationRequestFilter tenantFilter) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .oauth2AuthorizationServer(oauth2 -> oauth2
                .authorizationEndpoint(endpoint -> endpoint
                    .authorizationRequestRepository(cookieAuthorizationRequestRepository())
                )
            )
            .addFilterBefore(tenantFilter, OAuth2AuthorizationRequestRedirectFilter.class);
        return http.build();
    }

    // 可选:用Cookie保存授权请求(默认实现)
    private OAuth2AuthorizationRequestRepository<OAuth2AuthorizationRequest> cookieAuthorizationRequestRepository() {
        return new CookieOAuth2AuthorizationRequestRepository();
    }
}

2. 避免租户回调的无限循环

核心是使用专属回调端点处理租户返回的JWT,而非直接回到授权端点:

租户回调控制器

@RestController
@RequestMapping("/tenant/callback")
public class TenantCallbackController {
    private final JwtDecoder jwtDecoder;
    private final RedisTemplate<String, String> redisTemplate;
    private final ObjectMapper objectMapper;

    public TenantCallbackController(JwtDecoder jwtDecoder, RedisTemplate<String, String> redisTemplate, ObjectMapper objectMapper) {
        this.jwtDecoder = jwtDecoder;
        this.redisTemplate = redisTemplate;
        this.objectMapper = objectMapper;
    }

    @GetMapping
    public String handleCallback(@RequestParam String state, @RequestParam String tenantJwt) throws Exception {
        // 1. 恢复授权请求上下文
        String contextStr = redisTemplate.opsForValue().get("auth_ctx:" + state);
        if (contextStr == null) throw new IllegalArgumentException("Invalid/expired state");
        Map<String, String> authCtx = objectMapper.readValue(contextStr, Map.class);
        String clientId = authCtx.get("clientId");
        String redirectUri = authCtx.get("redirectUri");

        // 2. 验证并解析租户JWT
        Jwt decodedJwt = jwtDecoder.decode(tenantJwt);
        Map<String, Object> userClaims = decodedJwt.getClaims();

        // 3. 构建认证对象,存入SecurityContext
        Collection<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ROLE_USER"));
        Authentication auth = new UsernamePasswordAuthenticationToken(
            decodedJwt.getSubject(),
            null,
            authorities
        );
        auth.setDetails(userClaims); // 将租户Claims存入认证详情
        SecurityContextHolder.getContext().setAuthentication(auth);

        // 4. 重定向回授权端点,继续流程(此时用户已认证,不会触发租户重定向)
        return String.format("redirect:/oauth2/authorize?client_id=%s&redirect_uri=%s&state=%s",
            clientId, redirectUri, state);
    }
}

关键逻辑:回调完成后用户已处于认证状态,再次进入/oauth2/authorize时,过滤器会跳过重定向,避免循环。

3. 让Spring Authorization Server自动生成令牌

自定义OAuth2UserService,从认证对象中提取租户JWT的Claims,供授权服务器使用:

自定义OAuth2用户服务

@Service
public class TenantClaimsOAuth2UserService implements OAuth2UserService<OAuth2AuthorizationRequest, OAuth2User> {
    @Override
    public OAuth2User loadUser(OAuth2AuthorizationRequest authorizationRequest) throws OAuth2AuthenticationException {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        // 从认证详情中获取租户JWT的Claims
        Map<String, Object> tenantClaims = (Map<String, Object>) auth.getDetails();
        
        // 返回OAuth2User对象,授权服务器会用这些Claims生成最终令牌
        return new DefaultOAuth2User(
            auth.getAuthorities(),
            tenantClaims,
            "username" // 指定用户名对应的Claim键
        );
    }
}

配置授权服务器使用自定义用户服务

@Configuration
public class AuthorizationServerConfig {
    @Bean
    public OAuth2AuthorizationServerConfigurer authorizationServerConfigurer(TenantClaimsOAuth2UserService userService) {
        return new OAuth2AuthorizationServerConfigurer()
            .authorizationEndpoint(endpoint -> endpoint
                .userService(userService) // 绑定自定义用户服务
            );
    }
}

这样Spring Authorization Server会自动使用租户JWT的Claims生成最终的访问令牌/ID令牌,无需手动处理令牌生成逻辑。

完整流程梳理

  1. 第三方客户端发起/oauth2/authorize请求,携带client_id、state、redirect_uri
  2. 租户过滤器拦截未认证请求,保存授权上下文,重定向到租户登录页
  3. 用户在租户端完成认证,租户携带state和tenantJwt回调到/tenant/callback
  4. 回调端点验证JWT,构建认证对象存入SecurityContext,重定向回授权端点
  5. 授权端点检测用户已认证,调用自定义用户服务获取租户Claims
  6. Spring Authorization Server生成包含租户Claims的授权码,返回给第三方客户端
  7. 第三方客户端用授权码换取最终令牌,流程完成

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 01:05:03