spring.security 禁止 curl post 请求

问题描述

我使用 SSM 框架开发了一个演示网站,并使用 spring.security 进行身份验证。我可以使用 post 请求登录站点并使用 get 请求获取数据。但是,我无法使用 post 请求添加数据。它总是被禁止的。如果禁用CSRF,则可以。我尝试了以下方法,但都不起作用。

  1. 标题添加“X-CSRF-TOKEN:CSRF 值”。
  2. 标题添加“_csrf:CSRF 值”。
  3. 将 _csrf 添加到发布请求正文。

那么如何在启用 CSRF 的情况下使 post 请求工作?另外,我不确定是否需要使用不同的帖子重新生成 CSRF 令牌。奇怪的是,登录帖子请求有效。

  • application.properties

     spring.datasource.url=jdbc:MysqL://localhost:3306/ssm?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
     spring.datasource.username=root
     spring.datasource.password=123456
    
     mybatis.mapper-locations=classpath:mybatis/*.xml
    
     spring.security.user.name=root
     spring.security.user.password=123456
    
     debug=true
    
  • 安全配置

     @Configuration
     public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .anyRequest().authenticated()
                 .and()
                 .formLogin().permitAll();
         }
     }
    
  • 用户控制器

     @RestController
     @RequestMapping("/rest/users")
     public class UserController {
         @Autowired
         private UserMapper userMapper;
    
         @PostMapping
         public void add(@RequestBody User user){
             userMapper.add(user);
         }
    
         @GetMapping
         public List<User> getAll(){
             return userMapper.findAll();
         }
     }
    
  • 卷曲脚本

      #!/usr/bin/env bash
    
      host=192.168.44.109:8080
      remote=http://${host}
      login=${remote}/login
      users=${remote}/rest/users/
    
      csrf=$( \
        curl --url ${login} -L -c cookie.txt 2>&1 \
        |grep _csrf \
        |sed 's/^.*value="\(.*\)".*$/\1/' \
      )
      echo "before login,csrf=${csrf}"
    
      curl --url ${login} -L -b cookie.txt -c cookie.txt -i \
          -d "username=root&password=123456&_csrf=${csrf}"
    
      curl --url ${users} -L -b cookie.txt -v \
          -H "Content-Type: application/json" \
          -H "X-CSRF-Token: ${csrf}" \
          -H "x-csrf-token: ${csrf}" \
          -H "_csrf: ${csrf}" \
          -d "{\"name\": \"name2\"}"
    
      rm -f cookie.txt
    
  • 脚本输出

     before login,csrf=6d7b2d7b-f9aa-4463-ad9b-468082df4d74
     HTTP/1.1 302 
     Set-Cookie: JSESSIONID=D728694163DEEC78DDBC8869DC54C870; Path=/; HttpOnly
     X-Content-Type-Options: nosniff
     X-XSS-Protection: 1; mode=block
     Cache-Control: no-cache,no-store,max-age=0,must-revalidate
     Pragma: no-cache
     Expires: 0
     x-frame-options: DENY
     Location: http://192.168.44.109:8080/
     Content-Length: 0
     Date: Sun,20 Jun 2021 11:05:52 GMT
    
     HTTP/1.1 200 
     vary: Origin
     vary: Access-Control-Request-Method
     vary: Access-Control-Request-Headers
     X-Content-Type-Options: nosniff
     X-XSS-Protection: 1; mode=block
     Cache-Control: no-cache,must-revalidate
     Pragma: no-cache
     Expires: 0
     x-frame-options: DENY
     Content-Type: application/hal+json
     transfer-encoding: chunked
     Date: Sun,20 Jun 2021 11:05:52 GMT
    
     {
       "_links" : {
         "profile" : {
           "href" : "http://192.168.44.109:8080/profile"
         }
       }
     }*   Trying 192.168.44.109...
     * TCP_NODELAY set
     * Connected to 192.168.44.109 (192.168.44.109) port 8080 (#0)
     > POST /rest/users/ HTTP/1.1
     > Host: 192.168.44.109:8080
     > User-Agent: curl/7.64.1
     > Accept: */*
     > Cookie: JSESSIONID=D728694163DEEC78DDBC8869DC54C870
     > Content-Type: application/json
     > X-CSRF-Token: 6d7b2d7b-f9aa-4463-ad9b-468082df4d74
     > x-csrf-token: 6d7b2d7b-f9aa-4463-ad9b-468082df4d74
     > _csrf: 6d7b2d7b-f9aa-4463-ad9b-468082df4d74
     > Content-Length: 17
     > 
     * upload completely sent off: 17 out of 17 bytes
     < HTTP/1.1 403 
     < X-Content-Type-Options: nosniff
     < X-XSS-Protection: 1; mode=block
     < Cache-Control: no-cache,must-revalidate
     < Pragma: no-cache
     < Expires: 0
     < x-frame-options: DENY
     < Content-Type: application/json
     < transfer-encoding: chunked
     < Date: Sun,20 Jun 2021 11:05:52 GMT
     < 
     * Connection #0 to host 192.168.44.109 left intact
     {"timestamp":"2021-06-20T11:05:52.540+00:00","status":403,"error":"Forbidden","message":"","path":"/rest/users/"}* Closing connection 0
    

解决方法

CSRF token 是否需要重新生成不同的帖子

不,没有必要。

奇怪的是登录帖子请求有效

登录后由spring.security重新生成token。

那么如何在启用 CSRF 的情况下使发布请求有效?

关键是如何获取登录后生成的新CSRF令牌。一种可能的解决方案是将 CSRF 令牌保存在 cookie 中。并且 SecurityConfig 应更改为:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin().permitAll();
    }
}

然后,可以通过 awk '/XSRF-TOKEN/{print $7}' cookie 从 cookie 中获取 csrf 令牌,并使用 X-XSRF-TOKEN 请求头发送到服务器。完整脚本改为:

#!/usr/bin/env bash

 host=192.168.44.109:8080
 remote=http://${host}
 login=${remote}/login
 users=${remote}/rest/users/

 csrf(){
     awk '/XSRF-TOKEN/{print $7}' cookie
 }

 curl ${login} -c cookie 1> /dev/null 2>/dev/null

 echo "before login,csrf=$(csrf)"
 curl ${login} -d "username=root&password=123456&_csrf=$(csrf)" -b cookie -c cookie -L -i
 echo "after login,csrf=$(csrf)"

 name=$(date '+%Y%m%d-%H:%M:%S')
 curl ${users} -H "X-XSRF-TOKEN: $(csrf)" -H 'Content-Type: application/json' -d "{\"name\": \"${name}\"}" -b cookie -L -v

 rm -f cookie