From 4da0f22a1863bd45c58304b52037f067f0b7ab57 Mon Sep 17 00:00:00 2001 From: devezhao Date: Fri, 5 Feb 2021 11:28:52 +0800 Subject: [PATCH] better csrfToken --- .codecov.yml | 2 +- .eslintrc.js | 1 + @rbv | 2 +- crowdin.yml | 8 --- .../com/rebuild/core/support/CsrfToken.java | 67 +++++++++++++++++++ src/main/java/com/rebuild/utils/AppUtils.java | 4 +- .../rebuild/web/RebuildWebInterceptor.java | 12 +++- src/main/resources/application.yml | 10 +-- src/main/resources/web/assets/css/rb-base.css | 5 +- src/main/resources/web/assets/css/rb-page.css | 2 +- src/main/resources/web/assets/js/rb-page.js | 10 +-- 11 files changed, 96 insertions(+), 27 deletions(-) delete mode 100644 crowdin.yml create mode 100644 src/main/java/com/rebuild/core/support/CsrfToken.java diff --git a/.codecov.yml b/.codecov.yml index cc2b01f97..3332c1300 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -4,7 +4,7 @@ coverage: precision: 2 status: - project: yes + project: true patch: no changes: no diff --git a/.eslintrc.js b/.eslintrc.js index 3adf921d1..a49965c7d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -55,6 +55,7 @@ module.exports = { $fileExtName: true, $gotoSection: true, $createUploader: true, + $initUploader: true, $cleanMenu: true, $cleanMap: true, $pages: true, diff --git a/@rbv b/@rbv index 7b0d1b946..0aa958543 160000 --- a/@rbv +++ b/@rbv @@ -1 +1 @@ -Subproject commit 7b0d1b946a9c4ba5a832a57dc3db17606c6e912f +Subproject commit 0aa958543b1eb60bbfe69bc3c9e689c5d0c224d6 diff --git a/crowdin.yml b/crowdin.yml deleted file mode 100644 index e65118f4d..000000000 --- a/crowdin.yml +++ /dev/null @@ -1,8 +0,0 @@ -commit_message: '[Crowdin] Update translations' -files: - - source: '/src/main/resources/i18n/language.zh_CN.json' - translation: '/src/main/resources/i18n/language.%locale%.json' - languages_mapping: - locale: - 'zh-CN': 'zh_CN' - 'en-US': 'en' diff --git a/src/main/java/com/rebuild/core/support/CsrfToken.java b/src/main/java/com/rebuild/core/support/CsrfToken.java new file mode 100644 index 000000000..693c4d629 --- /dev/null +++ b/src/main/java/com/rebuild/core/support/CsrfToken.java @@ -0,0 +1,67 @@ +/* +Copyright (c) REBUILD and/or its owners. All rights reserved. + +rebuild is dual-licensed under commercial and open source licenses (GPLv3). +See LICENSE and COMMERCIAL in the project root for license information. +*/ + +package com.rebuild.core.support; + +import cn.devezhao.commons.CodecUtils; +import com.rebuild.core.Application; +import com.rebuild.core.cache.CommonsCache; +import com.rebuild.utils.AppUtils; +import org.apache.commons.lang.StringUtils; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author devezhao + * @since 2020/12/15 + */ +public class CsrfToken { + + // Token 存储前缀 + private static final String TOKEN_PREFIX = "RBCSRF."; + + /** + * 生成并存储 Token + * + * @return + */ + public static String generate() { + String token = CodecUtils.randomCode(60); + Application.getCommonsCache().putx(TOKEN_PREFIX + token, + System.currentTimeMillis(), CommonsCache.TS_HOUR * 2); + return token; + } + + /** + * 验证 Token + * + * @param token + * @param destroy + * @return + */ + public static boolean verify(String token, boolean destroy) { + if (StringUtils.isBlank(token)) return false; + + token = TOKEN_PREFIX + token; + Object exists = Application.getCommonsCache().getx(token); + if (exists != null && destroy) { + Application.getCommonsCache().evict(token); + } + return exists != null; + } + + /** + * @param request + * @param destroy + * @return + */ + public static boolean verify(HttpServletRequest request, boolean destroy) { + String token = request.getHeader("X-Csrf-Token"); + if (token == null) token = request.getParameter("_token"); + return verify(token, destroy); + } +} diff --git a/src/main/java/com/rebuild/utils/AppUtils.java b/src/main/java/com/rebuild/utils/AppUtils.java index 081aa7609..7720b95ae 100644 --- a/src/main/java/com/rebuild/utils/AppUtils.java +++ b/src/main/java/com/rebuild/utils/AppUtils.java @@ -187,10 +187,10 @@ public class AppUtils { public static MimeType parseMimeType(HttpServletRequest request) { try { String acceptType = request.getHeader("Accept"); - if (acceptType == null) acceptType = request.getContentType(); + if (acceptType == null || "*/*".equals(acceptType)) acceptType = request.getContentType(); // Via Spider? - if (acceptType == null) return MimeTypeUtils.TEXT_HTML; + if (StringUtils.isBlank(acceptType)) return MimeTypeUtils.TEXT_HTML; acceptType = acceptType.split("[,;]")[0]; // Accpet ALL? diff --git a/src/main/java/com/rebuild/web/RebuildWebInterceptor.java b/src/main/java/com/rebuild/web/RebuildWebInterceptor.java index 646cbc021..e92608dec 100644 --- a/src/main/java/com/rebuild/web/RebuildWebInterceptor.java +++ b/src/main/java/com/rebuild/web/RebuildWebInterceptor.java @@ -18,6 +18,7 @@ import com.rebuild.core.UserContextHolder; import com.rebuild.core.cache.CommonsCache; import com.rebuild.core.privileges.bizz.ZeroEntry; import com.rebuild.core.support.ConfigurationItem; +import com.rebuild.core.support.CsrfToken; import com.rebuild.core.support.License; import com.rebuild.core.support.RebuildConfiguration; import com.rebuild.core.support.setup.InstallState; @@ -28,8 +29,8 @@ import org.apache.commons.lang.StringUtils; import org.springframework.core.NamedThreadLocal; import org.springframework.http.HttpStatus; import org.springframework.util.MimeTypeUtils; +import org.springframework.web.servlet.AsyncHandlerInterceptor; import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -45,7 +46,7 @@ import java.io.IOException; * @since 2.0 */ @Slf4j -public class RebuildWebInterceptor extends HandlerInterceptorAdapter implements InstallState { +public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallState { private static final ThreadLocal REQUEST_ENTRY = new NamedThreadLocal<>("RequestEntry"); @@ -143,6 +144,11 @@ public class RebuildWebInterceptor extends HandlerInterceptorAdapter implements } } else if (!isIgnoreAuth(requestUri)) { + // 外部表单特殊处理(媒体字段上传/预览) + if (requestUri.contains("/filex/") && CsrfToken.verify(request, false)) { + return true; + } + log.warn("Unauthorized access {} via {}", RebuildWebConfigurer.getRequestUrls(request), ServletUtils.getRemoteAddr(request)); @@ -160,7 +166,7 @@ public class RebuildWebInterceptor extends HandlerInterceptorAdapter implements @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { - super.postHandle(request, response, handler, modelAndView); + // Notings } @Override diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9c5ba5e52..af7357a64 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,12 +20,13 @@ server: # SPRING spring: - mvc: - static-path-pattern: /assets/** - resources: - static-locations: classpath:/web/assets servlet: multipart.enabled: false + web: + resources: + static-locations: classpath:/web/assets + mvc: + static-path-pattern: /assets/** thymeleaf: prefix: classpath:/web cache: true @@ -37,3 +38,4 @@ spring: size: 5 main: banner-mode: off + diff --git a/src/main/resources/web/assets/css/rb-base.css b/src/main/resources/web/assets/css/rb-base.css index c68d91d85..240b0a04d 100644 --- a/src/main/resources/web/assets/css/rb-base.css +++ b/src/main/resources/web/assets/css/rb-base.css @@ -20135,12 +20135,11 @@ label { z-index: -1; } -.inputfile + label { +.inputfile + label, .inputfile-label { padding: 0 12px; font-size: 1rem; line-height: 30px; - border: 1px solid transparent; - border-color: #d5d8de; + border: 1px solid #d5d8de; border-radius: 2px; display: inline-block; cursor: pointer; diff --git a/src/main/resources/web/assets/css/rb-page.css b/src/main/resources/web/assets/css/rb-page.css index fffd288a4..b78990fb5 100644 --- a/src/main/resources/web/assets/css/rb-page.css +++ b/src/main/resources/web/assets/css/rb-page.css @@ -1014,7 +1014,7 @@ a { border: 0 none; color: #404040; border-left: 2px solid #ccc; - width: 205px; + width: 239px; border-radius: 2px; max-height: 38px; cursor: default; diff --git a/src/main/resources/web/assets/js/rb-page.js b/src/main/resources/web/assets/js/rb-page.js index b020eae5e..5aa5f4727 100644 --- a/src/main/resources/web/assets/js/rb-page.js +++ b/src/main/resources/web/assets/js/rb-page.js @@ -441,13 +441,15 @@ var $createUploader = function (input, next, complete, error) { var local = input.data('local') if (!input.attr('data-maxsize')) input.attr('data-maxsize', 1024 * 1024 * 100) // default 100M + var useToken = rb.csrfToken ? ('&_token=' + rb.csrfToken) : '' + if (window.qiniu && rb.storageUrl && !local) { input.on('change', function () { var file = this.files[0] if (!file) return var putExtra = imgOnly ? { mimeType: ['image/png', 'image/jpeg', 'image/gif', 'image/bmp'] } : null - $.get('/filex/qiniu/upload-keys?file=' + $encode(file.name), function (res) { + $.get('/filex/qiniu/upload-keys?file=' + $encode(file.name) + useToken, function (res) { var o = qiniu.upload(file, res.data.key, res.data.token, putExtra) o.subscribe({ next: function (res) { @@ -466,7 +468,7 @@ var $createUploader = function (input, next, complete, error) { return false }, complete: function (res) { - if (file.size > 0) $.post('/filex/store-filesize?fs=' + file.size + '&fp=' + $encode(res.key)) + if (file.size > 0) $.post('/filex/store-filesize?fs=' + file.size + '&fp=' + $encode(res.key) + useToken) typeof complete === 'function' && complete({ key: res.key }) }, }) @@ -475,7 +477,7 @@ var $createUploader = function (input, next, complete, error) { } else { input.html5Uploader({ name: input.attr('id') || input.attr('name') || 'H5Upload', - postUrl: rb.baseUrl + '/filex/upload?type=' + (imgOnly ? 'image' : 'file') + '&temp=' + (local === 'temp'), + postUrl: rb.baseUrl + '/filex/upload?type=' + (imgOnly ? 'image' : 'file') + '&temp=' + (local === 'temp') + useToken, onSelectError: function (file, err) { if (err === 'ErrorType') { RbHighbar.create($L(imgOnly ? 'PlsUploadImg' : 'FileTypeError')) @@ -492,7 +494,7 @@ var $createUploader = function (input, next, complete, error) { onSuccess: function (e, file) { e = $.parseJSON(e.currentTarget.response) if (e.error_code === 0) { - if (local !== 'temp' && file.size > 0) $.post('/filex/store-filesize?fs=' + file.size + '&fp=' + $encode(e.data)) + if (local !== 'temp' && file.size > 0) $.post('/filex/store-filesize?fs=' + file.size + '&fp=' + $encode(e.data) + useToken) complete({ key: e.data }) } else { RbHighbar.error($L('ErrorUpload'))