Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(控制反转),DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
在项目的开发中,我们和前端联调的时候,经常会遇到跨域的问题,那跨域是什么?Spring Security
是怎么实现的呢?怎么去解决跨域的问题呢?
跨域是浏览器的同源策略,是浏览器限制脚本的跨域访问。在通常情况下请求是可以正常发起的,后端也进行相关的处理。只是在返回相应的数据的时候被浏览器拦截了,导致相应的内容不可使用。其中的一个场景就是CSRF攻击。
在进行跨域处理的时候,跨域的产生条件是什么呢?
不仅不同站点间的访问存在跨域问题, 同站点间的访问可能也会遇到跨域问题, 只要请求的URL与所在页面的URL首部不同即产生跨域。
请求的协议不同,如在http协议下访问https协议的资源时,会产生跨域问题
不同的域名访问会产生主机跨域
相同的域名,但是不同的端口会产生端口跨域
我们解决跨域问题有如下几种方式:
使用jsonp
使用nginx等网络代理进行转发请求
CROS处理等
JSONP(JSON With Padding)
是一种非官方的解决方案。 由于浏览器允许一些带src属性的标签跨域, 例如, iframe、 script、 img等, 所以JSONP
利用script标签可以实现跨域.
加入我们有如下场景:我们访问user/list的时候,后台返回的json对象的数组如下:
{
"code":"0000",
"message":"请求成功",
"success":true,
"data":[
{
"username":"tony",
"password":"asdfacasdradxx"
},
{
"username":"tony2",
"password":"asdfacasdradxx"
}
]
}
以上是获取用户的列表接口返回的结果。但在跨域的情况下,浏览器的同源策略导致用户请求无法响应信息,此时通过script标签去加载响应的接口
<script src="user/list?callback=jsonp"></script>
这样便可以成功获取响应信息了, 只是得到的JSON数据无法直接在JavaScript中使用。 如果后端介入, 那么在返回浏览器之前应将响应信息包装成JSONP
的形式,如下:
jsonp({
"code":"0000",
"message":"请求成功",
"success":true,
"data":[
{
"username":"tony",
"password":"asdfacasdradxx"
},
{
"username":"tony2",
"password":"asdfacasdradxx"
}
]
})
之后我们就可以通过调用JSONP
的方法进行返回值的处理了.就像调用javascript
函数一样调用。
?callback=jsonp
相当于请求之后调用回调方法jsonp,jsonp方法直接这样处理
window.jsonp = function(data){
console.log(data);
}
其中的data就是后台返回的相关数据。
注意:
JSONP只支持GET请求。
CORS(Cross-Origin Resource Sharing)
的规范中有一组新增的HTTP首部字段,允许服务器声明其提供的资源允许哪些站点跨域使用。通常情况下,跨域请求即便在不被支持的情况下, 服务器也会接收并进行处理, 在CORS
的规范中则避免了这个问题。 浏览器首先会发起一个请求方法为OPTIONS
的预检请求, 用于确认服务器是否允许跨域, 只有在得到许可后才会发出实际请求。 此外, 预检请求还允许服务器通知浏览器跨域携带身份凭证。
CORS新增的HTTP首部字段由服务器控制, 下面我们来看看常用的几个首部字段:
或*
:指被允许的站点,使用URL首部匹配原则*
:匹配所有站点, 表示允许来自所有域的请求。如果需要浏览器在发起请求时携带凭证信息, 则不允许设置为*
。如果设置了具体的站点信息, 则响应头中的 Vary
字段还需要携带Origin
属性,因为服务器对不同的域会返回不同的内容。Vary
:Accept-Encoding,Origintrue|false
true
时, 浏览器会在接下来的真实请求中携带用户凭证信息(cookie等),服务器也可以使用Set-Cookie向用户浏览器写入新的cookie。注意,使用AccessControl-Allow-Credentials时, Access-Control-Allow-Origin不应该设置为*
注意:
CORS不支持IE8以下版本的浏览器GET
请求、HEAD
请求,以及Content-Type
为application/x-www-form-urlencoded
、multipart/form-data
或text/plain
的POST
请求, 这类请求被称为简单请求。Origin
属性, 值为当前页面的 URL 首部。当服务器返回响应时,若存在跨域访问控制属性,则浏览器会通过这些属性判断本次请求是否被允许。如果允许,则跨域成功OPTIONS
请求到目标站点,以查明该请求是否安全,防止请求对目标站点的数据造成破坏。若是请求以GET
、HEAD
、POST
以外的方法发起;或者使用POST
方法,但请求数据为application/x-www-form-urlencoded
、multipart/form-data
和text/plain
以外的数据类型;再或者,使用了自定义请求头,则都会被当成预检请求类型处理。在Spring Security
中提供cors
的支持也很简单,只需要调整响应的配置类,创建相应的bean即可。下面就一起来看看。
pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/user/**").hasAnyRole("USER","ADMIN")
.antMatchers("/books/**").hasAnyRole("ADMIN")
.antMatchers("/").permitAll()
.and()
.formLogin()
.loginPage("/login.html")
.permitAll()
.and()
//启用
.cors();
}
CorsConfigurationSource
bean@Bean
public CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration configuration = new CorsConfiguration();
//允许google站点跨域
configuration.setAllowedOrigins(Arrays.asList("www.google.com"));
//允许跨域的防范
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
//是否允许携带凭证
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
//对所有的url开放
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",configuration);
return urlBasedCorsConfigurationSource;
}
以上是Spring Security
实现CORS
实现,是不是相当简单。
我们看到在http配置中,我们添加了一个cors()
方法进行处理。我们可以看看详细的细节。一起来看看。
public CorsConfigurer<HttpSecurity> cors() throws Exception {
return (CorsConfigurer)this.getOrApply(new CorsConfigurer());
}
由上面可以看出,会去获取一个CorsConfigurer
类去进行处理。
我们来进入CorsConfigurer
看看。
CorsConfigurer.class
:
public class CorsConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<CorsConfigurer<H>, H> {
public CorsConfigurer<H> configurationSource(CorsConfigurationSource configurationSource) {
this.configurationSource = configurationSource;
return this;
}
public void configure(H http) {
ApplicationContext context = (ApplicationContext)http.getSharedObject(ApplicationContext.class);
//获取corsFilter过滤器
CorsFilter corsFilter = this.getCorsFilter(context);
//请配置corsFilter bean或corsConfigurationSourcebean
Assert.state(corsFilter != null, () -> {
return "Please configure either a corsFilter bean or a corsConfigurationSourcebean.";
});
http.addFilter(corsFilter);
}
//获取CorsFilter的处理逻辑
private CorsFilter getCorsFilter(ApplicationContext context) {
//configurationSource不为空
if (this.configurationSource != null) {
//创建CorsFilter
return new CorsFilter(this.configurationSource);
} else {
boolean containsCorsFilter = context.containsBeanDefinition("corsFilter");
//如果包含corsFilter的bean,就直接返回
if (containsCorsFilter) {
return (CorsFilter)context.getBean("corsFilter", CorsFilter.class);
} else {
//否则就新建一个
boolean containsCorsSource = context.containsBean("corsConfigurationSource");
if (containsCorsSource) {
CorsConfigurationSource configurationSource = (CorsConfigurationSource)context.getBean("corsConfigurationSource", CorsConfigurationSource.class);
return new CorsFilter(configurationSource);
} else {
boolean mvcPresent = ClassUtils.isPresent("org.springframework.web.servlet.handler.HandlerMappingIntrospector", context.getClassLoader());
return mvcPresent ? CorsConfigurer.MvcCorsFilter.getMvcCorsFilter(context) : null;
}
}
}
}
static class MvcCorsFilter {
private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
MvcCorsFilter() {
}
private static CorsFilter getMvcCorsFilter(ApplicationContext context) {
if (!context.containsBean("mvcHandlerMappingIntrospector")) {
throw new NoSuchBeanDefinitionException("mvcHandlerMappingIntrospector", "A Bean named mvcHandlerMappingIntrospector of type " + HandlerMappingIntrospector.class.getName() + " is required to use MvcRequestMatcher. Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext.");
} else {
HandlerMappingIntrospector mappingIntrospector = (HandlerMappingIntrospector)context.getBean("mvcHandlerMappingIntrospector", HandlerMappingIntrospector.class);
return new CorsFilter(mappingIntrospector);
}
}
}
}
上面可以看出是通过过滤器进行处理,之后将过滤器添加到Spring Security
的过滤器链路中。
CorsFilter.class
:
private final CorsConfigurationSource configSource;
//DefaultCorsProcessor处理cors
private CorsProcessor processor = new DefaultCorsProcessor();
public CorsFilter(CorsConfigurationSource configSource) {
Assert.notNull(configSource, "CorsConfigurationSource must not be null");
this.configSource = configSource;
}
public void setCorsProcessor(CorsProcessor processor) {
Assert.notNull(processor, "CorsProcessor must not be null");
this.processor = processor;
}
从上面可以看出,是通过DefaultCorsProcessor
进行处理
DefaultCorsProcessor.class
:
public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException {
Collection<String> varyHeaders = response.getHeaders("Vary");
if (!varyHeaders.contains("Origin")) {
response.addHeader("Vary", "Origin");
}
if (!varyHeaders.contains("Access-Control-Request-Method")) {
response.addHeader("Vary", "Access-Control-Request-Method");
}
if (!varyHeaders.contains("Access-Control-Request-Headers")) {
response.addHeader("Vary", "Access-Control-Request-Headers");
}
if (!CorsUtils.isCorsRequest(request)) {
return true;
} else if (response.getHeader("Access-Control-Allow-Origin") != null) {
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
} else {
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
this.rejectRequest(new ServletServerHttpResponse(response));
return false;
} else {
return true;
}
} else {
return this.handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
}
}
}
//拒绝请求
protected void rejectRequest(ServerHttpResponse response) throws IOException {
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getBody().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));
response.flush();
}
//处理
protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response, CorsConfiguration config, boolean preFlightRequest) throws IOException {
//获取Origin
String requestOrigin = request.getHeaders().getOrigin();
//获取allowOrigin
String allowOrigin = this.checkOrigin(config, requestOrigin);
HttpHeaders responseHeaders = response.getHeaders();
if (allowOrigin == null) {
logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
this.rejectRequest(response);
return false;
} else {
//校验请求的方法
HttpMethod requestMethod = this.getMethodToUse(request, preFlightRequest);
List<HttpMethod> allowMethods = this.checkMethods(config, requestMethod);
if (allowMethods == null) {
logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
this.rejectRequest(response);
return false;
} else {
//请求中header列表
List<String> requestHeaders = this.getHeadersToUse(request, preFlightRequest);
//请求中允许的header列表
List<String> allowHeaders = this.checkHeaders(config, requestHeaders);
if (preFlightRequest && allowHeaders == null) {
logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
this.rejectRequest(response);
return false;
} else {
responseHeaders.setAccessControlAllowOrigin(allowOrigin);
if (preFlightRequest) {
responseHeaders.setAccessControlAllowMethods(allowMethods);
}
if (preFlightRequest && !allowHeaders.isEmpty()) {
responseHeaders.setAccessControlAllowHeaders(allowHeaders);
}
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
}
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
responseHeaders.setAccessControlAllowCredentials(true);
}
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
response.flush();
return true;
}
}
}
}
CSRF
的全称是(Cross Site Request Forgery) , 可译为跨域请求伪造,也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF。是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
CSRF攻击过程:
CSRF漏洞产生的原因主要是对用户请求缺少更安全的验证机制。防范CSRF漏洞的主要思路就是加强后台对用户及用户请求的验证,而不能仅限于cookie的识别。
遇到CRSF我们该如何防御呢?
CSRF 攻击完全是基于浏览器进行的,如果我们的系统前端并非在浏览器中运作,就应当关闭CSRF。
Spring Security通过注册一个CsrfFilter来专门处理CSRF攻击。
在相应的配置类中添加如下代码:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/user/**").hasAnyRole("USER","ADMIN")
.antMatchers("/books/**").hasAnyRole("ADMIN")
.antMatchers("/").permitAll()
.and()
.formLogin()
.loginPage("/login.html")
.permitAll()
.and()
//启用
.cors()
.and()
//开启csrf
.csrf();
}
}
csrf()
方法:
public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
ApplicationContext context = this.getContext();
return (CsrfConfigurer)this.getOrApply(new CsrfConfigurer(context));
}
从上面可以看出,是通过CsrfConfigurer
类进行处理
CsrfConfigurer.class
:
public void configure(H http) {
CsrfFilter filter = new CsrfFilter(this.csrfTokenRepository);
RequestMatcher requireCsrfProtectionMatcher = this.getRequireCsrfProtectionMatcher();
if (requireCsrfProtectionMatcher != null) {
filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
}
AccessDeniedHandler accessDeniedHandler = this.createAccessDeniedHandler(http);
if (accessDeniedHandler != null) {
filter.setAccessDeniedHandler(accessDeniedHandler);
}
LogoutConfigurer<H> logoutConfigurer = (LogoutConfigurer)http.getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null) {
logoutConfigurer.addLogoutHandler(new CsrfLogoutHandler(this.csrfTokenRepository));
}
SessionManagementConfigurer<H> sessionConfigurer = (SessionManagementConfigurer)http.getConfigurer(SessionManagementConfigurer.class);
if (sessionConfigurer != null) {
sessionConfigurer.addSessionAuthenticationStrategy(this.getSessionAuthenticationStrategy());
}
filter = (CsrfFilter)this.postProcess(filter);
http.addFilter(filter);
}
通过上面的类可以了解到,配置使用CsrfFilter
过滤器进行处理,并且使用默认的csrfTokenRepository
,这个属性值为LazyCsrfTokenRepository
private CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository());
LazyCsrfTokenRepository.class
类:
public final class LazyCsrfTokenRepository implements CsrfTokenRepository {
//响应属性
private static final String HTTP_RESPONSE_ATTR = HttpServletResponse.class.getName();
//代理类
private final CsrfTokenRepository delegate;
//构造器
public LazyCsrfTokenRepository(CsrfTokenRepository delegate) {
Assert.notNull(delegate, "delegate cannot be null");
this.delegate = delegate;
}
//生产csrfToken
public CsrfToken generateToken(HttpServletRequest request) {
return this.wrap(request, this.delegate.generateToken(request));
}
//保存csrfToken
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
if (token == null) {
this.delegate.saveToken(token, request, response);
}
}
//加载csrfToken
public CsrfToken loadToken(HttpServletRequest request) {
return this.delegate.loadToken(request);
}
//包含csrfToken
private CsrfToken wrap(HttpServletRequest request, CsrfToken token) {
HttpServletResponse response = this.getResponse(request);
return new LazyCsrfTokenRepository.SaveOnAccessCsrfToken(this.delegate, request, response, token);
}
private static final class SaveOnAccessCsrfToken implements CsrfToken {
private transient CsrfTokenRepository tokenRepository;
private transient HttpServletRequest request;
private transient HttpServletResponse response;
private final CsrfToken delegate;
SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository, HttpServletRequest request, HttpServletResponse response, CsrfToken delegate) {
this.tokenRepository = tokenRepository;
this.request = request;
this.response = response;
this.delegate = delegate;
}
public String getHeaderName() {
return this.delegate.getHeaderName();
}
public String getParameterName() {
return this.delegate.getParameterName();
}
public String getToken() {
this.saveTokenIfNecessary();
return this.delegate.getToken();
}
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj != null && this.getClass() == obj.getClass()) {
LazyCsrfTokenRepository.SaveOnAccessCsrfToken other = (LazyCsrfTokenRepository.SaveOnAccessCsrfToken)obj;
if (this.delegate == null) {
if (other.delegate != null) {
return false;
}
} else if (!this.delegate.equals(other.delegate)) {
return false;
}
return true;
} else {
return false;
}
}
public int hashCode() {
int prime = true;
int result = 1;
int result = 31 * result + (this.delegate == null ? 0 : this.delegate.hashCode());
return result;
}
public String toString() {
return "SaveOnAccessCsrfToken [delegate=" + this.delegate + "]";
}
private void saveTokenIfNecessary() {
if (this.tokenRepository != null) {
synchronized(this) {
if (this.tokenRepository != null) {
this.tokenRepository.saveToken(this.delegate, this.request, this.response);
this.tokenRepository = null;
this.request = null;
this.response = null;
}
}
}
}
}
}
SaveOnAccessCsrfToken
implements CsrfToken
Spring security中CsrfToken是一个用于描述Token值, 以及验证时应当获取哪个请求参数或请求头字段的接口。
public interface CsrfToken extends Serializable {
//获取header
String getHeaderName();
//获取参数名
String getParameterName();
//获取CsrfToken
String getToken();
}
CsrfTokenRepository
是实现CsrfToken
生成,保存以及加载操作
public interface CsrfTokenRepository {
//生成
CsrfToken generateToken(HttpServletRequest request);
//保存
void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);
//加载
CsrfToken loadToken(HttpServletRequest request);
}
在默认情况下,Spring Security
使用的是HttpSessionCsrfTokenRepository
.
HttpSessionCsrfTokenRepository.class
类:
public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
//默认的CSRF参数
private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
//默认的CSRF名
private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";
//默认的CSRFToken属性名
private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
private String parameterName = "_csrf";
private String headerName = "X-CSRF-TOKEN";
private String sessionAttributeName;
public HttpSessionCsrfTokenRepository() {
this.sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
}
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
HttpSession session;
if (token == null) {
session = request.getSession(false);
if (session != null) {
session.removeAttribute(this.sessionAttributeName);
}
} else {
session = request.getSession();
//将csrfToken与session关联
session.setAttribute(this.sessionAttributeName, token);
}
}
//加载CsrfToken
public CsrfToken loadToken(HttpServletRequest request) {
HttpSession session = request.getSession(false);
//从session中获取
return session == null ? null : (CsrfToken)session.getAttribute(this.sessionAttributeName);
}
//生成CsrfToken
public CsrfToken generateToken(HttpServletRequest request) {
//返回默认的DefaultCsrfToken实例
return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
}
//设置参数名
public void setParameterName(String parameterName) {
Assert.hasLength(parameterName, "parameterName cannot be null or empty");
this.parameterName = parameterName;
}
//设置header
public void setHeaderName(String headerName) {
Assert.hasLength(headerName, "headerName cannot be null or empty");
this.headerName = headerName;
}
//设置session属性名
public void setSessionAttributeName(String sessionAttributeName) {
Assert.hasLength(sessionAttributeName, "sessionAttributename cannot be null or empty");
this.sessionAttributeName = sessionAttributeName;
}
//创建新的token
private String createNewToken() {
return UUID.randomUUID().toString();
}
}
HttpSessionCsrfTokenRepository
将CsrfToken
值存储在HttpSession
中,并指定前端把CsrfToken
值放在名为_csrf
的请求参数或名为X-CSRF-TOKEN
的请求头字段里(可以调用相应的设置方法来重新设定)。校验时,通过对比HttpSession内存储的CsrfToken值与前端携带的CsrfToken值是否一致,便能断定本次请求是否为CSRF攻击。
CsrfTokenRepository
接口的另外一个实现是基于cookie的。
CookieCsrfTokenRepository.class
类:
public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
private String parameterName = "_csrf";
private String headerName = "X-XSRF-TOKEN";
private String cookieName = "XSRF-TOKEN";
private boolean cookieHttpOnly = true;
private String cookiePath;
private String cookieDomain;
private Boolean secure;
private int cookieMaxAge = -1;
public CookieCsrfTokenRepository() {
}
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
}
//保存CsrfToken
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
String tokenValue = token != null ? token.getToken() : "";
//将CsrfToken与cookie关联 以及设置cookie的参数
Cookie cookie = new Cookie(this.cookieName, tokenValue);
cookie.setSecure(this.secure != null ? this.secure : request.isSecure());
cookie.setPath(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request));
cookie.setMaxAge(token != null ? this.cookieMaxAge : 0);
cookie.setHttpOnly(this.cookieHttpOnly);
if (StringUtils.hasLength(this.cookieDomain)) {
cookie.setDomain(this.cookieDomain);
}
response.addCookie(cookie);
}
//加载CsrfToken
public CsrfToken loadToken(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
if (cookie == null) {
return null;
} else {
String token = cookie.getValue();
return !StringUtils.hasLength(token) ? null : new DefaultCsrfToken(this.headerName, this.parameterName, token);
}
}
public void setParameterName(String parameterName) {
Assert.notNull(parameterName, "parameterName cannot be null");
this.parameterName = parameterName;
}
public void setHeaderName(String headerName) {
Assert.notNull(headerName, "headerName cannot be null");
this.headerName = headerName;
}
public void setCookieName(String cookieName) {
Assert.notNull(cookieName, "cookieName cannot be null");
this.cookieName = cookieName;
}
public void setCookieHttpOnly(boolean cookieHttpOnly) {
this.cookieHttpOnly = cookieHttpOnly;
}
private String getRequestContext(HttpServletRequest request) {
String contextPath = request.getContextPath();
return contextPath.length() > 0 ? contextPath : "/";
}
public static CookieCsrfTokenRepository withHttpOnlyFalse() {
CookieCsrfTokenRepository result = new CookieCsrfTokenRepository();
result.setCookieHttpOnly(false);
return result;
}
private String createNewToken() {
return UUID.randomUUID().toString();
}
public void setCookiePath(String path) {
this.cookiePath = path;
}
public String getCookiePath() {
return this.cookiePath;
}
public void setCookieDomain(String cookieDomain) {
this.cookieDomain = cookieDomain;
}
public void setSecure(Boolean secure) {
this.secure = secure;
}
public void setCookieMaxAge(int cookieMaxAge) {
Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero");
this.cookieMaxAge = cookieMaxAge;
}
}
cookie只有在同域的情况下才能被读取, 所以杜绝了第三方站点跨域获取CsrfToken
值的可能。CSRF
攻击本身是不知道cookie内容的,只是利用了当请求自动携带cookie时可以通过身份验证的漏洞。但服务器对CsrfToken值的校验并非取自cookie,而是需要前端手动将CsrfToken值作为参数携带在请求里,所以cookie内的CsrfToken值并没有被校验的作用, 仅仅作为一个存储容器使用.
CsrfToken
相关的管理已经了解了,我们实现的时候是通过CsrfFilter
处理的。
CsrfFilter.class
类:
public final class CsrfFilter extends OncePerRequestFilter {
public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfFilter.DefaultRequiresCsrfMatcher();
private static final String SHOULD_NOT_FILTER = "SHOULD_NOT_FILTER" + CsrfFilter.class.getName();
private final Log logger = LogFactory.getLog(this.getClass());
private final CsrfTokenRepository tokenRepository;
private RequestMatcher requireCsrfProtectionMatcher;
private AccessDeniedHandler accessDeniedHandler;
public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
this.accessDeniedHandler = new AccessDeniedHandlerImpl();
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
this.tokenRepository = csrfTokenRepository;
}
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return Boolean.TRUE.equals(request.getAttribute(SHOULD_NOT_FILTER));
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
//加载csrfToken
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
//如果不存在就生成
csrfToken = this.tokenRepository.generateToken(request);
//保存csrfToken
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not protect against CSRF since request did not match " + this.requireCsrfProtectionMatcher);
}
filterChain.doFilter(request, response);
} else {
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
this.logger.debug(LogMessage.of(() -> {
return "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request);
}));
AccessDeniedException exception = !missingToken ? new InvalidCsrfTokenException(csrfToken, actualToken) : new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception);
} else {
filterChain.doFilter(request, response);
}
}
}
public static void skipRequest(HttpServletRequest request) {
request.setAttribute(SHOULD_NOT_FILTER, Boolean.TRUE);
}
public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
}
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
this.accessDeniedHandler = accessDeniedHandler;
}
private static boolean equalsConstantTime(String expected, String actual) {
if (expected == actual) {
return true;
} else if (expected != null && actual != null) {
byte[] expectedBytes = Utf8.encode(expected);
byte[] actualBytes = Utf8.encode(actual);
return MessageDigest.isEqual(expectedBytes, actualBytes);
} else {
return false;
}
}
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
//允许的方法
private final HashSet<String> allowedMethods;
private DefaultRequiresCsrfMatcher() {
//默认的允许方法
this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
}
public boolean matches(HttpServletRequest request) {
//方法的校验
return !this.allowedMethods.contains(request.getMethod());
}
public String toString() {
return "CsrfNotRequired " + this.allowedMethods;
}
}
}
CsrfFilter
的处理流程很清晰,当一个请求到达时,首先会调用csrfTokenRepository的loadToken方法加载该会话的CsrfToken
值。如果加载不到,则证明请求是首次发起的,应该生成并保存一个新的CsrfToken
值。如果可以加载到CsrfToken
值, 那么先排除部分不需要验证CSRF攻击的请求方法(默认忽略了GET、 HEAD、 TRACE和OPTIONS) 。
Spring Security
还提供了一个LazyCsrfTokenRepository
,用来延时保存CsrfToken值(允许创建,但只有真正使用时才会被保存)