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

如何在Spring Cloud Gateway中通过WebSession存储OAuth2密码授权信息?

问题描述
  • 基于Spring Cloud Gateway实现BFF架构,需将原有授权码流替换为资源所有者密码凭证流(密码授权)
  • 已通过OAuth2AuthorizeRequest成功获取Access Token,但不清楚如何基于返回结果创建SecurityContext并将其存储到BFF的WebSession中
  • 尝试配置AuthenticationWebFilter与ServerFormLoginAuthenticationConverter转换请求,但未找到可行方向
  • 备注:已知密码授权已被弃用,但无法自主决定技术选型

解决方案

1. 先确保OAuth2授权客户端与管理器配置正确

首先要初始化支持密码授权的客户端注册与授权管理器,这是后续操作的基础:

@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
        ReactiveClientRegistrationRepository clientRegistrationRepository,
        ReactiveOAuth2AuthorizedClientService authorizedClientService) {

    // 构建支持密码授权+刷新令牌的客户端提供者
    ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
            ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                    .password()
                    .refreshToken()
                    .build();

    DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultReactiveOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientService);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

2. 自定义登录接口,生成SecurityContext并绑定到WebSession

修改你的/do-login接口,在获取到授权客户端后,创建包含用户身份与令牌的认证对象,将其存入SecurityContext并绑定到当前WebSession:

@GetMapping("/do-login")
public Mono<String> login(ServerWebExchange exchange,
                          @RequestParam String username,
                          @RequestParam String password) {
    // 构建密码授权请求
    OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("keycloak-pwd")
            .principal(new UsernamePasswordAuthenticationToken(username, password))
            .attribute(ServerWebExchange.class.getName(), exchange)
            .build();

    return this.authorizedClientManager.authorize(authorizeRequest)
            .flatMap(authorizedClient -> {
                // 从授权客户端中提取令牌与用户身份信息
                OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
                OAuth2AuthenticatedPrincipal principal = authorizedClient.getPrincipal();
                
                // 构建标准认证对象,可按需添加用户权限信息
                Authentication authentication = new OAuth2AuthenticationToken(
                        principal,
                        Collections.emptyList(),
                        authorizedClient.getClientRegistration().getRegistrationId()
                );

                // 将SecurityContext存入WebSession
                return exchange.getSession()
                        .flatMap(session -> {
                            SecurityContext securityContext = new SecurityContextImpl();
                            securityContext.setAuthentication(authentication);
                            return session.getAttributes()
                                    .put(SecurityRepositoryWebFilter.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME, securityContext)
                                    .then(Mono.just("index"));
                        });
            })
            .onErrorResume(e -> {
                // 处理认证失败场景,比如返回登录失败页面或JSON
                return Mono.just("login-failure");
            });
}

3. 配置Spring Security过滤器链,启用Session与认证加载

配置过滤器链,确保系统能从WebSession中读取SecurityContext,同时保护你的路由资源:

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
            .authorizeExchange(exchanges -> exchanges
                    .pathMatchers("/do-login").permitAll() // 开放登录接口
                    .anyExchange().authenticated() // 其他接口需认证
            )
            .oauth2Login(oauth2 -> oauth2.disable()) // 禁用默认授权码流登录逻辑
            .sessionManagement(session -> session
                    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 按需创建Session
            )
            .securityContextRepository(new WebSessionServerSecurityContextRepository()); // 指定从WebSession加载SecurityContext

    return http.build();
}

4. (可选)通过表单登录方式处理密码授权

如果需要支持表单提交用户名密码(而非直接调用/do-login),可以配置AuthenticationWebFilter来处理:

第一步:自定义认证管理器与登录过滤器

// 自定义认证管理器,调用授权管理器完成密码授权并生成认证对象
@Bean
public ReactiveAuthenticationManager authenticationManager(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
    return authentication -> {
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
        String username = token.getName();
        String password = token.getCredentials().toString();

        OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("keycloak-pwd")
                .principal(token)
                .build();

        return authorizedClientManager.authorize(authorizeRequest)
                .map(authorizedClient -> {
                    OAuth2AuthenticatedPrincipal principal = authorizedClient.getPrincipal();
                    return new OAuth2AuthenticationToken(
                            principal,
                            Collections.emptyList(),
                            authorizedClient.getClientRegistration().getRegistrationId()
                    );
                });
    };
}

// 配置表单登录过滤器
@Bean
public AuthenticationWebFilter authenticationWebFilter(ReactiveAuthenticationManager authenticationManager) {
    AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager);
    // 从表单参数中提取用户名密码
    filter.setServerAuthenticationConverter(new ServerFormLoginAuthenticationConverter());
    // 指定触发登录的路径与请求方法
    filter.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login"));
    return filter;
}

第二步:将过滤器添加到Security链中

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
                                                       AuthenticationWebFilter authenticationWebFilter) {
    http
            .authorizeExchange(exchanges -> exchanges
                    .pathMatchers("/login").permitAll()
                    .anyExchange().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2.disable())
            .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) // 添加表单登录过滤器
            .sessionManagement(session -> session
                    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            )
            .securityContextRepository(new WebSessionServerSecurityContextRepository());

    return http.build();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 08:33:38