18.SpringSecurity-web权限方案-CSRF功能

CSRF 理解

  跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

  跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。

  这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

  从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。

案例

  SecurityConfig:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    UserDetailsService userDetailsService;

    //实现用户身份认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //配置url的访问权限
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/**update**").permitAll()
                .antMatchers("/login/**").permitAll()
                .anyRequest().authenticated();

        //关闭csrf保护功能
        //http.csrf().disable();
//       
        //使用自定义的登录窗口
      http.formLogin()
              .loginPage("/userLogin").permitAll()
              .usernameParameter("username").passwordParameter("password")
              .defaultSuccessUrl("/")
              .failureUrl("/userLogin?error");
    }
} 

  controller:

@Controller
public class CSRFController {

     @GetMapping("/toupdate")
     public String test(Model model){
         return "csrf/csrfTest";
     }

    @PostMapping("/update_token")
    public String getToken() {
        return "csrf/csrf_token";
    }
}
@Controller
public class LoginController {
    @GetMapping("/userLogin")
    public String login(){
        return "login/login";
    }
}

  UserDetailsServiceImpl:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

            List<SimpleGrantedAuthority> list = new ArrayList<>();
            list.add(new SimpleGrantedAuthority("role"));
            UserDetails userDetails = new User("lisi", new BCryptPasswordEncoder().encode("996")
                    , list);
            return userDetails;
    }
}

  pom:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.3.0.RELEASEversion>
        <relativePath/> 
    parent>
    <groupId>com.zscgroupId>
    <artifactId>ch07-securityartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>ch07-securityname>
    <description>Demo project for Spring Bootdescription>

    <properties>
        <java.version>1.8java.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-thymeleafartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-configuration-processorartifactId>
            <optional>trueoptional>
        dependency>

        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-testartifactId>
            <scope>testscope>
        dependency>

        
        <dependency>
            <groupId>org.thymeleaf.extrasgroupId>
            <artifactId>thymeleaf-extras-springsecurity5artifactId>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>

        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druid-spring-boot-starterartifactId>
            <version>1.1.14version>
        dependency>
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.1.2version>
        dependency>
    dependencies>
project>

  登录html:

DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户修改title>
head>
<body>
<div align="center">
    <form  method="post" action="update_token">
      <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
        用户名: <input type="text" name="username" /><br />  码: <input type="password" name="password" /><br />
        <button type="submit">修改button>
    form>
div>
body>

  token展示html:

DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户修改title>
head>
<body>
<div>
    <span th:text="${_csrf.token}">span>
div>

body>
html>

  测试:

  客户端访问的时候会进行认证,认证过程中生成 csrfToken 保存到 HttpSession 或者 Cookie 中,再次进行访问的时候带着token字符串和session中存储的token进行比较,一致的情况下才允许访问。

源码

  CsrfFilter:这是做csrf防护的过滤器,核心是doFilterInternal方法

@Override
protected void doFilterInternal(HttpServletRequest request,
      HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
   request.setAttribute(HttpServletResponse.class.getName(), response);
   //生成一个Token,并存到session中
   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);
   //csrfToken.getParameterName(),获取表单中传进来的Token值
   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);
}

  注意看这段代码:

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

  比对的时候使用的是requireCsrfProtectionMatcher

public final class CsrfFilter 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 DefaultRequiresCsrfMatcher();

   /**
    * The attribute name to use when marking a given request as one that should not be filtered.
    *
    * To use, set the attribute on your {@link HttpServletRequest}:
    * 
    *     CsrfFilter.skipRequest(request);
    * 
*/
private static final String SHOULD_NOT_FILTER = "SHOULD_NOT_FILTER" + CsrfFilter.class.getName(); private final Log logger = LogFactory.getLog(getClass()); private final CsrfTokenRepository tokenRepository; private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER; private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl(); }

  我们往翻发现它的实现实际上是DefaultRequiresCsrfMatcher,而防护的时候下面的这些请求是不做防护的;

private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
   private final HashSet<String> allowedMethods = new HashSet<>(
         Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));

你可能感兴趣的:(Spring,Security,OAuth2.0,java)