如何通过Spring RestTemplate调用Keycloak保护的REST端点
用Spring RestTemplate调用受Keycloak保护的REST端点解决方案
嘿,结合你提到的JHipster创建的两个单体应用(A和B)、Keycloak认证授权的场景,我整理了两种最实用的方案,覆盖用户登录态下的调用和服务间无用户参与的调用两种情况:
场景1:前端用户已登录,从应用A后端调用应用B的受保护端点
这种情况很常见——用户在应用A的前端登录后,后端需要调用应用B的接口来获取数据。我们可以直接复用当前登录用户的JWT令牌,让RestTemplate自动携带到请求头里。
步骤1:从SecurityContext提取当前用户的JWT令牌
因为你的应用已经集成了Spring Security + Keycloak,登录用户的认证信息会存在SecurityContextHolder中。写个简单的工具方法就能拿到令牌:
import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; // 可以把这个方法放到工具类或者你的服务类里 private String getCurrentUserJwtToken() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication instanceof JwtAuthenticationToken jwtAuthToken) { Jwt jwt = jwtAuthToken.getToken(); return jwt.getTokenValue(); } throw new IllegalStateException("当前用户未通过JWT认证,请先登录"); }
步骤2:给RestTemplate加拦截器自动带Authorization头
创建一个RestTemplate的配置类,添加拦截器,这样每次请求都会自动把令牌放到请求头里:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.web.client.RestTemplate; @Configuration public class UserContextRestTemplateConfig { @Bean public RestTemplate userContextRestTemplate() { RestTemplate restTemplate = new RestTemplate(); ClientHttpRequestInterceptor authInterceptor = (request, body, execution) -> { String jwtToken = getCurrentUserJwtToken(); // 调用上面的工具方法 request.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + jwtToken); return execution.execute(request, body); }; restTemplate.getInterceptors().add(authInterceptor); return restTemplate; } }
步骤3:直接调用应用B的端点
在你的业务服务类里注入这个配置好的RestTemplate,直接用就行:
import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class AppABusinessService { private final RestTemplate userContextRestTemplate; // 构造注入RestTemplate public AppABusinessService(RestTemplate userContextRestTemplate) { this.userContextRestTemplate = userContextRestTemplate; } public String fetchDataFromAppB() { // 替换成应用B的实际接口地址 String appBProtectedUrl = "http://app-b-domain/api/your-protected-endpoint"; return userContextRestTemplate.getForObject(appBProtectedUrl, String.class); } }
场景2:应用A后端独立调用应用B(无前端用户,用客户端凭证模式)
如果是应用之间的后台服务调用(比如定时任务、数据同步),不需要用户参与,这时候可以用Keycloak的客户端凭证模式来获取令牌。
步骤1:先在Keycloak里配置好客户端权限
- 找到应用A对应的Keycloak客户端,开启
Service Accounts Enabled选项; - 给这个客户端分配应用B的API所需的角色/权限(确保应用B的资源服务器能识别这些权限)。
步骤2:写个工具类获取客户端凭证令牌
通过Keycloak的令牌端点,用客户端ID和密钥换取JWT:
import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; public class KeycloakClientTokenProvider { private final String keycloakServerUrl; private final String realm; private final String clientId; private final String clientSecret; public KeycloakClientTokenProvider(String keycloakServerUrl, String realm, String clientId, String clientSecret) { this.keycloakServerUrl = keycloakServerUrl; this.realm = realm; this.clientId = clientId; this.clientSecret = clientSecret; } public String getClientCredentialsToken() { String tokenEndpoint = keycloakServerUrl + "/realms/" + realm + "/protocol/openid-connect/token"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>(); requestBody.add("grant_type", "client_credentials"); requestBody.add("client_id", clientId); requestBody.add("client_secret", clientSecret); HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(requestBody, headers); RestTemplate restTemplate = new RestTemplate(); // 用简单的POJO映射令牌响应 TokenResponse tokenResponse = restTemplate.postForObject(tokenEndpoint, request, TokenResponse.class); assert tokenResponse != null; return tokenResponse.getAccessToken(); } // 内部类,映射Keycloak返回的令牌结构 private static class TokenResponse { private String access_token; public String getAccessToken() { return access_token; } public void setAccessToken(String access_token) { this.access_token = access_token; } } }
步骤3:配置服务间调用的RestTemplate
在配置类里创建专用的RestTemplate,自动获取并携带客户端令牌:
import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.web.client.RestTemplate; @Configuration public class ServiceToServiceRestTemplateConfig { @Value("${keycloak.auth-server-url}") private String keycloakServerUrl; @Value("${keycloak.realm}") private String realm; @Value("${keycloak.resource}") // 应用A的客户端ID private String clientId; @Value("${keycloak.credentials.secret}") private String clientSecret; @Bean public RestTemplate serviceToServiceRestTemplate() { RestTemplate restTemplate = new RestTemplate(); KeycloakClientTokenProvider tokenProvider = new KeycloakClientTokenProvider(keycloakServerUrl, realm, clientId, clientSecret); ClientHttpRequestInterceptor authInterceptor = (request, body, execution) -> { String clientToken = tokenProvider.getClientCredentialsToken(); request.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + clientToken); return execution.execute(request, body); }; restTemplate.getInterceptors().add(authInterceptor); return restTemplate; } }
关键注意事项
- 令牌验证:确保应用B的
ResourceServerConfiguration能正确验证令牌——JHipster通常已经配置了从Keycloak的JWKS端点自动获取公钥,不需要额外操作,但如果有自定义配置,要确保公钥正确。 - 令牌缓存:如果是高频调用,建议给客户端令牌加缓存(比如用Guava Cache或者Spring Cache),避免每次请求都去Keycloak拿令牌,提升性能。
- CORS配置:如果是前端触发的跨域调用,要在应用B的CORS配置里允许应用A的域名访问。
内容的提问来源于stack exchange,提问作者Christoph J.
相关产品推荐
相关产品推荐

