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

