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

