Spring oauth2 授权服务器:无法注销用户

Ery*_*ing 2 spring spring-security oauth-2.0 spring-security-oauth2 spring-oauth2

我创建了一个用作 oauth2 客户端的 Angular 应用程序。我已经使用以下安全配置使用 spring oauth2 创建了授权服务器

@Bean
@Order(1)
public SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(corsFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf().disable()
                .headers().frameOptions().disable()
            .and()
                .antMatcher("/auth/account/**")
                .authorizeRequests()
                .anyRequest().authenticated()
            .and()
                .logout()
                .clearAuthentication(true)
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
            .and()
                .oauth2ResourceServer().jwt();
        return http.build();
    }

@Bean
@Order(2)
public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
        // @formatter:off
        http
                .addFilterBefore(corsFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf().disable()
                .headers().frameOptions().disable()
            .and()
                .authorizeRequests()
                .antMatchers("/management/**").permitAll()
                .antMatchers("/h2-console/**").permitAll()
                .anyRequest().authenticated()
            .and()
                .logout()
                .clearAuthentication(true)
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
            .and()
                .formLogin(withDefaults());
        return http.build();
    }
Run Code Online (Sandbox Code Playgroud)

这是我的授权服务器配置

@Configuration
public class AuthServerConfig {

    private final DataSource dataSource;
    private final AuthProperties authProps;
    private final PasswordEncoder encoder;

    public AuthServerConfig(DataSource dataSource, AuthProperties authProps, PasswordEncoder encoder) {
        this.dataSource = dataSource;
        this.authProps = authProps;
        this.encoder = encoder;
    }

    @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);
        RegisteredClient webClient = RegisteredClient.withId("98a9104c-wertyuiop")
                .clientId(authProps.getClientId())
                .clientName(authProps.getClientName())
                .clientSecret(encoder.encode(authProps.getClientSecret()))
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://127.0.0.1:4200/xxxx/yyy")
                .redirectUri("http://127.0.0.1:8000/xxxx/yyy")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .scope("farmer:read")
                .scope("farmer:write")
                .tokenSettings(tokenSettings())
                .build();

        clientRepository.save(webClient);
        return clientRepository;
    }

    @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();
    }

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

}

Run Code Online (Sandbox Code Playgroud)

这是我的 build.gradle 文件

plugins {
    id 'org.springframework.boot' version '2.6.2'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'org.liquibase.gradle' version '2.1.0'
    id 'java'
}

group = 'com.shamba.records'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
    maven { url 'https://repo.spring.io/release' }
}

ext {
    set('springCloudVersion', "2021.0.0")
    set('liquibaseVersion', "4.6.1")
}

configurations {
    liquibaseRuntime.extendsFrom runtimeClasspath
}

dependencies {

    implementation 'tech.jhipster:jhipster-framework:7.4.0'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.security:spring-security-oauth2-authorization-server:0.2.1'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    implementation 'org.springframework.security:spring-security-cas:5.6.1'

    // mapstruct
    implementation 'org.mapstruct:mapstruct:1.4.2.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'

    // jackson
    implementation 'com.fasterxml.jackson.module:jackson-module-jaxb-annotations'
    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hppc'
    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
    implementation 'org.zalando:problem-spring-web:0.26.0'

    // configure liquibase
    implementation "org.liquibase:liquibase-core:${liquibaseVersion}"
    liquibaseRuntime 'org.liquibase:liquibase-groovy-dsl:3.0.0'
    liquibaseRuntime 'info.picocli:picocli:4.6.1'
    liquibaseRuntime 'org.postgresql:postgresql'
    liquibaseRuntime group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
    liquibaseRuntime 'org.liquibase.ext:liquibase-hibernate5:3.6'
    liquibaseRuntime sourceSets.main.output

    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'org.postgresql:postgresql'


    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

test {
    useJUnitPlatform()
}
Run Code Online (Sandbox Code Playgroud)

这是属性的一部分,为了简洁我省略了其他内容

spring:
    security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://${AUTH_SERVICE_HOST:127.0.0.1}:5000
          jwk-set-uri: http://${AUTH_SERVICE_HOST:127.0.0.1}:5000/oauth2/jwks
Run Code Online (Sandbox Code Playgroud)

我可以使用授权代码流登录和注销用户,但问题出现在第一次成功登录后,当用户单击登录时,即使在调用端点后,身份验证服务器也会自动登录/oauth2/revoke用户并在身份验证服务器中指定下面的注销配置

.and()
                .logout()
                .clearAuthentication(true)
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
Run Code Online (Sandbox Code Playgroud)

我还尝试实现一个自定义端点/auth/account/revoke来手动注销用户,但似乎没有任何效果。这是实现

@RestController
@RequestMapping("auth/account")
public class AccountResource {

    @GetMapping("/revoke")
    public void revoke(HttpServletRequest request) {
        Assert.notNull(request, "HttpServletRequest required");
        HttpSession session = request.getSession(false);
        if (!Objects.isNull(session)) {
            session.removeAttribute("SPRING_SECURITY_CONTEXT");
            session.invalidate();
        }
        SecurityContextHolder.getContext().setAuthentication(null);
        SecurityContextHolder.clearContext();
    }
}
Run Code Online (Sandbox Code Playgroud)

可能是什么问题?任何帮助都很重要

- - - - -更新 - - - - - - -

升级spring-security-oauth2-authorization-server版本后0.2.2我更新了这个方法

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

对此

@Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer<>();
        authorizationServerConfigurer.tokenRevocationEndpoint(tokenRevocationEndpoint -> tokenRevocationEndpoint
                .revocationResponseHandler((request, response, authentication) -> {
                    Assert.notNull(request, "HttpServletRequest required");
                    HttpSession session = request.getSession(false);
                    if (!Objects.isNull(session)) {
                        session.removeAttribute("SPRING_SECURITY_CONTEXT");
                        session.invalidate();
                    }
                    SecurityContextHolder.getContext().setAuthentication(null);
                    SecurityContextHolder.clearContext();
                    response.setStatus(HttpStatus.OK.value());
                })
        );
        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

        http
                .requestMatcher(endpointsMatcher)
                .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
                .apply(authorizationServerConfigurer);

        return http.formLogin(Customizer.withDefaults()).build();
    }
Run Code Online (Sandbox Code Playgroud)

Ste*_*erg 7

有两个概念有些令人困惑。

  1. 安全注销
  2. 令牌撤销

关于注销应用程序,当使用基于浏览器的会话时这是必要的,这通常是流程的情况authorization_code。严格来说,只要终止会话即可实现您的目标。

关于令牌撤销,这更多的是与 OAuth 相关的安全问题,并且在这个意义上与传统的注销功能不同。通常,对令牌撤销的最直接需求是在refresh_token(或较小程度上相关的access_token)被盗时作为风险缓解策略。


澄清更新:值得一提的是,如果令牌没有被盗,那么此时通常没有理由撤销令牌。您可以想象从客户端的内存中丢弃令牌,并简单地让它在服务器上过期。在大多数情况下,即使用户注销,客户端实际上也应该保持授权(通过刷新令牌),因为这是首先获得授权的目的。

如果您觉得仅仅因为用户注销就需要撤销令牌,那么您可能正在尝试使用access_token和/或refresh_tokenAS 会话机制,但事实并非如此。


如果由于某种原因您仍然需要将注销与撤销联系起来,那么问题是:我们如何同时实现这两个目标,对吗?

不幸的是,尽管有一些规范可以用作功能开发的基础,但目前它还没有内置到 Spring Security 或 Spring Authorization Server 中(出于上述原因)。

一般来说,您需要在后端解决这个问题,因为前端无能为力。如果您只是从 UI 访问POST端点/logout,则无法在浏览器中操作为该主机存储的 cookie(使用 CORS 时)。我建议您的 OAuth 客户端使用后端换前端模式。但即使(尤其是)如果您不这样做,您也需要确保无论浏览器中是否存在 cookie,您都可以终止会话。这意味着将会话存储在数据库中,以某种方式将其与刷新令牌关联,并使用刷新令牌对吊销端点进行安全调用以同时从数据库中删除两者。

您可以通过设置 来实现此目的,如下AuthenticationSuccessHandler所示OAuth2TokenRevocationEndpointFilter

OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
        new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer.tokenRevocationEndpoint(tokenRevocationEndpoint -> tokenRevocationEndpoint
        .revocationResponseHandler((request, response, authentication) -> {
            /* delete session here... */
            response.setStatus(HttpStatus.OK.value());
        })
);
// ...
Run Code Online (Sandbox Code Playgroud)

希望这足以让您开始。您可能会发现自己研究如何实现这一目标的具体细节会带来一些好处(例如,这是一次很好的学习经历)。如果您遇到困难,这可能是请求操作指南的好机会,因为我们现在正在收集指南的想法。请参阅#499获取现有操作指南列表,请随时提交您自己的指南!