赞
踩
官网: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中提交数据,则报错
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。