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

Spring Boot 3+OAuth2+Keycloak 无效会话ID及401授权问题求助

Spring Boot 3.1.5 + Keycloak 22.0.5 集成Basic Authentication遇401及无效会话ID问题

我基于Spring Boot 3.1.5、OAuth2集成Keycloak 22.0.5,期望使用Basic Authentication。已在Keycloak端创建Realm、客户端、用户及对应接口权限角色,但请求接口时持续报错:

DEBUG o.s.s.w.s.SessionManagementFilter.doFilter - Request requested invalid session id SESSION_ID_TOKEN

每次请求的SESSION_ID_TOKEN均相同,且始终返回401 Unauthorized。

当前配置

application.yml

origins: http://localhost:9999
issuer: **KEYCLOAK-URL**

com:
  c4-soft:
    springaddons:
      oidc:
        ops:
          - iss: ${issuer}
            username-claim: preferred_username
            authorities:
              - path: $.realm_access.roles
                prefix: ROLE_
              - path: $.resource_access.*.roles
        resourceserver:
          cors:
            - path: /**
              allowed-origin-patterns: ${origins}
          permit-all:
            - "/example"

WebSecurityConfig

@Configuration
@EnableMethodSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable);
        http.authorizeHttpRequests(accessManagement -> accessManagement
                .anyRequest().authenticated());
        http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        http.exceptionHandling(eh -> eh.authenticationEntryPoint((request, response, authException) -> {
            response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Bearer realm=\"Restricted Content\"");
            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        }));
        return http.build();
    }
}

Keycloak Realm会话配置

会话设置截图1
会话设置截图2


问题排查与修复方案

1. 核心配置冲突:未启用Basic Authentication

当前SecurityFilterChain配置的认证入口返回Bearer类型的WWW-Authenticate头,完全没启用Basic Auth支持,这和需求直接冲突。需修改配置添加Basic Auth支持,并整合Keycloak用户校验逻辑:

@Configuration
@EnableMethodSecurity
public class WebSecurityConfig {

    @Value("${issuer}")
    private String issuerUri;

    @Value("${keycloak.client-id}")
    private String clientId;

    @Value("${keycloak.client-secret}")
    private String clientSecret;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable);
        http.authorizeHttpRequests(accessManagement -> accessManagement
                .requestMatchers("/example").permitAll()
                .anyRequest().authenticated());
        // 启用Basic Authentication,替换认证入口为Basic类型
        http.httpBasic(basic -> basic.authenticationEntryPoint((request, response, authException) -> {
            response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        }));
        http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }

    // 配置从Keycloak校验用户凭证的UserDetailsService
    @Bean
    public UserDetailsService keycloakUserDetailsService() {
        return username -> {
            // 从请求头解析密码,调用Keycloak接口校验用户名密码
            RestTemplate restTemplate = new RestTemplate();
            MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
            params.add("grant_type", "password");
            params.add("client_id", clientId);
            params.add("client_secret", clientSecret);
            params.add("username", username);
            // 实际代码中需从Basic Auth头解析密码,此处为示例占位
            params.add("password", "用户提交的密码");

            ResponseEntity<Map> response = restTemplate.postForEntity(
                    issuerUri + "/protocol/openid-connect/token",
                    params,
                    Map.class
            );

            if (response.getStatusCode().is2xxSuccessful()) {
                Map<String, Object> body = response.getBody();
                List<String> realmRoles = (List<String>) ((Map) body.get("realm_access")).get("roles");
                Collection<GrantedAuthority> authorities = realmRoles.stream()
                        .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                        .collect(Collectors.toList());
                return new User(username, "", authorities);
            }
            throw new UsernameNotFoundException("用户不存在或凭证错误");
        };
    }
}

2. 无效会话ID问题处理

报错提示无效会话ID,是因为请求携带了JSESSIONID Cookie,但服务配置了STATELESS(无状态),不会创建或识别会话。解决方式:

  • 在请求工具中禁用Cookie(如Postman关闭"Send cookies"选项)
  • 或在SecurityFilterChain中添加无效会话处理逻辑:
http.sessionManagement(sm -> sm
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .invalidSessionStrategy((request, response) -> {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), "服务为无状态,无效会话");
        }));

3. Keycloak客户端配置检查

  • 确保客户端Access Type设置为confidential(Basic Auth需要客户端密钥)
  • 开启Direct Access Grants Enabled(允许使用password grant类型验证用户凭证)
  • 确认客户端会话配置继承Realm设置(从截图看Realm会话超时30分钟,无异常)

4. 验证请求格式

请求时必须在Authorization头中传递Base64编码的用户名:密码,格式示例:
Authorization: Basic dXNlcjE6cGFzc3dvcmQ=
(dXNlcjE6cGFzc3dvcmQ=是user1:password的Base64编码)


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 15:19:57