假客户端向 micro-service 发出 POST/PUT 请求时出现错误 403
Error 403 when feign client makes POST/PUT request to micro-service
我在 java 中有一个无法使用 spring 云的旧版应用程序。它使用假客户端通过网关访问微服务。
网关和服务由 jhipster 5.7.2 使用 OAuth2/OIDC 选项生成。
在我的客户端中,RequestInterceptor 调用 keycloak 以获得令牌(直接访问授权)并将其注入 header。
当我发出 GET 请求时没问题,但我在 POST 或 PUT 请求后收到 403。
网关上启用了 CORS(但未使用,因为该请求不是 cors 请求)。我 运行 它处于开发模式。
Zuul 路线似乎没问题。
我没有更改网关和服务上的配置。
有人知道吗?
下面是我的假客户:
public interface SmartDocumentClient {
@RequestLine("GET /api/ebox/test")
//@Headers("Content-Type: application/json")
public ResponseEntity<HasEboxResponse> test();
@RequestLine("POST /api/ebox/test")
@Headers("Content-Type: application/json")
public ResponseEntity<HasEboxResponse> testPost(HasEboxRequest request);
@RequestLine("PUT /api/ebox/test")
@Headers("Content-Type: application/json")
public ResponseEntity<HasEboxResponse> testPut(HasEboxRequest request); }
我的客户端配置:
T client = Feign.builder()
.contract(new feign.Contract.Default()) //annotation openfeign pour éviter bug d'upload avec SpringMvc
.client(new OkHttpClient())
.encoder(new FormEncoder(new GsonEncoder())) //pour gérer le formData
.decoder(new ResponseEntityDecoder(new ResponseEntityDecoder(new CustomFileDecoder(new CustomGsonDecoder()))))
.requestInterceptor(interceptor)
.options(new Request.Options(timeout, timeout))
.target(SmartDocumentClient, url);
拦截器:
public class GedRequestInterceptor implements RequestInterceptor {
public static final String AUTHORIZATION = "Authorization";
public static final String BEARER = "Bearer";
private String authUrl;
private String user;
private String password;
private String clientId;
private String clientSecret;
private RestTemplate restTemplate;
private CustomOAuth2ClientContext oAuth2ClientContext;
public GedRequestInterceptor(String authUrl, String user, String password, String clientId, String clientSecret) {
super();
this.authUrl = authUrl;
this.user = user;
this.password = password;
this.clientId = clientId;
this.clientSecret = clientSecret;
restTemplate = new RestTemplate();
//oAuth2ClientContext = new DefaultOAuth2ClientContext();
}
@Override
public void apply(RequestTemplate template) {
// demander un token à keycloak et le joindre à la request
Optional<String> token = getToken();
if (token.isPresent()) {
template.header(HttpHeaders.ORIGIN, "localhost");
template.header(AUTHORIZATION, String.format("%s %s", BEARER, token.get()));
}
}
private Optional<String> getToken() {
if (oAuth2ClientContext.getAccessToken() == null || oAuth2ClientContext.getAccessToken().isExpired()) {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", this.clientId);
map.add("client_secret", this.clientSecret);
map.add("grant_type", "password"); // client_credentials //password
map.add("username", this.user);
map.add("password", this.password);
oAuth2ClientContext.setAccessToken(askToken(map));
}
if (oAuth2ClientContext.getAccessToken() != null){
return Optional.ofNullable(oAuth2ClientContext.getAccessToken().getValue());
} else {
return Optional.empty();
}
}
private CustomOAuth2AccessToken askToken( MultiValueMap<String, String> map) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
ResponseEntity<CustomOAuth2AccessToken> response = restTemplate.postForEntity(
this.authUrl, request, CustomOAuth2AccessToken.class);
if (response != null && response.hasBody()) {
return response.getBody();
} else {
return null;
}
}
}
最后是资源:
@RestController
@RequestMapping("/api")
public class DocumentResource {
private static String TMP_FILE_PREFIX = "smartdoc_tmp";
public DocumentResource() {
}
@GetMapping("/ebox/test")
public ResponseEntity<HasEboxResponse> test() {
return ResponseEntity.ok(new HasEboxResponse());
}
@PostMapping("/ebox/test")
public ResponseEntity<HasEboxResponse> testPost(@RequestBody HasEboxRequest request) {
return ResponseEntity.ok(new HasEboxResponse());
}
@PutMapping("/ebox/test")
public ResponseEntity<HasEboxResponse> testPut(@RequestBody HasEboxRequest request) {
return ResponseEntity.ok(new HasEboxResponse());
}
}
谢谢!
问题出在 spring 安全配置中。 WebSecurity 不允许在未经身份验证的情况下调用“[SERVICE_NAME]/api”之类的 url。我添加了一条规则以允许访问某些网址。如果访问令牌在 header 中,它将由 zuul 转发给服务。
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/ext/*/api/**") // allow calls to services, redirect by zuul
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/index.html")
.antMatchers("/test/**");
}
为了通过UI调用其他服务并让网关注入访问令牌,我在我的zuul配置中定义了两组路由,
routes:
myservice:
path: /myservice/**
serviceId: myservice
myservice_ext:
path: /ext/myservice/**
serviceId: myservice
- /ext/myService... : 引用服务,不要被 spring secu
忽略
- /myService...:引用服务但由 spring secu
处理
我在 java 中有一个无法使用 spring 云的旧版应用程序。它使用假客户端通过网关访问微服务。
网关和服务由 jhipster 5.7.2 使用 OAuth2/OIDC 选项生成。
在我的客户端中,RequestInterceptor 调用 keycloak 以获得令牌(直接访问授权)并将其注入 header。
当我发出 GET 请求时没问题,但我在 POST 或 PUT 请求后收到 403。
网关上启用了 CORS(但未使用,因为该请求不是 cors 请求)。我 运行 它处于开发模式。 Zuul 路线似乎没问题。 我没有更改网关和服务上的配置。
有人知道吗?
下面是我的假客户:
public interface SmartDocumentClient {
@RequestLine("GET /api/ebox/test")
//@Headers("Content-Type: application/json")
public ResponseEntity<HasEboxResponse> test();
@RequestLine("POST /api/ebox/test")
@Headers("Content-Type: application/json")
public ResponseEntity<HasEboxResponse> testPost(HasEboxRequest request);
@RequestLine("PUT /api/ebox/test")
@Headers("Content-Type: application/json")
public ResponseEntity<HasEboxResponse> testPut(HasEboxRequest request); }
我的客户端配置:
T client = Feign.builder()
.contract(new feign.Contract.Default()) //annotation openfeign pour éviter bug d'upload avec SpringMvc
.client(new OkHttpClient())
.encoder(new FormEncoder(new GsonEncoder())) //pour gérer le formData
.decoder(new ResponseEntityDecoder(new ResponseEntityDecoder(new CustomFileDecoder(new CustomGsonDecoder()))))
.requestInterceptor(interceptor)
.options(new Request.Options(timeout, timeout))
.target(SmartDocumentClient, url);
拦截器:
public class GedRequestInterceptor implements RequestInterceptor {
public static final String AUTHORIZATION = "Authorization";
public static final String BEARER = "Bearer";
private String authUrl;
private String user;
private String password;
private String clientId;
private String clientSecret;
private RestTemplate restTemplate;
private CustomOAuth2ClientContext oAuth2ClientContext;
public GedRequestInterceptor(String authUrl, String user, String password, String clientId, String clientSecret) {
super();
this.authUrl = authUrl;
this.user = user;
this.password = password;
this.clientId = clientId;
this.clientSecret = clientSecret;
restTemplate = new RestTemplate();
//oAuth2ClientContext = new DefaultOAuth2ClientContext();
}
@Override
public void apply(RequestTemplate template) {
// demander un token à keycloak et le joindre à la request
Optional<String> token = getToken();
if (token.isPresent()) {
template.header(HttpHeaders.ORIGIN, "localhost");
template.header(AUTHORIZATION, String.format("%s %s", BEARER, token.get()));
}
}
private Optional<String> getToken() {
if (oAuth2ClientContext.getAccessToken() == null || oAuth2ClientContext.getAccessToken().isExpired()) {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", this.clientId);
map.add("client_secret", this.clientSecret);
map.add("grant_type", "password"); // client_credentials //password
map.add("username", this.user);
map.add("password", this.password);
oAuth2ClientContext.setAccessToken(askToken(map));
}
if (oAuth2ClientContext.getAccessToken() != null){
return Optional.ofNullable(oAuth2ClientContext.getAccessToken().getValue());
} else {
return Optional.empty();
}
}
private CustomOAuth2AccessToken askToken( MultiValueMap<String, String> map) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
ResponseEntity<CustomOAuth2AccessToken> response = restTemplate.postForEntity(
this.authUrl, request, CustomOAuth2AccessToken.class);
if (response != null && response.hasBody()) {
return response.getBody();
} else {
return null;
}
}
}
最后是资源:
@RestController
@RequestMapping("/api")
public class DocumentResource {
private static String TMP_FILE_PREFIX = "smartdoc_tmp";
public DocumentResource() {
}
@GetMapping("/ebox/test")
public ResponseEntity<HasEboxResponse> test() {
return ResponseEntity.ok(new HasEboxResponse());
}
@PostMapping("/ebox/test")
public ResponseEntity<HasEboxResponse> testPost(@RequestBody HasEboxRequest request) {
return ResponseEntity.ok(new HasEboxResponse());
}
@PutMapping("/ebox/test")
public ResponseEntity<HasEboxResponse> testPut(@RequestBody HasEboxRequest request) {
return ResponseEntity.ok(new HasEboxResponse());
}
}
谢谢!
问题出在 spring 安全配置中。 WebSecurity 不允许在未经身份验证的情况下调用“[SERVICE_NAME]/api”之类的 url。我添加了一条规则以允许访问某些网址。如果访问令牌在 header 中,它将由 zuul 转发给服务。
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/ext/*/api/**") // allow calls to services, redirect by zuul
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/index.html")
.antMatchers("/test/**");
}
为了通过UI调用其他服务并让网关注入访问令牌,我在我的zuul配置中定义了两组路由,
routes:
myservice:
path: /myservice/**
serviceId: myservice
myservice_ext:
path: /ext/myservice/**
serviceId: myservice
- /ext/myService... : 引用服务,不要被 spring secu 忽略
- /myService...:引用服务但由 spring secu 处理