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

Spring MVC配置远程OAuth2密码模式登录过滤器的问题

问题描述

我有一个Spring MVC应用和一个独立运行的OAuth2授权服务器,这个授权服务器需要支持Web、移动端登录以及微服务授权。目前授权服务器本身工作正常,直接调用令牌接口能正确返回access_token和refresh_token。

现在我遇到的问题是:无法在Spring MVC应用的WebSecurityConfigurerAdapter配置中,保留本地登录表单的同时,通过密码授权模式(Password Credentials Grant)向远程OAuth2服务器发起令牌请求。我希望loginProcessingUrl()不要指向默认的j_spring_security_check,而是触发向授权服务器的请求,尝试用addFilterBefore()配置但没成功。

以下是两端的配置代码:

1. 远程OAuth2授权服务器配置(AuthorizationServerConfigurerAdapter)

package com.mydomain.oauth2.configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;

@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("userDetailsService")
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenStore tokenStore;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure( AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                .inMemory()
                .withClient("acme")
                .secret("acmesecret")
                .accessTokenValiditySeconds(3600)
                .scopes("read", "write")
                .redirectUris("http://localhost:8080/login")
                .authorizedGrantTypes("password", "refresh_token")
                .autoApprove(true)
                .resourceIds("resource");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer configurer) throws Exception {
        configurer.authenticationManager(authenticationManager);
        configurer.userDetailsService(userDetailsService);
        configurer.tokenStore(tokenStore);
    }
}

2. Spring MVC应用的WebSecurity配置(WebSecurityConfigurerAdapter)

import java.util.Arrays;
import javax.servlet.Filter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import com.humanbizz.web.security.UserInfoTokenServices;

@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private OAuth2ClientContextFilter oauth2ClientContextFilter;

    @Autowired
    OAuth2ClientContext oauth2ClientContext;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/login*", "/forgot-password*", "/signup**", "/signup/**", "/resources/**", "/403", "/storage/api/download/**", "/test/public/**").permitAll()
                .antMatchers("/user**").anonymous()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling().accessDeniedPage("/403")
                .and()
                .exceptionHandling()
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("?")
                .usernameParameter("username")
                .passwordParameter("password")
                .defaultSuccessUrl("/")
                .failureUrl("/login?error=true")
                .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .and()
                .logout().logoutSuccessUrl("/login").deleteCookies("JSESSIONID")
                .and()
                .addFilterBefore( myFilter(), BasicAuthenticationFilter.class);
    }

    private Filter myFilter() throws Exception{
        OAuth2ClientAuthenticationProcessingFilter myFilter = new OAuth2ClientAuthenticationProcessingFilter("/login");
        OAuth2RestTemplate myTemplate = new OAuth2RestTemplate(template(), oauth2ClientContext);
        teodeskFilter.setRestTemplate(myTemplate);
        UserInfoTokenServices tokenServices = new UserInfoTokenServices("http://localhost:9000/user", "acme");
        tokenServices.setRestTemplate(myTemplate);
        teodeskFilter.setTokenServices(tokenServices);
        return myFilter;
    }

    @Bean
    public OAuth2ProtectedResourceDetails template() {
        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setClientId("acme");
        details.setClientSecret("acmesecret");
        details.setAccessTokenUri("http://localhost:9000/oauth/token");
        details.setUserAuthorizationUri("http://localhost:9000/oauth/authorize");
        details.setGrantType("password");
        details.setScope(Arrays.asList("read"));
        return details;
    }
}

我需要知道如何正确配置过滤器,让本地登录表单通过密码授权模式向远程授权服务器获取令牌。我找到的案例大多是Spring Boot或者授权服务器与资源服务器同应用的场景,不太适配我的情况。


解决方案

我来帮你梳理下问题并给出修正方案,你的核心问题出在OAuth2资源配置类选型错误和过滤器逻辑不匹配密码授权流程上:

1. 替换资源配置类为密码授权专用实现

你当前用的AuthorizationCodeResourceDetails是为授权码模式设计的,完全不适合密码授权场景。我们需要换成ResourceOwnerPasswordResourceDetails,它会自动处理用户名密码的传递逻辑:

@Bean
public OAuth2ProtectedResourceDetails oauth2ResourceDetails() {
    ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
    details.setClientId("acme");
    details.setClientSecret("acmesecret");
    details.setAccessTokenUri("http://localhost:9000/oauth/token");
    details.setGrantType("password");
    details.setScope(Arrays.asList("read", "write")); // 和授权服务器配置的scope保持一致
    return details;
}

2. 自定义登录过滤器,适配密码授权流程

OAuth2ClientAuthenticationProcessingFilter是用来处理OAuth2第三方登录回调的,不能直接用来处理本地表单登录。我们需要自定义一个过滤器,拦截表单提交请求,调用远程授权服务器获取令牌,再转换为Spring Security的认证对象:

private Filter passwordGrantLoginFilter() throws Exception {
    // 拦截登录表单的提交路径,这里设为"/login-process"
    UsernamePasswordAuthenticationFilter filter = new UsernamePasswordAuthenticationFilter() {
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            // 从表单获取用户名和密码
            String username = obtainUsername(request);
            String password = obtainPassword(request);

            // 初始化OAuth2RestTemplate,用来向授权服务器发起请求
            OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(oauth2ResourceDetails(), oauth2ClientContext);
            // 将用户名密码存入请求参数
            restTemplate.getOAuth2ClientContext().getAccessTokenRequest().set("username", username);
            restTemplate.getOAuth2ClientContext().getAccessTokenRequest().set("password", password);

            try {
                // 请求授权服务器获取令牌
                OAuth2AccessToken token = restTemplate.getAccessToken();
                
                // 通过用户信息接口获取用户详情(如果授权服务器提供该接口)
                UserInfoTokenServices tokenServices = new UserInfoTokenServices("http://localhost:9000/user", "acme");
                tokenServices.setRestTemplate(restTemplate);
                Authentication auth = tokenServices.loadAuthentication(token.getValue());
                
                // 将令牌存入上下文,方便后续接口调用使用
                oauth2ClientContext.setAccessToken(token);
                return auth;
            } catch (OAuth2Exception e) {
                // 令牌请求失败,抛出认证异常
                throw new BadCredentialsException("Invalid username or password", e);
            }
        }
    };

    // 设置认证管理器(这里复用默认实现,因为核心认证逻辑在远程服务器)
    filter.setAuthenticationManager(authenticationManagerBean());
    
    // 配置登录成功处理器,跳转到首页
    filter.setAuthenticationSuccessHandler(new SavedRequestAwareAuthenticationSuccessHandler() {
        {
            setDefaultTargetUrl("/");
        }
    });
    
    // 配置登录失败处理器,跳回登录页并携带错误标记
    filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error=true"));
    
    // 设置表单参数名称,和你的登录表单保持一致
    filter.setUsernameParameter("username");
    filter.setPasswordParameter("password");
    
    // 设置过滤器拦截的路径,也就是表单提交的目标路径
    filter.setFilterProcessesUrl("/login-process");

    return filter;
}

3. 调整WebSecurity配置,替换过滤器并更新登录路径

修改configure(HttpSecurity http)方法,将loginProcessingUrl指向我们自定义过滤器的拦截路径,并替换原来的过滤器:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .antMatchers("/login*", "/forgot-password*", "/signup**", "/signup/**", "/resources/**", "/403", "/storage/api/download/**", "/test/public/**").permitAll()
            .antMatchers("/user**").anonymous()
            .anyRequest().authenticated()
            .and()
            .exceptionHandling().accessDeniedPage("/403")
            .and()
            .formLogin()
            .loginPage("/login")
            .loginProcessingUrl("/login-process") // 和过滤器的setFilterProcessesUrl保持一致
            .usernameParameter("username")
            .passwordParameter("password")
            .defaultSuccessUrl("/")
            .failureUrl("/login?error=true")
            .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            .logout().logoutSuccessUrl("/login").deleteCookies("JSESSIONID")
            .and()
            .addFilterBefore(passwordGrantLoginFilter(), UsernamePasswordAuthenticationFilter.class); // 替换原有过滤器
}

4. 额外注意事项

  • 确保你的登录表单的action属性设置为/login-process,和配置的loginProcessingUrl一致
  • 如果授权服务器没有提供/user用户信息接口,你可以调用授权服务器的/oauth/check_token接口来验证令牌并获取用户信息,需要在授权服务器配置中开放该接口的访问权限
  • 记得在Spring MVC应用中引入spring-security-oauth2依赖,确保所有OAuth2相关类都能正常加载

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 08:05:12