Spring + Angular 2 + Oauth2 + CORS 的 CSRF 问题
Posted
技术标签:
【中文标题】Spring + Angular 2 + Oauth2 + CORS 的 CSRF 问题【英文标题】:CSRF issue with Spring + Angular 2 + Oauth2 + CORS 【发布时间】:2018-01-30 16:37:46 【问题描述】:我正在开发一个基于 Spring 4.3 和 Angular (TypeScript) 4.3 的客户端-服务器应用程序,在 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);
受 Oauth2 保护,如 spring 文档中所述。显然我修改了上面的内容以适应我的应用程序。
一切正常:我可以通过 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 添加我需要的标题的地方:
@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 &#39;null&#39; was found on the request parameter &#39;_csrf&#39; or header &#39;X-XSRF-TOKEN&#39;."
为什么会发生这种情况(据我所知..)? 通过探索 CSRF 的工作机制,我注意到 Spring 正确地生成了一个名为 XSRF-TOKEN 的 cookie,该 cookie 设置在响应 cookie 中(通过使用 Chrome 检查请求可见,响应标头下的 Set-Cookie)。
接下来应该发生的是,Angular 在执行第一个 POST 请求时,应该读取从 Spring 接收到的 cookie 并生成一个名为 X-XSRF-TOKEN 的请求 Header,其值等于cookie 的值。
如果我检查失败的 POST 请求的 Header,我会看到没有 X-XSRF-TOKEN,就像 angular 没有按照它的 CSRF 规范做它应该做的事情,见图:
Failed CSRF request, Chrome inspector
查看 xsrf 的 Angular 实现 (/angular/angular/blob/4.3.5/packages/common/http/src/xsrf.ts) 可以看到,在 HttpXsrfInterceptor 中,如果目标 URL 开始,则不会添加 csrf 标头用http
(下面是角xsrf.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 标头会怎样? 由于 Angular 没有,我尝试通过如下修改请求标头将标头附加到请求中:
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')
返回null。
为什么?据我了解, javascript cookie 检索失败是因为,即使 Spring 在响应中返回 XSRF-TOKEN cookie,它也没有在浏览器中设置,因为它相对于客户端(服务器)位于不同的域中域:10.0.0.143:8080
,客户域:localhost:4200
)。
如果服务器也运行在本地主机上,即使在不同的端口上(即服务器域:localhost:8080
,客户端域:localhost:4200
),响应中从 spring 服务器设置的 cookie 设置正确浏览器,因此可以通过角度检索 使用方法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。甚至客户端也可以,但前提是它与服务器在同一个域中运行(我正确吗?)。
相反,如果服务器和客户端都在同一个域上运行,即使在不同的端口上,手动添加标头也可以(但在生产服务器和客户端位于不同的域中,所以这不是解决方案)。
所以问题是
目前的选择是:
-
1234563到 XSRF-TOKEN cookie,因为后者在服务器的域上。如果是这种情况,我是否可以仅依靠无状态 oauth2 refresh_token 和 access_token 安全性,而无需 CSRF?从安全角度来看可以吗?
或者,另一方面,我遗漏了一些东西,还有另一个我看不到的原因(这就是我问你的原因,亲爱的开发人员),实际上 CSRF 和 CORS 应该可以工作,所以这是我的代码错误或缺少某些内容。
鉴于这种情况,你能告诉我你会怎么做吗?我的代码中是否有一些错误导致跨域场景中的 CSRF 不起作用? 如果您需要更多信息来提出我的问题,请告诉我。
对不起,有点长,但我认为最好解释完整的解决方案,以便让您了解我面临的问题。 此外,我编写和解释的代码可能对某些人有用。
最好的问候, 詹卡洛
【问题讨论】:
【参考方案1】:广告 2。您的代码完全有效,并且您正确设置了所有内容。 Spring中的CSRF保护设计为前端与后端位于同一域中。由于 Angular 无法访问 CSRF 数据,因此显然无法在修改请求中设置它。如果不在常规标头(不是 cookie)的服务器过滤器中设置它们,就无法访问它们。
广告 1。 JWT 代币的安全性足够好,因为大公司成功使用它们。但是,请记住,令牌本身应使用 RSA 密钥(而不是更简单的 MAC 密钥)进行签名,并且所有通信必须通过安全连接 (https/ssl)。使用刷新令牌总是会稍微降低安全性。业务应用程序通常会忽略它们。一般受众应用程序必须安全地存储它们,但仍然可以选择在被滥用的情况下放弃它们的有效性。
【讨论】:
亲爱的 Axinet,感谢您的回答。你能解释一下为什么使用刷新令牌会降低安全性吗?我会说每次将刷新令牌而不是用户名和密码传递给服务器更安全(显然我错过了一些东西:))。如果我不使用刷新令牌,当访问令牌过期时,我需要通过将用户名和密码传递给服务器来请求一个新令牌。相反,如果我使用刷新令牌,我可以通过将刷新令牌本身传递给服务器来更新访问令牌(它有一个到期日期,而用户名和密码没有)。你能告诉我我错过了什么吗? 抱歉回复晚了@gc.mnt,我错过了评论通知。通过 SSL 发送刷新令牌和发送用户和密码同样(不)安全。但是,当您发出刷新令牌时,它必须被安全地存储,因为当泄漏时,它允许(通过访问令牌)获得对受限区域的访问权限。通常,您不需要在应用程序中存储用户名和密码,但您要求用户提供它们,并且他有责任安全地存储它。请注意,为了提高安全性,访问令牌是短暂的,因此长期刷新令牌显然是相反的;)。以上是关于Spring + Angular 2 + Oauth2 + CORS 的 CSRF 问题的主要内容,如果未能解决你的问题,请参考以下文章
Oaut2RestTemplate 与 client_credentials 错误(不允许匿名)
Spring Security - 用于多租户应用程序的 OAuth、LDAP 集成
Angular 2 + CORS + 带有 Spring Security 的 Spring Boot Rest Controller