Angular通过SSL的PUT请求被禁止。

12

我正在使用Angular 5作为前端和Spring Boot REST服务器。如果不使用SSL,一切都可以正常工作。但是当我切换到SSL后,我最终让一切都有些工作。它适用于GET请求,但到目前为止,我无法使PUT请求通过。

我猜这可能是CORS问题,因为GET是一个简单的请求,而PUT显然不是 (CORS参考),但我无法找出如何解决问题。

在我的Spring Boot Rest Controllers上,我使用注释@CrossOrigin("*"),所以我认为那不是问题,但我不确定。

谜题的另一部分是身份验证是通过CAS服务器处理的。我已将以下配置添加到CAS属性中。 这是允许GET请求工作的最后一步,但我不确定要在它们上更改什么(如果有)来处理PUT请求:

cas.httpWebRequest.cors.enabled=true
cas.httpWebRequest.cors.allowOrigins[0]=*
cas.httpWebRequest.cors.allowMethods[0]=*
cas.httpWebRequest.cors.allowHeaders[0]=*

以下是我的请求头和响应:

请求:

Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.5
Connection: keep-alive
Content-Length: 1975
Content-Type: application/json
Cookie: JSESSIONID=117D9345E985D824E46…BF32; io=gLhCcBoZrfNcppioAAAB
Host: localhost:4200
Referer: https://localhost:4200/sales/proposals/dashboard
User-Agent: Mozilla/5.0 (Windows NT 10.0; …) Gecko/20100101 Firefox/57.0

响应(状态码 - 403 Forbidden):

access-control-allow-origin: *
cache-control: no-cache, no-store, max-age=0, must-revalidate
content-length: 56
content-type: application/json;charset=UTF-8
date: Wed, 03 Jan 2018 16:39:14 GMT
expires: 0
pragma: no-cache
strict-transport-security: max-age=31536000 ; includeSubDomains
x-content-type-options: nosniff
X-Firefox-Spdy: h2
x-frame-options: DENY
x-powered-by: Express
x-xss-protection: 1; mode=block

Angular服务运行在https://localhost:4200上。

Spring Boot服务运行在https://localhost:8493上。

CAS服务运行在https://localhost:8443上。

我没有看到任何日志中的错误信息。我想要理解为什么PUT请求被禁止,以及如何修复它,使得PUT请求也能正常工作。谢谢!

编辑:添加Spring Boot安全配置

<http pattern="/**" entry-point-ref="casEntryPoint">
            <intercept-url pattern="/api/holidays" access="permitAll"/>
            <intercept-url pattern="/api/unit**" access="permitAll"/>
            <intercept-url pattern="/**" access="isAuthenticated()" />

            <custom-filter ref="casAuthenticationFilter" before="CAS_FILTER"/>

            <csrf/>         
    </http>

    <global-method-security pre-post-annotations="enabled"/>

    <!-- CAS Config -->
    <beans:bean id="casEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
            <beans:property name="loginUrl" value="${cas.server.host.login_url}"/>
            <beans:property name="serviceProperties" ref="serviceProperties"/>
    </beans:bean>

    <beans:bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
            <beans:property name="service" value="${app.server.host.url}login/cas"></beans:property>
    </beans:bean>

    <beans:bean id="casAuthenticationFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
            <beans:property name="authenticationManager" ref="authenticationManager"/>
    </beans:bean>

    <beans:bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
            <beans:property name="ticketValidator" ref="ticketValidator"></beans:property>
            <beans:property name="serviceProperties" ref="serviceProperties"></beans:property>
            <beans:property name="key" value="Key"></beans:property>
            <beans:property name="authenticationUserDetailsService" ref="userDetailsWrapper"/>
    </beans:bean>

    <beans:bean id="userDetailsWrapper" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
        <beans:property name="userDetailsService" ref="userDetails"></beans:property>
    </beans:bean>

    <ldap-user-service id="userDetails"
        server-ref="ldapServer"
        group-search-base="ou=ERPGroups,OU=MyBusiness"
        group-search-filter="(member={0})"
        user-search-base="ou=SBSUsers,OU=Users,OU=MyBusiness"
        user-search-filter="(sAMAccountName={0})" />

    <ldap-server id="ldapServer" url="${ldap.urls}/${ldap.base}" manager-dn="${ldap.username}" manager-password="${ldap.password}" />

    <beans:bean id="ticketValidator" class="org.jasig.cas.client.validation.Cas30ServiceTicketValidator">
            <beans:constructor-arg value="${cas.server.host.url}"></beans:constructor-arg>
    </beans:bean>

    <authentication-manager alias="authenticationManager">  
        <authentication-provider ref="casAuthenticationProvider" />
    </authentication-manager>

编辑:添加Angular代理配置

{
    "/api": {
        "target" : "https://localhost:8493",
        "changeOrigin": true,
        "secure" : false
    }
}

403表示禁止访问,就像用户未经身份验证/允许访问服务器端方法一样。我对CAS不是很了解,但CAS cookie是否在PUT请求中发送?(我看到一个会话cookie和“io”cookie)。如果这是一个cors问题,它将是客户端问题,如果您启动浏览器的开发工具/调试器,您应该能看到一些东西。 - David
你能提供在Spring Boot服务器应用程序上使用的安全配置吗? - vsoni
我刚刚发现了 x-powered-by: Express 和 x-xss-protection: 1; mode=block。你正在使用 NodeJS Express 代理吗? - jornare
如遇CORS问题,请参考此链接 https://stackoverflow.com/questions/46788969/angular2-spring-boot-allow-cross-origin-on-put/46789290#46789290 - hrdkisback
8个回答

3
在您的Spring Boot安全配置中,我看到启用了CSRF。但是在您的请求中,我没有看到任何cookie“XSRF-TOKEN”和头文件“X-XSRF-TOKEN”。这就是为什么服务器不接受请求并响应403的原因。
您的浏览器可能没有创建SSL连接。这通常发生在浏览器找到错误证书、自签名证书或由不受信任的认证机构签名的证书时。
如果您使用Chrome浏览器,可以通过启用“allow-insecure-localhost”属性来为本地主机进行测试。在地址栏中输入此URL,单击“启用”,然后重新启动Chrome。
chrome://flags/#allow-insecure-localhost

请参考以下链接,了解如何在Chrome和Firefox中启用它。 https://improveandrepeat.com/2016/09/allowing-self-signed-certificates-on-localhost-with-chrome-and-firefox/


谢谢您的想法。我尝试从Spring Boot配置中删除csrf,并在启用不安全的localhost的Chrome中尝试(之前一直使用Firefox),但无论哪种方式,我都得到了相同的响应。 - Tim
1
从Spring Security 4.0开始,默认启用CSRF保护和XML配置,因此简单地删除它是不够的。您应该使用<csrf disabled="true"/>将其禁用以使其正常工作。但一旦您证明它是CSRF,请将其重新启用并修复您的Angular代码以正确处理CSRF,以确保您的网站不会留下漏洞。 - kichik

3

我认为您在请求头中应该有一个X-XSRF-TOKEN令牌,因为您的安全配置启用了csrf。对我而言,以下方法有效:

 constructor(private http: HttpClient, private tokenExtractor: HttpXsrfTokenExtractor) {
    }

然后提取一个令牌。
const token = this.tokenExtractor.getToken() as string;

并将其添加到头部

this.http.post<any>(url, body, {headers: new HttpHeaders().set('X-XSRF-TOKEN', token)})

希望这能有所帮助。

2
浏览器强制执行CORS,但在这种情况下,您从服务器收到403。正如其他人提到的那样,您的请求似乎缺少CSRF标头,这可能是服务器拒绝您的请求的原因。我无法确定为什么在切换到HTTPS时会出现这种情况,但我可以告诉您,在您的配置中有,但没有匹配的标头。
为了验证这个理论,您可以通过更改以下内容来禁用CSRF保护:CSRF
<csrf/>

to:

<csrf disabled="true"/>

从Spring 4开始,默认启用CSRF保护,因此仅删除<csrf/>将不足以解决问题。

一旦确认CSRF是问题所在,并且不再收到403错误,您应该按照Angular支持的方式重新启用它。这是一项重要的保护措施,可以保护您的用户。Angular期望有一个名为XSRF-TOKEN的Cookie,并将其与X-XSRF-TOKEN头一起发送。这些是可配置的,但如果您使用最新的Spring和最新的Angular,则应该与Spring的默认值匹配。请按照Spring文档添加:

<http>
    <!-- ... -->
    <csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
    class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
    p:cookieHttpOnly="false"/>

2

可能会修改服务器上某些内容的HTTP请求,如PUT、POST、DELETE和PATCH,如果事先没有告知,它们不知道是否被允许执行该请求。

它们可以通过读取先前允许它们修改所请求资源的请求的标头,或者它们将在执行请求之前触发一个OPTIONS请求来了解。这称为“预检”请求。

如果您尚未实现OPTIONS方法以响应CORS标头,则需要执行以下操作。

请查看此处获取更多详细信息。


感谢提供这个信息丰富的资源。我读到的一些内容让我觉得浏览器应该自动发送预检请求。但是在浏览器网络调试工具中,我没有看到任何预检请求。我是否需要手动创建它,或者如何强制浏览器发送预检请求? - Tim
它是自动发送的,但您可能需要在服务器端处理它。我之前关于express的问题是因为如果您使用代理,则可能根本没有进行CORS。 - jornare
我在开发过程中使用代理。在 package.json 中,我有 start: ng serve --proxy-config proxy.conf.json ...,在该配置文件中我有 ...target: https://localhost:8493 ...。因此,不同的端口正在使用,我认为这将被视为不同的来源。 - Tim
不,它并不会,因为源是浏览器查看的内容,并且通过您的Angular开发服务器进行通信,该服务器在返回之前转发请求并添加自己的标头。 - jornare
所以如果CORS不是问题,为什么GET请求可以工作,但PUT请求会得到“Forbidden”响应? - Tim
这是一个有趣的问题。我现在无法提供明确的答案,因为转发SSL请求并不是一件简单的事情。这里有一个有趣的线程 https://github.com/angular/angular-cli/issues/4031,讨论了一些机会。此外,官方文档说明了如何在需要时修改代理中的标头。你能发布你的proxy.config吗?你应该从 'changeOrigin': true 开始。 - jornare

1
我遇到了类似的问题,我的前端是Angular4,后端是Spring,认证使用的是基于JWT的认证。我在进行POST请求时遇到了这个错误,但我想PUT也适用,因为两者都属于“不那么简单的请求”(预检请求的别名)。
我的UI工作了一段时间,然后我得到了以下错误:
无法加载https://localhost:8080/myApp/services/stationCollection/viewCollected。所请求的资源上不存在'Access-Control-Allow-Origin'标头。
经过分析,我知道预检请求已经通过了,完全成功,主请求被拒绝访问服务器(您可以在chrome开发工具的网络选项卡中检查并获取详细信息)。发送的CORS请求类型是“不太简单的请求”。事实上,如果您使用Rest Client / curl将相同的请求发送到同一台服务器,则一切正常。
当您在Web中浏览此错误时,会找到很多解决方案,所有解决方案都指导在服务器端配置CORS,但事实上我已经实现了CORS。
可能的解决方案:

所有这些都导致我寻找解决方案,经过长时间的搜索,我发现有一个可能的黑客/替代方法解决了这个问题,最好在以下博客中进行定义:

https://laracasts.com/discuss/channels/laravel/cros-access-control-allow-origin-not-in-the-headers

这篇博客要求你创建一个中间件,用于在所有从用户界面传递到前端的请求之间进行处理。如果您知道您的应用程序是一个真实的应用程序,请添加。
myOptions.headers.append('Access-Control-Allow-Origin', '*');

在中间件中。希望这可以帮助你。

谢谢,Rahul。当我观察网络工具时,没有预检请求被发送。显然,当使用代理时,它会以某种方式被处理。但是没有预检请求,我认为这个选项不会起作用。 - Tim

0
为什么不在你的Spring Boot应用程序上尝试这个呢?
HttpHeaders headers = new HttpHeaders();

headers.add("Access-Control-Allow-Origin", "*");
headers.add("Access-Control-Allow-Credentials", "true");
headers.add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS, HEAD");
headers.add("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");

return new ResponseEntity<>(service.update(input), headers, HttpStatus.OK);

更多阅读内容可以在这里找到 https://spring.io/blog/2015/06/08/cors-support-in-spring-framework


0

更改Spring配置以允许CORS可能不是一个好主意。我的解决方案是安装nginx服务器并将其用作反向代理。以下是我的样本nginx.conf:

# Spring backend
location /api
{
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_pass http://localhost:8493;
}

# Angular front end
location /
{
  root www/apps;
  try_files $uri $uri/ /index.html;
}

0

尝试在服务器端添加此标头:Access-Control-Allow-Credentials: true

更多信息

如果这不能帮助您并且其他的POST请求按预期工作,那么恐怕您的代码某些部分是错误的。


我尝试将以下代码添加到我的Spring Boot Rest Controller中,例如:HttpHeaders headers = new HttpHeaders();headers.add("Access-Control-Allow-Credentials", "true");return new ResponseEntity<>(service.update(input), headers, HttpStatus.OK);,但似乎没有任何区别。如果只有SSL或非SSL的更改,那么代码怎么可能出错呢?此外,为什么响应会是“Forbidden”? - Tim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接