我正在开发基于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保护。显然我修改了上面的内容以适合我的应用程序。
一切正常:我可以通过refresh_token和access_token保护/my-api/test
和Oauth2。 Oauth2没问题。
的 2。 CORS CONFIGURATION:
由于服务器位于相对于客户端的单独域(服务器: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 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'."
为什么会发生这种情况(据我所知......)?
通过探索CSRF如何工作的机制,我注意到Spring正确生成了一个名为 XSRF-TOKEN 的cookie,该cookie在响应cookie中设置(通过检查请求与Chrome,Set-Cookie在响应标头下可见)。
接下来应该发生的是,当执行第一个POST请求时,应该读取从Spring收到的cookie并生成一个名为 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),您可以看到在HttpXsrfInterceptor中,如果目标URL启动,则不会添加csrf头文件使用http
(以下是angular 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 HEADER MYSELF怎么办?
由于角度没有,我尝试通过修改请求标头来自己将标头附加到请求中,如下所示:
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运行,即使在不同的端口(即服务器域: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进行修改。只有服务器才能读写自己域的cookie。甚至客户端也可以,但前提是它与服务器在同一个域中运行(我是否正确?)。
如果服务器和客户端都在同一个域上运行,即使在不同的端口上运行,手动标头添加也会起作用(但是在生产服务器和客户端中位于不同的域,因此这不是解决方案)。
问题是
目前的选项是:
如果我理解正确的机制,如果客户端和服务器在不同的域上,则Spring和Angular标准的CSRF令牌交换机制无法工作,因为(1)角度实现不支持它和(2)javascript无法访问到XSRF-TOKEN cookie,因为后者位于服务器的域上。如果是这种情况,我可以依靠无状态oauth2 refresh_token和access_token安全性,而不使用CSRF吗?从安全角度来看是否可以?
或者,另一方面,我错过了一些东西,还有另一个原因我看不到(这就是我问你的原因,亲爱的开发人员),实际上CSRF和CORS应该有效,所以这是我的代码错了或遗漏了什么。
鉴于这种情况,你能告诉我你会做什么吗?我的代码中是否存在一些错误,这使得跨域方案的CSRF不起作用? 如果您需要其他信息来询问我的问题,请与我们联系。
抱歉有点长,但我认为最好先解释完整的解决方案,以便让您了解我所面临的问题。 此外,我编写和解释的代码对某些人有用。
祝你好运, 吉安卡洛
答案 0 :(得分:2)
Ad.2。您的代码完全有效,您可以正确设置所有内容。春天的CSRF保护设计与前端在同一领域。由于Angular无法访问CSRF数据,因此显然无法在修改请求中设置它。如果没有在常规标题(而不是cookie)中将它们设置在服务器过滤器中,则无法访问它们。
Ad.1。 JWT令牌的安全性足够好,因为大公司成功使用它们。但是,请记住,令牌本身应使用RSA密钥(不是更简单的MAC密钥)进行签名,以及所有通信必须通过安全连接(https / ssl)。使用刷新令牌总是会略微降低安全性。业务应用程序通常会省略它们。一般受众应用程序必须安全地存储它们,然而可以选择在滥用时放弃它们的有效性。