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

Spring Boot微服务携带有效Access Token仍重定向至Login问题

问题描述

我有一个使用JHipster生成、集成Keycloak的Spring Boot微服务应用,版本信息如下:

  • JHipster - 7.9.3
  • Spring Boot - 3.0.2
  • Spring Cloud - 2022.0.1
  • Keycloak - 20.0.3

手动升级了JHipster生成的Spring Boot版本。

安全配置类代码:

@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Import(SecurityProblemSupport.class)
public class SecurityConfiguration {

    private final JHipsterProperties jHipsterProperties;

    @Value("${spring.security.oauth2.client.provider.oidc.issuer-uri}")
    private String issuerUri;

    private final SecurityProblemSupport problemSupport;

    public SecurityConfiguration(JHipsterProperties jHipsterProperties, SecurityProblemSupport problemSupport) {
        this.problemSupport = problemSupport;
        this.jHipsterProperties = jHipsterProperties;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // @formatter:off
        http
            .csrf()
            .disable()
            .exceptionHandling()
                .authenticationEntryPoint(problemSupport)
                .accessDeniedHandler(problemSupport)
        .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeHttpRequests()
            .requestMatchers("/api/authenticate").permitAll()
            .requestMatchers("/api/auth-info").permitAll()
            .requestMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .requestMatchers("/api/**").authenticated()
            .requestMatchers("/management/health").permitAll()
            .requestMatchers("/management/health/**").permitAll()
            .requestMatchers("/management/info").permitAll()
            .requestMatchers("/management/prometheus").permitAll()
            .requestMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
        .and()
            .oauth2ResourceServer()
                .jwt()
                .jwtAuthenticationConverter(authenticationConverter())
                .and()
            .and()
                .oauth2Client();
        return http.build();
        // @formatter:on
    }

    Converter<Jwt, AbstractAuthenticationToken> authenticationConverter() {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new JwtGrantedAuthorityConverter());
        return jwtAuthenticationConverter;
    }

    @Bean
    JwtDecoder jwtDecoder() {
        NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuerUri);

        OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(jHipsterProperties.getSecurity().getOauth2().getAudience());
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

        jwtDecoder.setJwtValidator(withAudience);

        return jwtDecoder;
    }
}

安全相关应用属性:

spring:
    security:
        oauth2:
          resource:
              filter-order: 3
          client:
            provider:
              oidc:
                issuer-uri: http://localhost:8080/realms/samplerealm
            registration:
              oidc:
                authorization-grant-type: client_credentials
                client-id: microservice-client
                client-secret: <VALID_CLIENT_SECRET>
                scope: openid, profile, email, offline_access # last one for refresh tokens

应用监听localhost:8087的HTTP请求。

在Keycloak中创建了dev-client客户端,使用Postman测试应用API:通过该客户端从Keycloak获取Access Token,将其以Bearer ----access token----形式放入Authorization请求头,但即使Token有效,API仍将请求重定向至localhost:8087/login并返回登录HTML页面:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Please sign in</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet"
        crossorigin="anonymous" />
</head>

<body>
    <div class="container">
        <h2 class="form-signin-heading">Login with OAuth 2.0</h2>
        <table class="table table-striped">
        </table>
    </div>
</body>

</html>

所有HTTP请求(如GET)均出现此问题,尝试使用password授权类型获取的Token测试,结果一致。

问题分析与解决

核心原因

出现重定向到/login的情况,说明Spring Security没有正确识别请求中的JWT Token,转而触发了OAuth2客户端的登录流程。通常是因为资源服务器的过滤器优先级异常,或Token验证环节不通过导致的。

解决方案步骤

1. 移除过时的filter-order配置

Spring Boot 3.x中,spring.security.oauth2.resource.filter-order已被废弃,该配置会干扰过滤器链的正常优先级。直接删除配置中的以下内容:

# 移除这部分配置
resource:
    filter-order: 3

2. 确保JWT Token受众(Audience)匹配

检查Keycloak生成的Token中的aud字段,是否包含应用配置中jhipster.security.oauth2.audience的值。如果未配置该属性,需在application.yml中添加:

jhipster:
    security:
        oauth2:
            audience: microservice-client

注意此处audience需与Keycloak客户端的Client ID(即microservice-client)一致,否则AudienceValidator会验证失败,导致Token不被认可。

3. 调整SecurityFilterChain配置

优化filterChain方法,确保资源服务器配置完整且无冗余:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // @formatter:off
    http
        .csrf(csrf -> csrf.disable())
        .exceptionHandling(exception -> exception
            .authenticationEntryPoint(problemSupport)
            .accessDeniedHandler(problemSupport)
        )
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/authenticate").permitAll()
            .requestMatchers("/api/auth-info").permitAll()
            .requestMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .requestMatchers("/api/**").authenticated()
            .requestMatchers("/management/health").permitAll()
            .requestMatchers("/management/health/**").permitAll()
            .requestMatchers("/management/info").permitAll()
            .requestMatchers("/management/prometheus").permitAll()
            .requestMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
        )
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt
                .jwtAuthenticationConverter(authenticationConverter())
            )
        )
        .oauth2Client(Customizer.withDefaults());
    return http.build();
    // @formatter:on
}

4. 修复角色权限映射

默认的JwtGrantedAuthorityConverter会从Token的scope字段提取权限,前缀为SCOPE_。若API使用AuthoritiesConstants.ADMIN(通常为ROLE_ADMIN),需自定义转换器映射Keycloak角色:

Converter<Jwt, AbstractAuthenticationToken> authenticationConverter() {
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> {
        Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
        Collection<String> roles = (Collection<String>) realmAccess.get("roles");
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
            .collect(Collectors.toList());
    });
    return jwtAuthenticationConverter;
}

5. 检查Keycloak客户端配置

确保Keycloak中的dev-client客户端:

  • 开启Service Accounts Enabled(使用client_credentials授权时)
  • 为用户/服务账号分配对应角色(如admin)
  • Token签名算法与应用默认配置一致(默认RS256)

验证步骤

  1. 使用JWT解析工具检查获取的Token,确认iss、aud、exp、realm_access.roles等字段是否正确。
  2. 重启应用,用Postman携带正确Token请求API,观察是否不再重定向到登录页面。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 01:13:59