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

使用Spring Authorization Server 1.0.0认证客户端遇登录重定向循环求助

Spring Boot客户端通过Spring Authorization Server 1.0.0认证时循环重定向问题

问题描述

我正在构建Spring Boot客户端应用,拟通过Spring Authorization Server 1.0.0实现认证,但无法正常登录,始终重定向至授权服务器登录页面。


授权服务器配置(pom.xml)

port: 9000

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.0</version>
</parent>
<properties>
    <java.version>17</java.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mariadb.jdbc</groupId>
        <artifactId>mariadb-java-client</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
        <version>2.7.5</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-authorization-server</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>5.0.1</version>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>31.1-jre</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.12.0</version>
    </dependency>
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>20041127.091804</version>
    </dependency>
    <dependency>
        <groupId>commons-beanutils</groupId>
        <artifactId>commons-beanutils</artifactId>
        <version>20030211.134440</version>
    </dependency>
</dependencies>  

授权服务器核心配置类

@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
    private final PasswordEncoder passwordEncoder;

    public AuthorizationServerConfig(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());   // Enable OpenID Connect 1.0

        // @formatter:off
        http
                .exceptionHandling(exceptions ->
                        exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
                )
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); // Accept access tokens for User Info and/or Client Registration
        // @formatter:on
        return http.build();
    }

    // @formatter:off
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {

        // Save registered client in db as if in-memory
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        RegisteredClient existingClient = registeredClientRepository.findByClientId("front-client");
        if(existingClient == null){
            RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                    .clientId("front-client")
                    .clientSecret(passwordEncoder.encode("123456"))
                    .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                    .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                    .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                    //.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                    .redirectUri("http://127.0.0.1:8080/login/oauth2/code/front-client-oidc")
                    .redirectUri("http://127.0.0.1:8080/authorized")
                    .scope(OidcScopes.OPENID)
                    .scope("read")                    
                    .tokenSettings(TokenSettings.builder()
                            .accessTokenTimeToLive(Duration.ofDays(30))
                            .refreshTokenTimeToLive(Duration.ofDays(365))
                            .build())
                    //.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                    .build();
            registeredClientRepository.save(registeredClient);
        }
        return registeredClientRepository;
    }
    // @formatter:on

    
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = Jwks.generateRsa();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }
}

授权服务器安全配置类

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class DefaultSecurityConfig {

    private final CustomAuthenticationProvider customAuthenticationProvider;

    public DefaultSecurityConfig(CustomAuthenticationProvider customAuthenticationProvider) {
        this.customAuthenticationProvider = customAuthenticationProvider;
    }

    // @formatter:off
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http    .csrf().disable()
                .authorizeHttpRequests(authorize ->
                        authorize.anyRequest().authenticated()
                )
                .formLogin(withDefaults()); //for default login page                
        return http.build();
    }
    // @formatter:on

    @Autowired
    public void bindAuthenticationProvider(AuthenticationManagerBuilder authenticationManagerBuilder){
        authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider);
    }
}

客户端应用配置(pom.xml)

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>  
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity6</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

客户端应用属性配置(application.yaml)

server:
  port: 8080
spring:
  security:
    oauth2:
      client:
        registration:
          front-client-oidc:
            provider: spring
            client-id: front-client
            client-secret: 123456
            authorization-grant-type: authorization_code
            redirect-uri: "http://127.0.0.1:8080/login/oauth2/code/front-client-oidc"
            scope: openid
            client-name: front-client-oidc
          front-client-authorization-code:
            provider: spring
            client-id: front-client
            client-secret: 123456
            authorization-grant-type: authorization_code
            redirect-uri: "http://127.0.0.1:8080/authorized"
            scope: read
            client-name:  front-client-authorization-code
          front-client-client-credentials:
            provider: spring
            client-id: front-client
            client-secret: 123456
            authorization-grant-type: client_credentials
            scope: read,write
            client-name: front-client-client-credentials
        provider:
          spring:
            issuer-uri: http://127.0.0.1:9000

客户端应用安全配置与控制器

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {

    // @formatter:off
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeHttpRequests(authorize ->
                authorize.anyRequest().authenticated()
            )
            .oauth2Login(oauth2Login ->
                oauth2Login.loginPage("/oauth2/authorization/front-client-oidc"))
            .oauth2Client(withDefaults());
        return http.build();
    }
    // @formatter:on
}

@Controller
public class DefaultController {
    @GetMapping("/")
    public String root() {
        return "redirect:/index";
    }

    @GetMapping("/index")
    public String index() {
        return "index";
    }
}

浏览器重定向日志

Request URL: http://localhost:8080

redirect to: http://127.0.0.1:9000/login

redirect to: http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=front-client&scope=openid&state=SKhb4bxYHH36G9mRVb4Dx4b4MCzLk8J85FiSjnwQjTw=&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/front-client-oidc&nonce=nmGlkPVUvvUFEE3_WhaiKonKcGqQQWUMbP__ABznb6A&continue

redirect to: http://127.0.0.1:8080/login/oauth2/code/front-client-oidc?code=yBikXvgTh3vVDztjLsHeFFzkN7xeskyo9dERmjG002d9Jnjwrw21PpzjzuttTZ4cfp5E4vj9FfqG23jObRFoiiPL3G1IHEIzXa2wf-l8f3iNEGBZ2GTXrSv_6ZxV3biJ&state=SKhb4bxYHH36G9mRVb4Dx4b4MCzLk8J85FiSjnwQjTw%3D

redirect to: http://127.0.0.1:8080/oauth2/authorization/front-client-oidc?error

again redirect to: http://127.0.0.1:9000/login

问题排查与解决方案

1. 自定义认证提供者有效性检查

授权服务器中使用了CustomAuthenticationProvider但未提供实现代码,若该类未正确返回已认证的Authentication对象,会导致登录后认证状态无效,触发循环重定向:

  • 检查CustomAuthenticationProvider的authenticate方法,确保返回的对象包含有效用户信息,且isAuthenticated()返回true;
  • 若无需自定义认证,可暂时注释掉相关配置,改用Spring Security默认用户认证机制测试。

2. 移除客户端登录页冗余配置

客户端安全配置中手动指定loginPage("/oauth2/authorization/front-client-oidc")会覆盖默认OAuth2跳转逻辑,导致授权码回调后无法完成认证:

// 修改后的客户端安全配置
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf().disable()
        .authorizeHttpRequests(authorize ->
            authorize.anyRequest().authenticated()
        )
        .oauth2Login(withDefaults())
        .oauth2Client(withDefaults());
    return http.build();
}

3. 调整授权服务器端点权限

授权服务器默认安全链设置anyRequest().authenticated(),会限制未登录用户访问/login等必要端点,需开放公共访问:

// 修改后的授权服务器默认安全链
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.csrf().disable()
        .authorizeHttpRequests(authorize ->
            authorize.requestMatchers("/login", "/error").permitAll()
                    .anyRequest().authenticated()
        )
        .formLogin(withDefaults());
    return http.build();
}

4. 验证JWT配置有效性

访问http://127.0.0.1:9000/.well-known/openid-configuration确认授权服务器的JWKS端点可正常访问,确保客户端能获取到有效的JWT验证密钥。

5. 确认重定向URI一致性

再次核对授权服务器注册的客户端重定向URI与客户端配置的redirect-uri,确保IP、端口、路径完全一致(无大小写或空格差异)。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 01:35:18