无法从不同来源调用受保护的 spring 启动休息服务
Unable to call a secured spring boot rest service from a different origin
我有 3 个 spring 启动应用程序:
localhost:8081(认证服务器)
localhost:8083 (UI)
localhost:8101(上传服务)
当用户转到 localhost:8083/app
时,它会将其重定向到 localhost:8081/login
以向他们提供一个表单 post 他们的凭据,然后将用户重定向回 localhost:8081/app
并显示网站。目前没有问题。
但是,我想添加一个上传功能,用户 drags/drops 将一些文件上传到输入文件中,文件类型如下:
<input type="file" multiple style="height: 100%; width: 100%; z-index: 100; opacity:0" v-bind:name="uploadFieldName" v-bind:disabled="isSaving" v-on:change="filesChange($event.target.name, $event.target.files);">
然后它将调用 localhost:8101/upload
通过 Axios 调用上传文件。
为了避免 CSRF 问题,我添加了
<meta th:name="_csrf" th:content="${_csrf.token}"/>
<meta th:name="_csrf_header" th:content="${_csrf.headerName}"/>
到 HTML 并且在 JS 中我有以下设置 Axios 以使用令牌并发送 cookie:
var csrfHeader = $("meta[name='_csrf_header']").attr("content");
var csrfToken = $("meta[name='_csrf']").attr("content");
axios.defaults.headers = {
'XSRF-TOKEN': csrfToken
}
axios.defaults.withCredentials = true;
我在JS中的调用如下:
upload(formData){
var url = 'http://localhost:8101/upload';
return axios.post(url, formData);
}
在我的上传服务后端,我只是获取文件并写下它们的名称来测试它是否有效:
@RestController
public class AssetController {
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public void importAssets(@RequestParam("upload") MultipartFile[] files){
for(MultipartFile file: files){
System.out.println(file.getOriginalFilename());
}
}
}
为了允许其他域(UI应用程序)访问上传服务,我设置了CORS映射如下:
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}
我还读到 here 我需要为多部分文件上传添加这个:
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
最后我的安全配置如下:
@EnableOAuth2Sso
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.antMatcher("/**")
.authorizeRequests()
.antMatchers("/login**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
当我尝试上传文件时,我看到 2 个具有 OPTION 状态的 localhost:8101/upload 调用具有相同的响应和请求 headers:
回复:
HTTP/1.1 302
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: XSRF-TOKEN=0ef5a80f-4ffc-49ab-bfb0-813ae4ca149e; Path=/
Location: http://localhost:8101/login
Content-Length: 0
Date: Thu, 23 Aug 2018 19:57:08 GMT
要求:
OPTIONS /upload HTTP/1.1
Host: localhost:8101
Connection: keep-alive
Access-Control-Request-Method: POST
Origin: http://localhost:8083
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Access-Control-Request-Headers: xsrf-token
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,tr;q=0.8
然而 POST 并没有发生。所以为了进一步测试,我允许 /upload**
在 SecurityConfig
中,如下所示:
@EnableOAuth2Sso
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.antMatcher("/**")
.authorizeRequests()
.antMatchers("/login**","/upload**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
随着它开始工作,我可以在网络中看到一个选项和一个 POST,并且我有以下请求和响应 headers:
要求:
POST /upload HTTP/1.1
Host: localhost:8101
Connection: keep-alive
Content-Length: 2507057
Origin: http://localhost:8083
X-XSRF-TOKEN: 4b070c11-9250-40d6-9d2a-d6587e814382
XSRF-TOKEN: 52f8fd81-2c80-493e-b7c6-f0a458b8c2e9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7lG6mO10sPzrDU5l
Accept: */*
Referer: http://localhost:8083/toybox
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,tr;q=0.8
Cookie: JSESSIONID=0EF7E75D8A8187BC7B5FB74341207E76; TSESSION=4A8DE8A4A4A430DF2AC9AF14A0BD0E50; XSRF-TOKEN=4b070c11-9250-40d6-9d2a-d6587e814382
回复:
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
X-Application-Context: toybox-asset-service:8101
Access-Control-Allow-Origin: http://localhost:8083
Vary: Origin
Access-Control-Allow-Credentials: true
Content-Length: 0
Date: Thu, 23 Aug 2018 20:05:47 GMT
我还在日志中看到文件名 posted:
cat-pet-animal-domestic-104827.jpeg
kittens-cat-cat-puppy-rush-45170.jpeg
所以调用成功。但是,由于我希望 localhost:8101/upload
受到保护,以便只有授权用户才能上传文件,所以不允许任何人自由使用它不是一种选择。
经过几个小时的研究,我得出的结论是,不知何故,cookie 不是由 Axios 发送的。因为在安全 /upload
场景中,我发现请求中没有发送任何 cookie。
我的问题是:
- 如果我的理论是正确的,我该如何将 cookie 与
要求?
- 还有 solution/configuration 我可以去吗?
如有任何帮助,我们将不胜感激。谢谢。
编辑 1:
我正在使用 oauth2 作为身份验证服务器,并且在 UI 和上传服务中的 application.properties 中进行了以下设置:
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=secret
security.oauth2.client.access-token-uri=http://localhost:8081/oauth/token
security.oauth2.client.user-authorization-uri=http://localhost:8081/oauth/authorize
security.oauth2.resource.user-info-uri=http://localhost:8081/me
localhost:8081 (认证服务器) localhost:8083 (UI) localhost:8101
所有这些都将被浏览器视为不同的域。因此,其中一个设置的 cookie 不会随请求一起发送给其他人。这里有 2 个选项
第一个选项是:如果你想坚持传统的身份验证方式,让你的 Auth 服务器处理所有请求,让它代理 UI 并将服务请求上传到相应的上游服务。您可以使用其他方式,例如防火墙 - iptables/firewalld 或 security-groups/ACLs(如果您在云端)来控制对其他 2 个应用程序(UI 和上传)
第二个选项是:使您的堆栈现代化,将 oauth2 服务器作为身份验证服务器,它将在对用户进行身份验证后发出 oauth2/jwt 令牌。您的 javascript(来自客户端浏览器上的网页 运行)应该连同所有后续请求一起发送该令牌。您的其他服务(UI 和上传)将规定与 Auth 服务器通信以验证令牌的有效性。如果发现无效,将他们重定向到相应的页面。
我通过在 Windows.
上为 运行 Redis 使用 Spring Session. Spring Session uses Redis, and since I am on Windows, I followed the instructions here 解决了这个问题
我从 https://github.com/ServiceStack/redis-windows/raw/master/downloads/redis-latest.zip 下载了 Redis 并将其解压到 C:\Redis
。
通过命令提示符导航到目录后,我 运行 命令 redis-server.exe redis.windows.conf
(或 Powershell 中的 ./redis-server.exe redis.windows.conf
)
我将以下内容添加到 UI 和上传服务的 POM 文件
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
最后,我在 UI 和上传服务的 application.properties
文件中添加了以下内容:
spring.session.store-type=redis
server.servlet.session.timeout=3600
spring.session.redis.flush-mode=on-save
spring.session.redis.namespace=spring:session
spring.redis.host=localhost
spring.redis.password=
spring.redis.port=6379
这里密码为空,因为Redis默认没有设置密码,我没有设置密码是为了测试。
当我重新启动我的应用程序时,我看到当我登录 UI 应用程序时,我可以使用我在 UI 应用程序中的会话成功调用上传服务。
我有 3 个 spring 启动应用程序:
localhost:8081(认证服务器) localhost:8083 (UI) localhost:8101(上传服务)
当用户转到 localhost:8083/app
时,它会将其重定向到 localhost:8081/login
以向他们提供一个表单 post 他们的凭据,然后将用户重定向回 localhost:8081/app
并显示网站。目前没有问题。
但是,我想添加一个上传功能,用户 drags/drops 将一些文件上传到输入文件中,文件类型如下:
<input type="file" multiple style="height: 100%; width: 100%; z-index: 100; opacity:0" v-bind:name="uploadFieldName" v-bind:disabled="isSaving" v-on:change="filesChange($event.target.name, $event.target.files);">
然后它将调用 localhost:8101/upload
通过 Axios 调用上传文件。
为了避免 CSRF 问题,我添加了
<meta th:name="_csrf" th:content="${_csrf.token}"/>
<meta th:name="_csrf_header" th:content="${_csrf.headerName}"/>
到 HTML 并且在 JS 中我有以下设置 Axios 以使用令牌并发送 cookie:
var csrfHeader = $("meta[name='_csrf_header']").attr("content");
var csrfToken = $("meta[name='_csrf']").attr("content");
axios.defaults.headers = {
'XSRF-TOKEN': csrfToken
}
axios.defaults.withCredentials = true;
我在JS中的调用如下:
upload(formData){
var url = 'http://localhost:8101/upload';
return axios.post(url, formData);
}
在我的上传服务后端,我只是获取文件并写下它们的名称来测试它是否有效:
@RestController
public class AssetController {
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public void importAssets(@RequestParam("upload") MultipartFile[] files){
for(MultipartFile file: files){
System.out.println(file.getOriginalFilename());
}
}
}
为了允许其他域(UI应用程序)访问上传服务,我设置了CORS映射如下:
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}
我还读到 here 我需要为多部分文件上传添加这个:
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
最后我的安全配置如下:
@EnableOAuth2Sso
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.antMatcher("/**")
.authorizeRequests()
.antMatchers("/login**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
当我尝试上传文件时,我看到 2 个具有 OPTION 状态的 localhost:8101/upload 调用具有相同的响应和请求 headers:
回复:
HTTP/1.1 302
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: XSRF-TOKEN=0ef5a80f-4ffc-49ab-bfb0-813ae4ca149e; Path=/
Location: http://localhost:8101/login
Content-Length: 0
Date: Thu, 23 Aug 2018 19:57:08 GMT
要求:
OPTIONS /upload HTTP/1.1
Host: localhost:8101
Connection: keep-alive
Access-Control-Request-Method: POST
Origin: http://localhost:8083
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Access-Control-Request-Headers: xsrf-token
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,tr;q=0.8
然而 POST 并没有发生。所以为了进一步测试,我允许 /upload**
在 SecurityConfig
中,如下所示:
@EnableOAuth2Sso
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.antMatcher("/**")
.authorizeRequests()
.antMatchers("/login**","/upload**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
随着它开始工作,我可以在网络中看到一个选项和一个 POST,并且我有以下请求和响应 headers:
要求:
POST /upload HTTP/1.1
Host: localhost:8101
Connection: keep-alive
Content-Length: 2507057
Origin: http://localhost:8083
X-XSRF-TOKEN: 4b070c11-9250-40d6-9d2a-d6587e814382
XSRF-TOKEN: 52f8fd81-2c80-493e-b7c6-f0a458b8c2e9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7lG6mO10sPzrDU5l
Accept: */*
Referer: http://localhost:8083/toybox
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,tr;q=0.8
Cookie: JSESSIONID=0EF7E75D8A8187BC7B5FB74341207E76; TSESSION=4A8DE8A4A4A430DF2AC9AF14A0BD0E50; XSRF-TOKEN=4b070c11-9250-40d6-9d2a-d6587e814382
回复:
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
X-Application-Context: toybox-asset-service:8101
Access-Control-Allow-Origin: http://localhost:8083
Vary: Origin
Access-Control-Allow-Credentials: true
Content-Length: 0
Date: Thu, 23 Aug 2018 20:05:47 GMT
我还在日志中看到文件名 posted:
cat-pet-animal-domestic-104827.jpeg
kittens-cat-cat-puppy-rush-45170.jpeg
所以调用成功。但是,由于我希望 localhost:8101/upload
受到保护,以便只有授权用户才能上传文件,所以不允许任何人自由使用它不是一种选择。
经过几个小时的研究,我得出的结论是,不知何故,cookie 不是由 Axios 发送的。因为在安全 /upload
场景中,我发现请求中没有发送任何 cookie。
我的问题是:
- 如果我的理论是正确的,我该如何将 cookie 与 要求?
- 还有 solution/configuration 我可以去吗?
如有任何帮助,我们将不胜感激。谢谢。
编辑 1: 我正在使用 oauth2 作为身份验证服务器,并且在 UI 和上传服务中的 application.properties 中进行了以下设置:
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=secret
security.oauth2.client.access-token-uri=http://localhost:8081/oauth/token
security.oauth2.client.user-authorization-uri=http://localhost:8081/oauth/authorize
security.oauth2.resource.user-info-uri=http://localhost:8081/me
localhost:8081 (认证服务器) localhost:8083 (UI) localhost:8101 所有这些都将被浏览器视为不同的域。因此,其中一个设置的 cookie 不会随请求一起发送给其他人。这里有 2 个选项
第一个选项是:如果你想坚持传统的身份验证方式,让你的 Auth 服务器处理所有请求,让它代理 UI 并将服务请求上传到相应的上游服务。您可以使用其他方式,例如防火墙 - iptables/firewalld 或 security-groups/ACLs(如果您在云端)来控制对其他 2 个应用程序(UI 和上传)
第二个选项是:使您的堆栈现代化,将 oauth2 服务器作为身份验证服务器,它将在对用户进行身份验证后发出 oauth2/jwt 令牌。您的 javascript(来自客户端浏览器上的网页 运行)应该连同所有后续请求一起发送该令牌。您的其他服务(UI 和上传)将规定与 Auth 服务器通信以验证令牌的有效性。如果发现无效,将他们重定向到相应的页面。
我通过在 Windows.
上为 运行 Redis 使用 Spring Session. Spring Session uses Redis, and since I am on Windows, I followed the instructions here 解决了这个问题我从 https://github.com/ServiceStack/redis-windows/raw/master/downloads/redis-latest.zip 下载了 Redis 并将其解压到 C:\Redis
。
通过命令提示符导航到目录后,我 运行 命令 redis-server.exe redis.windows.conf
(或 Powershell 中的 ./redis-server.exe redis.windows.conf
)
我将以下内容添加到 UI 和上传服务的 POM 文件
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
最后,我在 UI 和上传服务的 application.properties
文件中添加了以下内容:
spring.session.store-type=redis
server.servlet.session.timeout=3600
spring.session.redis.flush-mode=on-save
spring.session.redis.namespace=spring:session
spring.redis.host=localhost
spring.redis.password=
spring.redis.port=6379
这里密码为空,因为Redis默认没有设置密码,我没有设置密码是为了测试。
当我重新启动我的应用程序时,我看到当我登录 UI 应用程序时,我可以使用我在 UI 应用程序中的会话成功调用上传服务。