spring 安全 hasAuthority("SCOPE_xxx") 方法不适用于 spring 授权服务器版本 0.2.0

spring security hasAuthority("SCOPE_xxx") method not working with spring authorization server version 0.2.0

我已经使用新的 spring 授权服务器模块创建了一个授权服务器。我能够成功获取令牌,但是当我尝试将令牌用于 hasAuthority() 的受保护端点时,我收到禁止 403 错误。在我的 pom.xml 文件下面

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.erycoking</groupId>
    <artifactId>auth-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>auth-service</name>
    <description>Auth Service</description>
    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>2021.0.0</spring-cloud.version>
        <jhipster-dependencies.version>7.0.1</jhipster-dependencies.version>
        <liquibase.version>4.6.1</liquibase.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>tech.jhipster</groupId>
            <artifactId>jhipster-framework</artifactId>
            <version>7.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-authorization-server</artifactId>
            <version>0.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

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

        <!-- Jackson Configurations -->
        <dependency>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-module-jaxb-annotations</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-hibernate5</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-hppc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>
        <dependency>
            <groupId>org.zalando</groupId>
            <artifactId>problem-spring-web</artifactId>
            <version>0.26.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </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.liquibase</groupId>
            <artifactId>liquibase-core</artifactId>
            <!-- Inherited version from Spring Boot can't be used because of regressions -->
            <version>${liquibase.version}</version>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>1.4.2.Final</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.liquibase</groupId>
                <artifactId>liquibase-maven-plugin</artifactId>
                <version>${liquibase.version}</version>
            </plugin>
        </plugins>
    </build>

</project>

下面是我的授权服务器配置

@Configuration(proxyBeanMethods = false)
public class AuthServerConfig {

    private final DataSource dataSource;
    private final AuthProperties authProps;
    private final TokenSettings tokenSettings;

    public AuthServerConfig(DataSource dataSource, AuthProperties authProps, TokenSettings tokenSettings) {
        this.dataSource = dataSource;
        this.authProps = authProps;
        this.tokenSettings = tokenSettings;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource);
    }

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

    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        JdbcRegisteredClientRepository clientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        clientRepository.save(webClient());
        return clientRepository;
    }

    private RegisteredClient webClient() {
        return RegisteredClient.withId("98a9104c-a9c7-4d7c-ad03-ec61bcfeab36")
                .clientId(authProps.getClientId())
                .clientName(authProps.getClientName())
                .clientSecret(authProps.getClientSecret())
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://127.0.0.1:8080/authorized")
                .scope("create").scope("read").scope("write").scope("update").scope("delete")
                .tokenSettings(tokenSettings)
                .build();
    }

    @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 = generateRsa();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    private static RSAKey generateRsa() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        return new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
    }

    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder()
                .issuer(authProps.getIssuerUri())
                .build();
    }

}

这是我的安全配置

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {

    private final UserDetailsService userDetailsService;

    public SecurityConfig(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    .antMatchers("/management/**").permitAll()
                    .antMatchers("/h2-console/**").permitAll()
                    //this does not work
                    .antMatchers(HttpMethod.POST, "/auth/user").hasAuthority(AuthoritiesConstants.ADMIN)
                    //this does not work
                    .antMatchers(HttpMethod.GET, "/auth/user").hasAuthority("SCOPE_read")
                    .anyRequest().authenticated()
                .and()
                .csrf().disable()
                .headers().frameOptions().disable()
                .and()
                .formLogin(withDefaults())
                .userDetailsService(userDetailsService);
        return http.build();
    }

    @Bean
    public PasswordEncoder delegatingPasswordEncoder() {
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder("bcrypt", encoders);
        passwordEncoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());
        return passwordEncoder;
    }

    @Bean
    public TokenSettings tokenSettings() {
        return TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofMinutes(1))
                .refreshTokenTimeToLive(Duration.ofHours(24))
                .build();
    }
}

这是我的用户详情服务

@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        return userRepository.findOneWithRolesByEmailIgnoreCase(login)
                .map(user -> createSpringSecurityUser(login, user))
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
    }

    private org.springframework.security.core.userdetails.User createSpringSecurityUser(String lowercaseLogin, User user) {
        if (!user.isActivated()) {
            throw new UserNotActivatedException("User " + lowercaseLogin + " was not activated");
        }
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        user.getRoles().forEach(e -> {
            grantedAuthorities.add(new SimpleGrantedAuthority(e.getName()));
            e.getPermissions().forEach(p -> grantedAuthorities.add(new SimpleGrantedAuthority(p.getName())));
        });
        return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), grantedAuthorities);
    }
}

当使用令牌向需要单独身份验证的端点发出请求时,它会成功,但是当我尝试在需要角色的端点上使用它时,它会失败。

可能是什么问题?

根据评论中为阐明您的设置而进行的简短讨论,您似乎将授权服务器配置为常规安全应用程序,目的是将范围用作权限。但是,这是对授权服务器的错误使用。

注意:我在您的描述中没有看到任何对 OAuth 2.0 客户端或资源服务器的引用,因此我假设您正试图直接在授权服务器上访问端点。如果不是这样,请告诉我。

OAuth 2.0 保护设置涉及三个应用程序:

  1. 授权服务器
  2. 资源服务器
  3. 客户

您的配置仅适用于#1(据我所知)。授权服务器包含两个过滤器链和一个单独的 oauth 客户端的配置。两个过滤器链执行以下操作:

  1. 授权服务器框架提供的安全端点
  2. 在使用授权端点 (/oauth2/authorize) 获取授权码之前保护用户将与之交互的登录端点,客户端稍后将使用该授权码获取访问令牌

您配置的范围将允许用户(资源所有者)授予 oauth 客户端使用访问令牌对资源服务器进行受保护调用的能力。只有当客户端调用资源服务器时,才会使用您配置的范围。当用户使用浏览器直接与授权服务器上的端点交互时,表单登录的配置正在发挥作用,正如我在评论中提到的那样,它使用数据库中的角色。

查看 SpringOne 2021 repository and presentation 以了解如何将应用程序从不安全的应用程序转变为安全的应用程序,然后了解我们如何将其转变为使用范围作为权限的资源服务器。

该演示文稿演示了所有三个应用程序,但重点放在资源服务器上,这与您使用范围作为权限尝试完成的目标非常匹配。