升级Spring Security OAuth2授权服务器至1.0.0遇无效Scope错误及登录跳转问题
升级Spring Security OAuth2授权服务器1.0.0后遇到无效Scope及不跳转登录页问题解决
问题描述
将Spring Security OAuth2授权服务器从0.3.1版本升级到1.0.0版本后,出现两个问题:
- 请求授权时返回
invalid_scope错误 - 访问授权端点时不再跳转到登录页面
错误信息
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
相关产品推荐
相关产品推荐

