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

升级Spring Security OAuth2授权服务器至1.0.0遇无效Scope错误及登录跳转问题

升级Spring Security OAuth2授权服务器1.0.0后遇到无效Scope及不跳转登录页问题解决

问题描述

将Spring Security OAuth2授权服务器从0.3.1版本升级到1.0.0版本后,出现两个问题:

  1. 请求授权时返回invalid_scope错误
  2. 访问授权端点时不再跳转到登录页面

错误信息

https://spring.io/?error=invalid_scope&error_description=OAuth%202.0%20Parameter:%20scope&error_uri=https://datatracker.ietf.org/doc/html/rfc6749%23section-4.1.2.1

相关配置及代码

OAuth2配置类代码

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain securityAuthFilterChain(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    return http.formLogin().and().build();
}

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

@Bean
public JWKSource<SecurityContext> jwkSource(){
    RSAKey rsaKey = JwksKeys.generateRSAKey();
    JWKSet set = new JWKSet(rsaKey);
    return (j, sc) -> j.select(set);
}

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
    return context -> {
        if (context.getTokenType() == OAuth2TokenType.ACCESS_TOKEN) {
            Authentication principal = context.getPrincipal();
            Set<String> authorities = principal.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.toSet());
            for (String s:authorities)
                System.out.println(s);
            context.getClaims().claim("roles", authorities);
        }
    };
}

WebSecurity配置类代码

private final CorsCustomizer corsCustomizer;
private final UserService userService;
private final ClientService clientService;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer();
    http.apply(configurer);

    configurer.registeredClientRepository(clientService);
    corsCustomizer.customize(http);

    return http.formLogin()
            .and()
            .authorizeHttpRequests()
            .requestMatchers("/users/**").permitAll()
            .requestMatchers("/clients/**").permitAll()
            .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .csrf().ignoringRequestMatchers("/users/**", "/clients/**")
            .and().build();
}

@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
    var provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(userService);
    provider.setPasswordEncoder(passwordEncoder());
    return new ProviderManager(provider);
}

@Bean
public PasswordEncoder passwordEncoder(){
    return NoOpPasswordEncoder.getInstance();
}

请求示例

http://localhost:8080/oauth2/authorize?response_type=code&client_id=client&scope=openid&redirect_uri=https://spring.io/auth

POM依赖

<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.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
        <version>3.0.0</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
        <version>1.18.22</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-authorization-server</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-ui</artifactId>
        <version>1.6.4</version>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.2.25</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.4.2.Final</version>
    </dependency>
</dependencies>

客户端服务代码

private final ClientRepository clientRepository;

@Override
public void save(RegisteredClient registeredClient) {

}

@Override
public RegisteredClient findById(String id) {
    Client client = clientRepository.findById(id).orElseThrow();
    return toRegisteredClient(client);
}

@Override
public RegisteredClient findByClientId(String clientId) {
    return findById(clientId);
}

public ClientDto createClient(CreateClientRequest request) {
    var client = new Client(request);
    var scopes = request.getScopes().stream().map(ClientScope::new).collect(Collectors.toSet());
    client.setScopes(scopes);
    client.setClientRedirectUrls(request.getRedirectUris().stream()
            .map(url -> new ClientRedirectUrl(url, client))
            .collect(Collectors.toSet()));
    clientRepository.save(client);
    return new ClientDto(client);
}

public RegisteredClient toRegisteredClient(Client client) {
    RegisteredClient.Builder builder = RegisteredClient.withId(client.getId())
            .clientId(client.getId())
            .clientSecret(client.getSecret())
            .clientAuthenticationMethod(client.getAuthenticationMethod())
            .authorizationGrantTypes(
                    authorizationGrantTypes -> authorizationGrantTypes.addAll(client.getGrantTypes()))
            .redirectUris(
                    redirectUris -> redirectUris.addAll(client.getClientRedirectUrls()
                            .stream()
                            .map(ClientRedirectUrl::getUrl)
                            .collect(Collectors.toSet())))
            .scopes(scopes -> scopes.addAll(client.getScopes()
                    .stream()
                    .map(ClientScope::getScope)
                    .collect(Collectors.toSet())));
    return builder.build();
}

解决方案

1. 修复无效Scope问题

  • 检查数据库中对应client的scopes列表,确认是否包含openid。如果没有,添加该scope到客户端的允许范围内。
  • 验证toRegisteredClient方法是否正确将数据库中的client scopes映射到RegisteredClient对象。1.0.0版本对scope的校验更严格,必须保证请求的所有scope都在客户端配置的允许列表中。

2. 修复不跳转登录页问题

  • 你的配置中存在两个SecurityFilterChain,其中securityFilterChain设置了requestMatchers("/**").permitAll(),这会直接放行包括/oauth2/authorize在内的所有请求,导致无需认证就可以访问授权端点,因此不会跳转登录页。
  • 修改securityFilterChain中的授权规则,移除"/**"的permitAll,仅放行需要公开访问的路径:
.authorizeHttpRequests()
.requestMatchers("/users/**").permitAll()
.requestMatchers("/clients/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
  • 确保securityAuthFilterChain的@Order(Ordered.HIGHEST_PRECEDENCE)生效,让它优先处理授权服务器的安全规则,避免被另一个过滤器链覆盖。

3. 其他注意事项

  • 1.0.0版本要求RegisteredClient的clientSecret必须是加密后的字符串,当前使用的NoOpPasswordEncoder仅适合测试环境,生产环境建议替换为BCrypt等强加密方式。如果数据库中存储的是明文密码,需要在toRegisteredClient方法中对密码进行编码:
.clientSecret(passwordEncoder().encode(client.getSecret()))
  • 检查客户端的authorizationGrantTypes是否包含authorization_code,因为请求使用的是response_type=code,必须确保客户端配置了对应的授权类型。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 04:55:13