如何配置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。
问题根因
- 概念认知偏差:ID Token的作用是完成本地用户身份认证,会存入SecurityContext的认证主体;但access_token是用于调用第三方资源接口的凭证,默认由Spring Security OAuth2 Client组件单独管理,不会直接暴露在SecurityContext中。
- 配置缺失:仅配置了
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
相关产品推荐
相关产品推荐

