Spring + Angular 2 + Oauth2 + CORS 的 CSRF 问题
CSRF issue with Spring + Angular 2 + Oauth2 + CORS
我正在开发一个基于 Spring 4.3 和 Angular (TypeScript) 4.3 的 client-server 应用程序,在 CORS 场景中,在生产环境中,服务器和客户端位于不同的域中。客户端通过 http 请求请求 REST 服务器 API。
1。 REST 和 OAUTH 配置:
服务器公开 REST API:
@RestController
@RequestMapping("/my-api")
public class MyRestController{
@RequestMapping(value = "/test", method = RequestMethod.POST)
public ResponseEntity<Boolean> test()
{
return new ResponseEntity<Boolean>(true, HttpStatus.OK);
}
}
如 spring 文档中所述,受 Oauth2 保护。显然我修改了上面的内容以适合我的应用程序。
一切正常:我可以通过 refresh_token 和 access_token 使用 Oauth2 保护 /my-api/test
。 Oauth2 没有问题。
2。 CORS 配置:
由于服务器相对于客户端位于单独的域中(服务器:10.0.0.143:8080
,客户端:localhost:4200
,正如我现在正在开发的那样),我需要在服务器端启用 CORS:
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
...
@Autowired
SimpleCorsFilter simpleCorsFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.addFilterAfter(simpleCorsFilter, CorsFilter.class)
.csrf().disable() // notice that now csrf is disabled
... (the rest of http security configuration follows)...
}
}
其中 SimpleCorsFilter 添加了我需要的 headers:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCorsFilter extends OncePerRequestFilter {
public SimpleCorsFilter() {
}
@Override
public void destroy() {
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
response.addHeader("Access-Control-Allow-Origin", "http://localhost:4200");
response.addHeader("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,PATCH,OPTIONS");
response.addHeader("Access-Control-Max-Age", "3600");
response.addHeader("Access-Control-Allow-Credentials", "true");
response.addHeader("Access-Control-Allow-Headers", "MyCustomHeader, Authorization, X-XSRF-TOKEN");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(request, response);
}
}
}
现在,如果我使用 angular2 发出 http get 或 post 请求,示例:
callPostMethod(tokenData) {
const url = 'my-api.domain.com/my-api';
const pars = new HttpParams();
const body = null;
let hds = new HttpHeaders()
.append('Authorization', 'Bearer ' + tokenData.access_token)
.append('Content-Type', 'application/x-www-form-urlencoded');
return this.http.post <Installation> (url, body, {
params : pars,
headers: hds,
withCredentials: true
});
}
一切正常。所以即使是 CORS 配置似乎也没问题。
3。 CSRF 配置:
如果我现在像这样在 Spring 中启用 CSRF 配置:
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
...
@Autowired
SimpleCorsFilter simpleCorsFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.addFilterAfter(simpleCorsFilter, CorsFilter.class)
.csrf().csrfTokenRepository(getCsrfTokenRepository()) // notice that csrf is now enabled
... (the rest of http security configuration follows)...
}
@Bean
@Autowired
// used only because I want to setCookiePath to /, otherwise I can simply use
// http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
public CsrfTokenRepository getCsrfTokenRepository() {
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
//tokenRepository.setHeaderName("X-XSRF-TOKEN"); // has already this name --> comment
tokenRepository.setCookiePath("/");
return tokenRepository;
}
}
在第一个 POST 请求中,它给了我 403 错误:
"Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'."
为什么会这样(据我了解..)?
通过探索 CSRF 的工作机制,我注意到 Spring 正确地生成了一个名为 XSRF-TOKEN 的 cookie,它是在响应 cookie 中设置的(通过使用 Chrome、Set-Cookie 下响应 Headers).
接下来应该发生的是 angular,在执行第一个 POST 请求时,应该读取从 Spring 收到的 cookie 并生成一个请求 Header 调用 X-XSRF-TOKEN,其值等于cookie的值
如果我检查失败的POST请求的Header,我看到没有X-XSRF-TOKEN,就像angular没有做出它应该做的按照它的 CSRF 规范来做,见图:
Failed CSRF request, Chrome inspector
查看xsrf (/angular/angular/blob/4.3.5/packages/common/http/src/xsrf.ts)的angular实现,你可以看到在HttpXsrfInterceptor中,如果没有添加csrf headers目标URL以http
开头(后面是angularxsrf.ts源码复制粘贴):
/**
* `HttpInterceptor` which adds an XSRF token to eligible outgoing requests.
*/
@Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor {
constructor(
private tokenService: HttpXsrfTokenExtractor,
@Inject(XSRF_HEADER_NAME) private headerName: string) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const lcUrl = req.url.toLowerCase();
// Skip both non-mutating requests and absolute URLs.
// Non-mutating requests don't require a token, and absolute URLs require special handling
// anyway as the cookie set
// on our origin is not the same as the token expected by another origin.
if (req.method === 'GET' || req.method === 'HEAD' || lcUrl.startsWith('http://') ||
lcUrl.startsWith('https://')) {
return next.handle(req);
}
const token = this.tokenService.getToken();
// Be careful not to overwrite an existing header of the same name.
if (token !== null && !req.headers.has(this.headerName)) {
req = req.clone({headers: req.headers.set(this.headerName, token)});
}
return next.handle(req);
}
}
如果我自己添加 X-XSRF-TOKEN HEADER 会怎样?
由于 angular 没有,我尝试通过如下修改请求 headers 将 header 附加到请求中:
const hds = new HttpHeaders()
.append('Authorization', 'Bearer ' + tokenData.access_token)
.append('Content-Type', 'application/x-www-form-urlencoded');
if (this.getCookie('XSRF-TOKEN') !== undefined) {
hds = hds.append('X-XSRF-TOKEN', this.getCookie('XSRF-TOKEN'));
}
其中 this.getCookie('XSRF-TOKEN')
是一种使用 'angular2-cookie/services'
读取浏览器 cookie 的方法,但是 this.getCookie('XSRF-TOKEN')
returns null.
为什么?据我了解,javascript cookie 检索失败,因为即使 XSRF-TOKEN cookie 在响应中由 Spring 返回,它也没有在浏览器中设置,因为它与客户端处于不同域(服务器域:10.0.0.143:8080
,客户端域:localhost:4200
)。
如果服务器 运行 也在本地主机上,即使在不同的端口上(即服务器域:localhost:8080
,客户端域:localhost:4200
), spring 服务器在响应中设置的 cookie 已在浏览器中正确设置,因此可以通过 angular 使用方法 this.getCookie('XSRF-TOKEN')
.
检索
看看我的意思,观察下图中两个不同调用的结果:
localhost and cross-domain POST requests, chrome inspection
如果我是正确的,这与域 localhost:4200
无法通过 javascript 域 10.0.0.143:8080
的 cookie 读取这一事实是一致的。请注意选项 withCredentials = true
允许 cookie 从服务器流向客户端,但只是透明的,这意味着它们不能通过 javascript 修改。只有服务器可以读写自己域的cookies。甚至客户端也可以,但前提是它 运行 与服务器位于同一域中(我说得对吗?)。
如果服务器和客户端 运行 在同一个域上,即使在不同的端口上,手动添加 header 也有效(但在生产服务器和客户端在不同的域上,所以这不是解决方案)。
问题是
目前的选项是:
如果我对机制的理解正确,Spring 和 Angular 如果客户端和服务器位于不同的域中,标准 CSRF 令牌交换机制将无法工作,因为 (1) angular 实现不支持它,并且 (2) javascript 无法访问 XSRF-TOKEN cookie,因为后者位于服务器的域中。如果是这种情况,我可以只依赖无状态 oauth2 refresh_token 和 access_token 安全性,而无需 CSRF 吗?从安全角度来看可以吗?
或者,也许,另一方面,我遗漏了一些东西,还有另一个我没有看到的原因(这就是我问你的原因,亲爱的开发人员)实际上 CSRF 和 CORS 应该工作,所以这是我的代码错误或遗漏了一些东西。
在这种情况下,你能告诉我你会怎么做吗?有没有电子我的代码中的 ror 使 cross-domain 场景中的 CSRF 不起作用?
如果您需要更多信息来问我的问题,请告诉我。
抱歉有点长,但我认为最好解释完整的解决方案,以便您理解我面临的问题。
此外,我编写和解释的代码可能对某些人有用。
此致,
詹卡洛
广告 2。您的代码完全有效,并且您正确设置了所有内容。 spring 中的 CSRF 保护设计为前端与后端位于同一域中。由于 Angular 无法访问 CSRF 数据,因此显然无法在修改请求中设置它。如果不在服务器过滤器中以常规 headers(不是 cookie)设置它们,就无法访问它们。
广告 1。 JWT 令牌的安全性足够好,因为大公司成功地使用了它们。但是,请记住,令牌本身应该使用 RSA 密钥(而不是更简单的 MAC 密钥)进行签名,并且所有通信 必须 通过安全连接(https/ssl)。使用刷新令牌总是会略微降低安全性。业务应用程序通常会省略它们。一般受众应用程序必须安全地存储它们,但可以选择在滥用情况下降低它们的有效性。
我正在开发一个基于 Spring 4.3 和 Angular (TypeScript) 4.3 的 client-server 应用程序,在 CORS 场景中,在生产环境中,服务器和客户端位于不同的域中。客户端通过 http 请求请求 REST 服务器 API。
1。 REST 和 OAUTH 配置:
服务器公开 REST API:
@RestController
@RequestMapping("/my-api")
public class MyRestController{
@RequestMapping(value = "/test", method = RequestMethod.POST)
public ResponseEntity<Boolean> test()
{
return new ResponseEntity<Boolean>(true, HttpStatus.OK);
}
}
如 spring 文档中所述,受 Oauth2 保护。显然我修改了上面的内容以适合我的应用程序。
一切正常:我可以通过 refresh_token 和 access_token 使用 Oauth2 保护 /my-api/test
。 Oauth2 没有问题。
2。 CORS 配置:
由于服务器相对于客户端位于单独的域中(服务器:10.0.0.143:8080
,客户端:localhost:4200
,正如我现在正在开发的那样),我需要在服务器端启用 CORS:
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
...
@Autowired
SimpleCorsFilter simpleCorsFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.addFilterAfter(simpleCorsFilter, CorsFilter.class)
.csrf().disable() // notice that now csrf is disabled
... (the rest of http security configuration follows)...
}
}
其中 SimpleCorsFilter 添加了我需要的 headers:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCorsFilter extends OncePerRequestFilter {
public SimpleCorsFilter() {
}
@Override
public void destroy() {
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
response.addHeader("Access-Control-Allow-Origin", "http://localhost:4200");
response.addHeader("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,PATCH,OPTIONS");
response.addHeader("Access-Control-Max-Age", "3600");
response.addHeader("Access-Control-Allow-Credentials", "true");
response.addHeader("Access-Control-Allow-Headers", "MyCustomHeader, Authorization, X-XSRF-TOKEN");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(request, response);
}
}
}
现在,如果我使用 angular2 发出 http get 或 post 请求,示例:
callPostMethod(tokenData) {
const url = 'my-api.domain.com/my-api';
const pars = new HttpParams();
const body = null;
let hds = new HttpHeaders()
.append('Authorization', 'Bearer ' + tokenData.access_token)
.append('Content-Type', 'application/x-www-form-urlencoded');
return this.http.post <Installation> (url, body, {
params : pars,
headers: hds,
withCredentials: true
});
}
一切正常。所以即使是 CORS 配置似乎也没问题。
3。 CSRF 配置:
如果我现在像这样在 Spring 中启用 CSRF 配置:
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
...
@Autowired
SimpleCorsFilter simpleCorsFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.addFilterAfter(simpleCorsFilter, CorsFilter.class)
.csrf().csrfTokenRepository(getCsrfTokenRepository()) // notice that csrf is now enabled
... (the rest of http security configuration follows)...
}
@Bean
@Autowired
// used only because I want to setCookiePath to /, otherwise I can simply use
// http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
public CsrfTokenRepository getCsrfTokenRepository() {
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
//tokenRepository.setHeaderName("X-XSRF-TOKEN"); // has already this name --> comment
tokenRepository.setCookiePath("/");
return tokenRepository;
}
}
在第一个 POST 请求中,它给了我 403 错误:
"Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'."
为什么会这样(据我了解..)?
通过探索 CSRF 的工作机制,我注意到 Spring 正确地生成了一个名为 XSRF-TOKEN 的 cookie,它是在响应 cookie 中设置的(通过使用 Chrome、Set-Cookie 下响应 Headers).
接下来应该发生的是 angular,在执行第一个 POST 请求时,应该读取从 Spring 收到的 cookie 并生成一个请求 Header 调用 X-XSRF-TOKEN,其值等于cookie的值
如果我检查失败的POST请求的Header,我看到没有X-XSRF-TOKEN,就像angular没有做出它应该做的按照它的 CSRF 规范来做,见图:
Failed CSRF request, Chrome inspector
查看xsrf (/angular/angular/blob/4.3.5/packages/common/http/src/xsrf.ts)的angular实现,你可以看到在HttpXsrfInterceptor中,如果没有添加csrf headers目标URL以http
开头(后面是angularxsrf.ts源码复制粘贴):
/**
* `HttpInterceptor` which adds an XSRF token to eligible outgoing requests.
*/
@Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor {
constructor(
private tokenService: HttpXsrfTokenExtractor,
@Inject(XSRF_HEADER_NAME) private headerName: string) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const lcUrl = req.url.toLowerCase();
// Skip both non-mutating requests and absolute URLs.
// Non-mutating requests don't require a token, and absolute URLs require special handling
// anyway as the cookie set
// on our origin is not the same as the token expected by another origin.
if (req.method === 'GET' || req.method === 'HEAD' || lcUrl.startsWith('http://') ||
lcUrl.startsWith('https://')) {
return next.handle(req);
}
const token = this.tokenService.getToken();
// Be careful not to overwrite an existing header of the same name.
if (token !== null && !req.headers.has(this.headerName)) {
req = req.clone({headers: req.headers.set(this.headerName, token)});
}
return next.handle(req);
}
}
如果我自己添加 X-XSRF-TOKEN HEADER 会怎样?
由于 angular 没有,我尝试通过如下修改请求 headers 将 header 附加到请求中:
const hds = new HttpHeaders()
.append('Authorization', 'Bearer ' + tokenData.access_token)
.append('Content-Type', 'application/x-www-form-urlencoded');
if (this.getCookie('XSRF-TOKEN') !== undefined) {
hds = hds.append('X-XSRF-TOKEN', this.getCookie('XSRF-TOKEN'));
}
其中 this.getCookie('XSRF-TOKEN')
是一种使用 'angular2-cookie/services'
读取浏览器 cookie 的方法,但是 this.getCookie('XSRF-TOKEN')
returns null.
为什么?据我了解,javascript cookie 检索失败,因为即使 XSRF-TOKEN cookie 在响应中由 Spring 返回,它也没有在浏览器中设置,因为它与客户端处于不同域(服务器域:10.0.0.143:8080
,客户端域:localhost:4200
)。
如果服务器 运行 也在本地主机上,即使在不同的端口上(即服务器域:localhost:8080
,客户端域:localhost:4200
), spring 服务器在响应中设置的 cookie 已在浏览器中正确设置,因此可以通过 angular 使用方法 this.getCookie('XSRF-TOKEN')
.
看看我的意思,观察下图中两个不同调用的结果:
localhost and cross-domain POST requests, chrome inspection
如果我是正确的,这与域 localhost:4200
无法通过 javascript 域 10.0.0.143:8080
的 cookie 读取这一事实是一致的。请注意选项 withCredentials = true
允许 cookie 从服务器流向客户端,但只是透明的,这意味着它们不能通过 javascript 修改。只有服务器可以读写自己域的cookies。甚至客户端也可以,但前提是它 运行 与服务器位于同一域中(我说得对吗?)。
如果服务器和客户端 运行 在同一个域上,即使在不同的端口上,手动添加 header 也有效(但在生产服务器和客户端在不同的域上,所以这不是解决方案)。
问题是
目前的选项是:
如果我对机制的理解正确,Spring 和 Angular 如果客户端和服务器位于不同的域中,标准 CSRF 令牌交换机制将无法工作,因为 (1) angular 实现不支持它,并且 (2) javascript 无法访问 XSRF-TOKEN cookie,因为后者位于服务器的域中。如果是这种情况,我可以只依赖无状态 oauth2 refresh_token 和 access_token 安全性,而无需 CSRF 吗?从安全角度来看可以吗?
或者,也许,另一方面,我遗漏了一些东西,还有另一个我没有看到的原因(这就是我问你的原因,亲爱的开发人员)实际上 CSRF 和 CORS 应该工作,所以这是我的代码错误或遗漏了一些东西。
在这种情况下,你能告诉我你会怎么做吗?有没有电子我的代码中的 ror 使 cross-domain 场景中的 CSRF 不起作用? 如果您需要更多信息来问我的问题,请告诉我。
抱歉有点长,但我认为最好解释完整的解决方案,以便您理解我面临的问题。 此外,我编写和解释的代码可能对某些人有用。
此致, 詹卡洛
广告 2。您的代码完全有效,并且您正确设置了所有内容。 spring 中的 CSRF 保护设计为前端与后端位于同一域中。由于 Angular 无法访问 CSRF 数据,因此显然无法在修改请求中设置它。如果不在服务器过滤器中以常规 headers(不是 cookie)设置它们,就无法访问它们。
广告 1。 JWT 令牌的安全性足够好,因为大公司成功地使用了它们。但是,请记住,令牌本身应该使用 RSA 密钥(而不是更简单的 MAC 密钥)进行签名,并且所有通信 必须 通过安全连接(https/ssl)。使用刷新令牌总是会略微降低安全性。业务应用程序通常会省略它们。一般受众应用程序必须安全地存储它们,但可以选择在滥用情况下降低它们的有效性。