spring security webflux 跨域防护_serveraccessdeniedhandler_o_瓜田李下_o的博客-程序员秘密

技术标签: spring security  


spring security webflux 跨域防护

 

官网:https://docs.spring.io/spring-security/site/docs/5.3.2.RELEASE/reference/html5/#webflux-csrf-using

 

跨域防护:客户端的请求携带 csrf token,后端接到请求后,与存储在session中的csrf token比对,若一致则请求通过,否则抛出异常,spring security 跨域防护默认开启

 

**************************

相关类及接口

 

ServerHttpSecurity

public class ServerHttpSecurity {


**************
内部类:ServerHttpSecurity.CsrfSpec

    public class CsrfSpec {
        private CsrfWebFilter filter;                           //拦截验证csrf token
        private ServerCsrfTokenRepository csrfTokenRepository;  //后端存储csrf token
        private boolean specifiedRequireCsrfProtectionMatcher;

        public ServerHttpSecurity.CsrfSpec accessDeniedHandler(ServerAccessDeniedHandler accessDeniedHandler) {
            this.filter.setAccessDeniedHandler(accessDeniedHandler);
            return this;
        }

        public ServerHttpSecurity.CsrfSpec csrfTokenRepository(ServerCsrfTokenRepository csrfTokenRepository) {
            this.csrfTokenRepository = csrfTokenRepository;
            return this;
        }

        public ServerHttpSecurity.CsrfSpec requireCsrfProtectionMatcher(ServerWebExchangeMatcher requireCsrfProtectionMatcher) {
            this.filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
            this.specifiedRequireCsrfProtectionMatcher = true;
            return this;
        }

        public ServerHttpSecurity.CsrfSpec tokenFromMultipartDataEnabled(boolean enabled) {
            this.filter.setTokenFromMultipartDataEnabled(enabled);
            return this;
        }

        public ServerHttpSecurity and() {
            return ServerHttpSecurity.this;
        }

        public ServerHttpSecurity disable() {
            ServerHttpSecurity.this.csrf = null;
            return ServerHttpSecurity.this;
        }

        protected void configure(ServerHttpSecurity http) {
            if (this.csrfTokenRepository != null) {
                this.filter.setCsrfTokenRepository(this.csrfTokenRepository);
                if (ServerHttpSecurity.this.logout != null) {
                    ServerHttpSecurity.this.logout.addLogoutHandler(new CsrfServerLogoutHandler(this.csrfTokenRepository));
                }
            }

            http.addFilterAt(this.filter, SecurityWebFiltersOrder.CSRF);
        }

        private CsrfSpec() {
            this.filter = new CsrfWebFilter();
            this.csrfTokenRepository = new WebSessionServerCsrfTokenRepository();
        }
    }

 

CsrfWebFilter:拦截验证csrf token,默认不拦截ALLOWED_METHODS包含的方法(GET、HEAD等)

public class CsrfWebFilter implements WebFilter {
    public static final ServerWebExchangeMatcher DEFAULT_CSRF_MATCHER = new CsrfWebFilter.DefaultRequireCsrfProtectionMatcher();
    private static final String SHOULD_NOT_FILTER = "SHOULD_NOT_FILTER" + CsrfWebFilter.class.getName();
    private ServerWebExchangeMatcher requireCsrfProtectionMatcher;
    private ServerCsrfTokenRepository csrfTokenRepository;
    private ServerAccessDeniedHandler accessDeniedHandler;
    private boolean isTokenFromMultipartDataEnabled;

    public CsrfWebFilter() {
        this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
        this.csrfTokenRepository = new WebSessionServerCsrfTokenRepository();
        this.accessDeniedHandler = new HttpStatusServerAccessDeniedHandler(HttpStatus.FORBIDDEN);
    } //服务端使用WebSessionServerCsrfTokenRepository存储csrf token

    public void setAccessDeniedHandler(ServerAccessDeniedHandler accessDeniedHandler) {
        Assert.notNull(accessDeniedHandler, "accessDeniedHandler");
        this.accessDeniedHandler = accessDeniedHandler;
    }

    public void setCsrfTokenRepository(ServerCsrfTokenRepository csrfTokenRepository) {
        Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
        this.csrfTokenRepository = csrfTokenRepository;
    }

    public void setRequireCsrfProtectionMatcher(ServerWebExchangeMatcher requireCsrfProtectionMatcher) {
        Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
        this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
    }

    public void setTokenFromMultipartDataEnabled(boolean tokenFromMultipartDataEnabled) {
        this.isTokenFromMultipartDataEnabled = tokenFromMultipartDataEnabled;
    }

    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return Boolean.TRUE.equals(exchange.getAttribute(SHOULD_NOT_FILTER)) ? chain.filter(exchange).then(Mono.empty()) : this.requireCsrfProtectionMatcher.matches(exchange).filter((matchResult) -> {
            return matchResult.isMatch();
        }).filter((matchResult) -> {
            return !exchange.getAttributes().containsKey(CsrfToken.class.getName());
        }).flatMap((m) -> {
            return this.validateToken(exchange);
        }).flatMap((m) -> {
            return this.continueFilterChain(exchange, chain);
        }).switchIfEmpty(this.continueFilterChain(exchange, chain).then(Mono.empty())).onErrorResume(CsrfException.class, (e) -> {
            return this.accessDeniedHandler.handle(exchange, e);
        });
    }

    public static void skipExchange(ServerWebExchange exchange) {
        exchange.getAttributes().put(SHOULD_NOT_FILTER, Boolean.TRUE);
    }

    private Mono<Void> validateToken(ServerWebExchange exchange) {
                      //从请求中加在csrf token,与session 中的csrf进行比对,验证csrf token

        return this.csrfTokenRepository.loadToken(exchange).switchIfEmpty(Mono.defer(() -> {
            return Mono.error(new CsrfException("CSRF Token has been associated to this client"));
        })).filterWhen((expected) -> {
            return this.containsValidCsrfToken(exchange, expected);
        }).switchIfEmpty(Mono.defer(() -> {
            return Mono.error(new CsrfException("Invalid CSRF Token"));
        })).then();
    }

    private Mono<Boolean> containsValidCsrfToken(ServerWebExchange exchange, CsrfToken expected) {
        return exchange.getFormData().flatMap((data) -> {
            return Mono.justOrEmpty(data.getFirst(expected.getParameterName()));
        }).switchIfEmpty(Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(expected.getHeaderName()))).switchIfEmpty(this.tokenFromMultipartData(exchange, expected)).map((actual) -> {
            return actual.equals(expected.getToken());
        });
    }

    private Mono<String> tokenFromMultipartData(ServerWebExchange exchange, CsrfToken expected) {
        if (!this.isTokenFromMultipartDataEnabled) {
            return Mono.empty();
        } else {
            ServerHttpRequest request = exchange.getRequest();
            HttpHeaders headers = request.getHeaders();
            MediaType contentType = headers.getContentType();
            return !contentType.includes(MediaType.MULTIPART_FORM_DATA) ? Mono.empty() : exchange.getMultipartData().map((d) -> {
                return (Part)d.getFirst(expected.getParameterName());
            }).cast(FormFieldPart.class).map(FormFieldPart::value);
        }
    }

    private Mono<Void> continueFilterChain(ServerWebExchange exchange, WebFilterChain chain) {
        return Mono.defer(() -> {
            Mono<CsrfToken> csrfToken = this.csrfToken(exchange);
            exchange.getAttributes().put(CsrfToken.class.getName(), csrfToken);
            return chain.filter(exchange);
        });
    }

    private Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
        return this.csrfTokenRepository.loadToken(exchange).switchIfEmpty(this.generateToken(exchange));
    }

    private Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
        return this.csrfTokenRepository.generateToken(exchange).delayUntil((token) -> {
            return this.csrfTokenRepository.saveToken(exchange, token);
        });
    }

    private static class DefaultRequireCsrfProtectionMatcher implements ServerWebExchangeMatcher {
        private static final Set<HttpMethod> ALLOWED_METHODS;

        private DefaultRequireCsrfProtectionMatcher() {
        }

        public Mono<MatchResult> matches(ServerWebExchange exchange) {
            return Mono.just(exchange.getRequest()).map((r) -> {
                return r.getMethod();
            }).filter((m) -> {
                return ALLOWED_METHODS.contains(m);
            }).flatMap((m) -> {
                return MatchResult.notMatch();
            }).switchIfEmpty(MatchResult.match());
        }

        static {
            ALLOWED_METHODS = new HashSet(Arrays.asList(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.TRACE, HttpMethod.OPTIONS));
        }  //默认不拦截 ALLOWED_METHODS 包含的方法
    }
}

 

WebSessionServerCsrfTokenRepository:session 中存储csrf token

public class WebSessionServerCsrfTokenRepository implements ServerCsrfTokenRepository {
    private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";      //csrf token若在表单中,参数名默认为:_csrf
    private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";  //csrf token若在header中,参数名默认为:X-CSRF-TOKEN
    private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = WebSessionServerCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");  
                                                                           //session 中存储的csrf token的名称默认为:WebSessionServerCsrfTokenRepository类名+“.CSRF_TOKEN”

    private String parameterName = "_csrf";
    private String headerName = "X-CSRF-TOKEN";
    private String sessionAttributeName;

    public WebSessionServerCsrfTokenRepository() {
        this.sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
    }

    public Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
        return Mono.fromCallable(() -> {
            return this.createCsrfToken();
        });
    }

    public Mono<Void> saveToken(ServerWebExchange exchange, CsrfToken token) {
        return exchange.getSession().doOnNext((session) -> {
            this.putToken(session.getAttributes(), token);
        }).flatMap((session) -> {
            return session.changeSessionId();
        });
    }

    private void putToken(Map<String, Object> attributes, CsrfToken token) {
        if (token == null) {
            attributes.remove(this.sessionAttributeName);
        } else {
            attributes.put(this.sessionAttributeName, token);
        }

    }

    public Mono<CsrfToken> loadToken(ServerWebExchange exchange) {
        return exchange.getSession().filter((s) -> {
            return s.getAttributes().containsKey(this.sessionAttributeName);
        }).map((s) -> {
            return (CsrfToken)s.getAttribute(this.sessionAttributeName);
        });
    }

    public void setParameterName(String parameterName) {
        Assert.hasLength(parameterName, "parameterName cannot be null or empty");
        this.parameterName = parameterName;
    }

    public void setHeaderName(String headerName) {
        Assert.hasLength(headerName, "headerName cannot be null or empty");
        this.headerName = headerName;
    }

    public void setSessionAttributeName(String sessionAttributeName) {
        Assert.hasLength(sessionAttributeName, "sessionAttributename cannot be null or empty");
        this.sessionAttributeName = sessionAttributeName;
    }

    private CsrfToken createCsrfToken() {
        return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
    }

    private String createNewToken() {
        return UUID.randomUUID().toString();
    }
}

 

 

**************************

示例

 

********************

controller 层

 

RedirectController

@Controller
public class RedirectController {

    @RequestMapping("/hello")
    public String hello2(){
        return "hello";
    }
}

 

HelloConreoller

@RestController
public class HelloController {

    @RequestMapping("/hello2")
    public void hello2(ServerWebExchange exchange){
        AtomicReference<String> result = new AtomicReference<>();

        exchange.getFormData().subscribe(map ->{
            System.out.println("name:"+map.getFirst("name"));
            System.out.println("age:"+map.getFirst("age"));

            result.set(map.getFirst("name")+" "+map.getFirst("age"));
        });

        return result.get();
    }
}

 

********************

前端页面

 

hello.html

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form th:action="@{/hello8}" method="post" th:align="center">
    name:<input type="text" name="name"><br>
    age :<input type="text" name="age"><br>
    <button> 提交 </button>
</form>
</body>

 

 

**************************

使用测试

 

localhost:8080/hello

                        

 

默认开启csrf,thymeleaf会自动添加csrf token

                       

 

提交数据

                      

控制台输出

name:瓜田李下
age:20

 

 

如果直接在postman中提交数据,则报错

                     

 

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_43931625/article/details/106592872

智能推荐

[高通MSM8953_64][Android10]移除开机进入充电界面_Mr. 码农的博客-程序员秘密

文章目录开发平台基本信息问题描述解决方法开发平台基本信息芯片: MSM8953_64版本: Android 10kernel: msm-4.9问题描述在移植开发Android 10的时候,一开始是用debug版本编译调试的,一直都很正常,然后,准备提交测试的时候,编译user版本却无法正常进入系统,一直在开机logo跟充电界面循环跳转。这是因为设备进入了关机充电模式导致的,在lk阶段,将充电界面屏蔽,即可正常进入系统。解决方法diff --git a/bootable/bootloader

Mac XMind8 保存时报错_weixin_30348519的博客-程序员秘密

错误提示截图日志查看错误日志的方式:打开xmind –&gt; 关于xmind –&gt; 安装细节 –&gt; 选项卡 “配置” –&gt; 查看错误日志看到有Caused by: org.eclipse.equinox.security.storage.StorageException: No password provided.解决方案卸载 xmind删除 ...

转:【Java集合源码剖析】HashMap源码剖析_weixin_30451709的博客-程序员秘密

转载请注明出处:http://blog.csdn.net/ns_code/article/details/36034955您好,我正在参加CSDN博文大赛,如果您喜欢我的文章,希望您能帮我投一票,谢谢!投票地址:http://vote.blog.csdn.net/Article/Details?articleid=35568011HashMap简介 ...

hdu2102.A计划_genuinecai的博客-程序员秘密

Problem Description 可怜的公主在一次次被魔王掳走一次次被骑士们救回来之后,而今,不幸的她再一次面临生命的考验。魔王已经发出消息说将在T时刻吃掉公主,因为他听信谣言说吃公主的肉也能长生不老。年迈的国王正是心急如焚,告招天下勇士来拯救公主。不过公主早已习以为常,她深信智勇的骑士LJ肯定能将她救出。 现据密探所报,公主被关在一个两层的迷宫里,迷宫的入口是S(0,0,0),公主的位置

springboot 自定义错误页面与全局异常处理_fastjson_的博客-程序员秘密

一、springboot自定义错误页面springboot为我们提供了一个默认的映射:/error 当处理中抛出异常,就会转到该请求中处理,并且该请求有一个全局的错误页面来展示异常,如下图,当我们输入一个不存在的地址,就会跳转到此页面上面的错误页面并不友好,下面我们自己实现错误提示页面第一步、在我们的Spring Boot项目目录/src/main/resources/stat...

执行delete、update语句时,出现 Error Code: 1175.解决方法_progressDabinge的博客-程序员秘密

当执行delete、update语句时,出现 Error Code: 1175;这是因为mysql语句的安全模式引起:别慌,既然知道了问题就有解决的办法:将代码 SET SQL_SAFE_UPDATES = 0;执行,在执行delete、update语句试试。

随便推点

万树:Java和Android有什么联系?有什么区别?学哪个好?_hnwsqy的博客-程序员秘密

    从整体来讲,Java和Android的区别在于Android程序是基于组件和配置的,而且Android开发以Java语言为开发工具,表面上看他们有点同宗不同门,但实际上区别十分大,Android是一个主流智能手机操作系统,Java是一种开发语言,两者没有好坏之分,而且两者也是不同的岗位,从工作岗位来看,安卓从事的是移动互联方向,Java则是从事开发方向。   Android和Ja...

win10系统还原失败错误0x80070091的解决方法_MonteFan的博客-程序员秘密

1.首先,这个问题是由于2017年1月win10的更新补丁KB3213986导致的而且即使回退该补丁,报错也仍然能会存在。2.什么是WindowsApp?win8及后续版本开始使用WindowApps文件夹保存预先装好的应用。3.不建议直接通过命令删除WindowsApps文件夹rd /s "C:\Program Files\Windows Apps"

一次软键盘引起的界面跳动_lidongxiu0714的博客-程序员秘密

软键盘导致界面被顶起在RelativeLayout布局中放置在顶部的View由于受到软键盘弹起的影响,会被顶到软键盘以上,体验非常不好,这时可以给Activity设置 android:windowSoftInputMode=“adjustPan|stateHidden”,就可以防止底部View被顶起。上面的问题应该是比较普遍的问题,解决方法网上说的也很多。但更多的时候我们遇到的是软键盘遮挡的问题...

如何文件操作_weixin_30861459的博客-程序员秘密

今日内容1.什么是文件 2.为何用文件 3.如何用文件 4.文件操作补 5.常用方法 6.文件内指针的移动 7.with的使用4. 文件操作4.1 主模式r:只读模式L(默认) 当文件不存在时,会报错 当文件存在时,文件指针指向文件的开头w:只写模式 当文件不存在时,新建一个空文档 当文件存在时,清空...

在anaconda平台下写第一个python程序 ‘hello‘遇到的问题以及解决方案_qq_43142545的博客-程序员秘密

一 在写输出为’hello’的程序,遇到的第一个问题是AttributeError: module ‘tensorflow’ has no attribute ‘Session’,解决办法是:在引用tensorflow时,使用import tensorflow.compat.v1 as tf ,出现这个原因是因为 tensorflow 2.x版本没有Session这个属性,如果使用的是tensorflow的2.x版本时又想使用Session属性,可以使用 import tensorflow.com

pycharm三个有引号不能自动生成函数注释_weixin_40248634的博客-程序员秘密

去File | Settings | Tools | Python Integrated Tools | Docstring format 这里改成你想要的格式,然后再回去看看你的三个引号。没有函数注释的情况下是plain。改成reStucturedText, 在def funciton() 下面打上三对双引号,中间位置回车,得到是这样的:def function(x,y,z) """ :param x: :param y: :param z: :re

推荐文章

热门文章

相关标签