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

如何配置Spring Boot前后端+Keycloak实现OAuth2认证授权

OAuth2 + Keycloak 前后端服务配置问题解答

问题梳理

提问者为OAuth2初学者,计划搭建如下架构:

  • Spring后端作为资源服务器:基于JWT实现API安全防护,根据令牌携带的用户角色返回对应数据库资源
  • 基于Thymeleaf的Spring Boot前端:通过授权码流程完成登录,获取access_token后携带令牌调用后端API接口

已完成的配置如下:

  • Keycloak中创建2个客户端,分别对应后端、前端服务
  • 后端通过spring.security.oauth2.resourceserver配置JWT校验,测试可正常识别Keycloak配置的角色并解析为系统权限,配置代码:
security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: baseurl/auth/realms/realmname
          jwk-set-uri: baseurl/auth/realms/realmname/protocol/openid-connect/certs
  • 前端配置OAuth2登录参数,可正常跳转Keycloak登录页完成认证,配置代码:
security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: id
            client-secret: secret
            scope: openid, profile, roles
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8082/login/oauth2/code/service-name
        provider:
          keycloak:
            issuer-uri: baseurl/auth/realms/realmname

遇到的核心问题:
完成登录后无法获取access_token调用后端API,前端SecurityContextHolder中仅能查询到ID Token,找不到access_token。


问题根因

  1. 概念认知偏差:ID Token的作用是完成本地用户身份认证,会存入SecurityContext的认证主体;但access_token是用于调用第三方资源接口的凭证,默认由Spring Security OAuth2 Client组件单独管理,不会直接暴露在SecurityContext中。
  2. 配置缺失:仅配置了oauth2Login登录流程,没有开启oauth2Client客户端能力,框架不会自动管理access_token的存储、生命周期维护。

正确配置步骤

1. Keycloak客户端配置校验

  • 前端客户端:访问类型设置为confidential,Valid Redirect URIs准确匹配http://localhost:8082/login/oauth2/code/*,Web Origins添加前端地址允许跨域;如果使用Spring Boot 2.7以下版本,暂时关闭PKCE配置避免校验失败。
  • 后端资源服务器客户端:访问类型设置为bearer-only,不需要配置重定向地址,确保两个客户端属于同一个realm,角色统一配置在realm角色或对应客户端下,保证生成JWT时会将角色写入claims。

2. 前端Thymeleaf服务配置修正

首先补充Security过滤链配置,开启OAuth2客户端支持:

@Configuration
@EnableWebSecurity
public class FrontendSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .oauth2Login(oauth2 -> oauth2.defaultSuccessUrl("/home", true))
            .oauth2Client(); // 核心配置:开启OAuth2客户端能力,自动管理access_token生命周期
        return http.build();
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

调用后端接口时,通过OAuth2AuthorizedClientService或注解注入方式获取access_token,不要直接从SecurityContext读取:

@Service
public class BackendApiService {
    private final RestTemplate restTemplate;
    private final OAuth2AuthorizedClientService authorizedClientService;

    public BackendApiService(RestTemplate restTemplate, OAuth2AuthorizedClientService authorizedClientService) {
        this.restTemplate = restTemplate;
        this.authorizedClientService = authorizedClientService;
    }

    public Object getResource(Authentication authentication) {
        // 加载当前用户对应Keycloak客户端的授权信息
        OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(
                "keycloak",
                authentication.getName()
        );
        String accessToken = authorizedClient.getAccessToken().getTokenValue();

        // 携带令牌请求后端接口
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        HttpEntity<Void> request = new HttpEntity<>(headers);
        return restTemplate.exchange(
                "http://localhost:8081/api/resource",
                HttpMethod.GET,
                request,
                Object.class
        ).getBody();
    }
}

如果需要在Controller层快速获取令牌,可直接使用@RegisteredOAuth2AuthorizedClient注解注入:

@GetMapping("/home")
public String home(@RegisteredOAuth2AuthorizedClient("keycloak") OAuth2AuthorizedClient client, Model model) {
    model.addAttribute("accessToken", client.getAccessToken().getTokenValue());
    return "home";
}

3. 后端资源服务器配置补充

当前的JWT校验配置已经可用,只需要补充权限转换器,将Keycloak写入JWT的角色映射为Spring Security可识别的权限:

@Configuration
@EnableWebSecurity
public class ResourceServerSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/user/**").hasRole("USER")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
            );
        return http.build();
    }

    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt -> {
            Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
            List<String> roles = realmAccess != null ? (List<String>) realmAccess.get("roles") : Collections.emptyList();
            return roles.stream()
                    .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                    .toList();
        });
        return converter;
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 21:27:22