Oauth 2.0 外部客户端向微服务发出POST/PUT请求时出现错误403

Oauth 2.0 外部客户端向微服务发出POST/PUT请求时出现错误403,oauth-2.0,jhipster,http-status-code-403,netflix-zuul,Oauth 2.0,Jhipster,Http Status Code 403,Netflix Zuul,我有一个java遗留应用程序不能使用SpringCloud。它使用外部客户端通过网关访问微服务 网关和服务由jhipster 5.7.2生成,带有OAuth2/OIDC选项 在我的客户机中,请求拦截器调用keydrope以获得令牌(直接访问授权),并将其注入到头中 当我发出GET请求时是可以的,但我在POST或PUT请求后收到403 CORS已在网关上启用(但未使用,因为请求不是CORS请求)。我在开发模式下运行它。 Zuul路线似乎还可以。 我没有在网关和服务上更改配置 有人有主意吗 下面是我

我有一个java遗留应用程序不能使用SpringCloud。它使用外部客户端通过网关访问微服务

网关和服务由jhipster 5.7.2生成,带有OAuth2/OIDC选项

在我的客户机中,请求拦截器调用keydrope以获得令牌(直接访问授权),并将其注入到头中

当我发出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); }
拦截器:

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;
        }
}
公共类GedRequestInterceptor实现RequestInterceptor{
公共静态最终字符串AUTHORIZATION=“AUTHORIZATION”;
公共静态最终字符串BEARER=“BEARER”;
私有字符串authUrl;
私有字符串用户;
私有字符串密码;
私有字符串clientId;
私有字符串clientSecret;
私有RestTemplate RestTemplate;
私有客户oAuth2ClientContext oAuth2ClientContext;
公共GedRequestInterceptor(字符串authUrl、字符串用户、字符串密码、字符串clientId、字符串clientSecret){
超级();
this.authUrl=authUrl;
this.user=用户;
this.password=密码;
this.clientId=clientId;
this.clientSecret=clientSecret;
restTemplate=新的restTemplate();
//oAuth2ClientContext=新的默认oAuth2ClientContext();
}
@凌驾
公共无效应用(请求模板模板){
//需求方未标记钥匙斗篷和联合请求
可选标记=getToken();
if(token.isPresent()){
template.header(HttpHeaders.ORIGIN,“localhost”);
template.header(AUTHORIZATION,String.format(“%s%s”,BEARER,token.get());
}
}
私有可选getToken(){
如果(oAuth2ClientContext.getAccessToken()==null | | oAuth2ClientContext.getAccessToken().isExpired()){
MultiValueMap=新链接的MultiValueMap();
map.add(“client_id”,this.clientId);
map.add(“client_secret”,this.clientSecret);
map.add(“授权类型”、“密码”);//客户端凭据//密码
map.add(“用户名”,this.user);
map.add(“password”,this.password);
oAuth2ClientContext.setAccessToken(askToken(map));
} 
如果(oAuth2ClientContext.getAccessToken()!=null){
返回可选的.ofNullable(oAuth2ClientContext.getAccessToken().getValue());
}否则{
返回可选的.empty();
}
}
私有CustomOAuth2AccessToken askToken(多值映射){
HttpHeaders=新的HttpHeaders();
headers.setContentType(MediaType.APPLICATION\u FORM\u URLENCODED);
HttpEntity请求=新的HttpEntity(映射、头);
ResponseEntity响应=restTemplate.postForEntity(
this.authUrl、request、CustomOAuth2AccessToken.class);
if(response!=null&&response.hasBody()){
返回response.getBody();
}否则{
返回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());
        }

}
@RestController
@请求映射(“/api”)
公共类文档资源{
私有静态字符串TMP\u FILE\u PREFIX=“smartdoc\u TMP”;
公共文档资源(){
}
@GetMapping(“/ebox/test”)
公众反应测试(){
返回ResponseEntity.ok(新的HasEboxResponse());
}
@后映射(“/ebox/test”)
公共响应性测试帖子(@RequestBody HasEboxRequest){
返回ResponseEntity.ok(新的HasEboxResponse());
}
@PutMapping(“/ebox/test”)
public ResponseEntity testPut(@RequestBody HasEboxRequest request){
返回ResponseEntity.ok(新的HasEboxResponse());
}
}

谢谢

问题出在spring安全配置中。WebSecurity不允许在没有身份验证的情况下调用像“[SERVICE\u NAME]/api”这样的URL。我添加了一个允许访问某些URL的规则。如果头中有一个访问令牌,它将由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 
  • /分机/我的服务…:spring secu引用服务和“不”和“忽略”
  • /我的服务…:引用服务,但由spring secu处理

看起来您正试图使用标头中的令牌发出请求。JHipster的OAuth2实现默认情况下使用cookie(启用了xsrf保护)。如果您想启用使用授权请求头,您可以添加一个配置,如感谢Jon,我将我的解决方案放在下面。我不想更改jhipster默认值,因为我也在使用网关的UI和服务。但我还需要允许来自旧版应用程序的呼叫,并在标题中添加令牌。因此,我不会更改默认设置,只是让zuul将调用重定向到服务。您可以在服务之间共享标题:@redoff,调用方不是服务,它是一个遗留应用程序,不能使用spring cloud、注册到eureka等。调用方必须是spring cloud服务不是强制性的,您可以尝试以下方法:
routes:
    myservice: 
        path: /myservice/**
        serviceId: myservice
    myservice_ext: 
        path: /ext/myservice/**
        serviceId: myservice