《Java实战开发》利用spring-security解决CSRF问题,通过重写CsrfFilter 过滤掉指定方法

最近项目渗透测试检测出一些安全问题其中一项为csrf攻击隐患,然后开始修复

csrf简介

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。——百度百科

详细介绍:浅谈CSRF攻击方式 - hyddd - 博客园

一、maven pom.xml 引入jar包

        <dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-web</artifactId>
			<version>5.0.6.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-config</artifactId>
			<version>5.0.6.RELEASE</version>
		</dependency>

二、web.xml配置filter

mgmtCsrfFilter是我重写的csrfFilter名称

<!-- CSRF filter -->
	<filter>
		<filter-name>mgmtCsrfFilter</filter-name>
		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>mgmtCsrfFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

 三、spring-application.xml配置

<bean id="mgmtCsrfFilter" class="com.haha.sps.mgmt.core.filter.MgmtCsrfFilter">
		<constructor-arg>
			<bean class="org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository" />
		</constructor-arg>
	</bean>
	<bean id="requestDataValueProcessor" class="org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor"></bean>

四、重写csrffilter

重写后的csrfFilter为MgmtCsrfFilter代码如下:

package com.haha.sps.mgmt.core.filter;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;

/**
 * Rewrite  org.springframework.security.web.csrf.CsrfFilter 
 * exclude user/doLogion method filter
 *
 * @author Pente
 */
public final class MgmtCsrfFilter extends OncePerRequestFilter {
	/**
	 * The default {@link RequestMatcher} that indicates if CSRF protection is required or
	 * not. The default is to ignore GET, HEAD, TRACE, OPTIONS and process all other
	 * requests.
	 */
	public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfSecurityRequestMatcher();

	private final Log logger = LogFactory.getLog(getClass());
	private final CsrfTokenRepository tokenRepository;
	private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
	private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();

	public MgmtCsrfFilter(CsrfTokenRepository csrfTokenRepository) {
		Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
		this.tokenRepository = csrfTokenRepository;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * org.springframework.web.filter.OncePerRequestFilter#doFilterInternal(javax.servlet
	 * .http.HttpServletRequest, javax.servlet.http.HttpServletResponse,
	 * javax.servlet.FilterChain)
	 */
	@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
					throws ServletException, IOException {
		request.setAttribute(HttpServletResponse.class.getName(), response);

		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		final boolean missingToken = csrfToken == null;
		if (missingToken) {
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);

		if (!this.requireCsrfProtectionMatcher.matches(request)) {
			filterChain.doFilter(request, response);
			return;
		}

		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (actualToken == null) {
			actualToken = request.getParameter(csrfToken.getParameterName());
		}
		if (!csrfToken.getToken().equals(actualToken)) {
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("Invalid CSRF token found for "
						+ UrlUtils.buildFullRequestUrl(request));
			}
			if (missingToken) {
				this.accessDeniedHandler.handle(request, response,
						new MissingCsrfTokenException(actualToken));
			}
			else {
				this.accessDeniedHandler.handle(request, response,
						new InvalidCsrfTokenException(csrfToken, actualToken));
			}
			return;
		}

		filterChain.doFilter(request, response);
	}

	/**
	 * Specifies a {@link RequestMatcher} that is used to determine if CSRF protection
	 * should be applied. If the {@link RequestMatcher} returns true for a given request,
	 * then CSRF protection is applied.
	 *
	 * <p>
	 * The default is to apply CSRF protection for any HTTP method other than GET, HEAD,
	 * TRACE, OPTIONS.
	 * </p>
	 *
	 * @param requireCsrfProtectionMatcher the {@link RequestMatcher} used to determine if
	 * CSRF protection should be applied.
	 */
	public void setRequireCsrfProtectionMatcher(
			RequestMatcher requireCsrfProtectionMatcher) {
		Assert.notNull(requireCsrfProtectionMatcher,
				"requireCsrfProtectionMatcher cannot be null");
		this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
	}

	/**
	 * Specifies a {@link AccessDeniedHandler} that should be used when CSRF protection
	 * fails.
	 *
	 * <p>
	 * The default is to use AccessDeniedHandlerImpl with no arguments.
	 * </p>
	 *
	 * @param accessDeniedHandler the {@link AccessDeniedHandler} to use
	 */
	public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
		Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
		this.accessDeniedHandler = accessDeniedHandler;
	}

	/*
	 * exclude "user/doLogion"  "GET", "HEAD", "TRACE", "OPTIONS"
	 */
	private static final class CsrfSecurityRequestMatcher implements RequestMatcher {
	    private RegexRequestMatcher unprotectedMatcher = new RegexRequestMatcher("^/user/doLogin", null);

		private final HashSet<String> allowedMethods = new HashSet<String>(
				Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));

		/*
		 * (non-Javadoc)
		 * @see
		 * org.springframework.security.web.util.matcher.RequestMatcher#matches(javax.
		 * servlet.http.HttpServletRequest)
		 */
		@Override
		public boolean matches(HttpServletRequest request) {
			  if(this.allowedMethods.contains(request.getMethod())){
		            return false;
		        }
		        return !unprotectedMatcher.matches(request);
		}
		
	}
}

 实现自定义过滤掉登录方法,主要是重写csrfFilter里的DefaultRequiresCsrfMatcher替换成自己写的CsrfSecurityRequestMatcher ,如下:

/*
	 * exclude "user/doLogion"  "GET", "HEAD", "TRACE", "OPTIONS"
	 */
	private static final class CsrfSecurityRequestMatcher implements RequestMatcher {
	    private RegexRequestMatcher unprotectedMatcher = new RegexRequestMatcher("^/user/doLogin", null);

		private final HashSet<String> allowedMethods = new HashSet<String>(
				Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));

		/*
		 * (non-Javadoc)
		 * @see
		 * org.springframework.security.web.util.matcher.RequestMatcher#matches(javax.
		 * servlet.http.HttpServletRequest)
		 */
		@Override
		public boolean matches(HttpServletRequest request) {
			  if(this.allowedMethods.contains(request.getMethod())){
		            return false;
		        }
		        return !unprotectedMatcher.matches(request);
		}
		
	}

五、页面配置

在post请求或ajax请求中会遇到问题,在公用jsp文件中加上,例如每个页面都会引用的头文件top.jsp

<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
 
<script>
 
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $.ajaxSetup({
        beforeSend: function (xhr) {
            if(header && token ){
                xhr.setRequestHeader(header, token);
            }
        }}
    );
// $(document).ajaxSend(function(e,xhr,opt){
// 	xhr.setRequestHeader(header,token);
// });

</script>

$.ajaxSetup的意思就是给我们所有的请求都加上这个header和token,或者放到form表单中。注意,_csrf这个要与spring security的配置文件中的配置相匹配,默认为_csrf

六、源码解析

看一下CsrfFilter中的doFilterInternal源码解析就知道我们为什么这么修改了

@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
					throws ServletException, IOException {
		request.setAttribute(HttpServletResponse.class.getName(), response);
		// 先从tokenRepository中加载token
		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		final boolean missingToken = csrfToken == null;
		  // 如果为空,则tokenRepository生成新的token,并保存到tokenRepository中
		if (missingToken) {
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}
        // 将token写入request的attribute中,方便页面上使用
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);
		//这个macher就是我们在Spring配置文件中自定义的过滤器,也就是GET,HEAD, TRACE, OPTIONS和我们的rest都不处理

        // 这个macher就是我们在Spring配置文件中自定义的过滤器,
		//如果不需要csrf验证的请求,则直接下传请求(requireCsrfProtectionMatcher是默认的对象,对符合^(GET|HEAD|TRACE|OPTIONS)$的请求和我们自定义的请求不验证)
		if (!this.requireCsrfProtectionMatcher.matches(request)) {
			filterChain.doFilter(request, response);
			return;
		}
        // 从用户请求中获取token信息
		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (actualToken == null) {
			actualToken = request.getParameter(csrfToken.getParameterName());
		}
        // 验证,如果相同,则下传请求,如果不同,则抛出异常
		if (!csrfToken.getToken().equals(actualToken)) {
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("Invalid CSRF token found for "
						+ UrlUtils.buildFullRequestUrl(request));
			}
			if (missingToken) {
				this.accessDeniedHandler.handle(request, response,
						new MissingCsrfTokenException(actualToken));
			}
			else {
				this.accessDeniedHandler.handle(request, response,
						new InvalidCsrfTokenException(csrfToken, actualToken));
			}
			return;
		}

		filterChain.doFilter(request, response);
	}

文章结构部分参考:利用spring-security解决CSRF问题_Frankenstein的博客-CSDN博客

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以通过在Spring Security中配置一个Token认证过滤器来实现基于WebSocket的Token认证。具体步骤如下: 1. 创建一个TokenAuthenticationFilter类,继承自OncePerRequestFilter并实现doFilterInternal方法。该类负责检查请求中是否包含有效的Token,并进行相应的认证处理。 ```java public class TokenAuthenticationFilter extends OncePerRequestFilter { private final TokenProvider tokenProvider; public TokenAuthenticationFilter(TokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = getTokenFromRequest(request); if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) { Authentication authentication = tokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String getTokenFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } ``` 2. 创建一个TokenProvider类,用于生成Token和验证Token的有效性,并根据Token获取用户信息。 ```java @Component public class TokenProvider { private static final String SECRET_KEY = "my-secret-key"; private static final long EXPIRATION_TIME = 86400000; // 1 day public String generateToken(Authentication authentication) { UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION_TIME); return Jwts.builder() .setSubject(Long.toString(principal.getId())) .setIssuedAt(new Date()) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token); return true; } catch (Exception e) { return false; } } public Authentication getAuthentication(String token) { Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); Long userId = Long.parseLong(claims.getSubject()); UserPrincipal principal = new UserPrincipal(userId); return new UsernamePasswordAuthenticationToken(principal, "", principal.getAuthorities()); } } ``` 3. 在配置类中注册TokenAuthenticationFilterTokenProvider,并将TokenAuthenticationFilter添加到Spring Security过滤器链中。 ```java @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Autowired private TokenProvider tokenProvider; @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS(); } @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(new ChannelInterceptorAdapter() { @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (StompCommand.CONNECT.equals(accessor.getCommand())) { String token = accessor.getFirstNativeHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); TokenAuthenticationFilter filter = new TokenAuthenticationFilter(tokenProvider); SecurityContextHolder.getContext().setAuthentication(filter.getAuthentication(token)); } } return message; } }); } @Override public void configureClientOutboundChannel(ChannelRegistration registration) { } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { } @Override public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) { } @Override public boolean configureMessageConverters(List<MessageConverter> messageConverters) { return true; } @Override public void configureWebSocketTransport(WebSocketTransportRegistration registry) { } @Bean public TokenAuthenticationFilter tokenAuthenticationFilter() throws Exception { TokenAuthenticationFilter filter = new TokenAuthenticationFilter(tokenProvider); filter.setAuthenticationManager(authenticationManager()); return filter; } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable().authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override @Bean public UserDetailsService userDetailsService() { return new UserDetailsServiceImpl(); } } ``` 在上述代码中,我们通过重写configureClientInboundChannel方法,在连接到WebSocket时获取请求中的Token,并使用TokenAuthenticationFilter进行认证。注意,我们需要将TokenAuthenticationFilter添加到Spring Security过滤器链中,以便它能够在WebSocket连接期间对请求进行拦截。 最后,我们需要在客户端的连接请求中添加Authorization头部,以便在服务端进行Token认证。例如: ```javascript stompClient.connect({}, function(frame) { console.log('Connected: ' + frame); stompClient.subscribe('/topic/greetings', function(greeting) { showGreeting(JSON.parse(greeting.body).content); }); }, function(error) { console.log('Error: ' + error); }, {"Authorization": "Bearer " + token}); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值