diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java index 10da47fb4..7e9347ee8 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java @@ -3,6 +3,7 @@ package com.ruoyi.web.controller.system; import java.util.Date; import java.util.List; import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @@ -45,7 +46,7 @@ public class SysIndexController extends BaseController // 系统首页 @GetMapping("/index") - public String index(ModelMap mmap) + public String index(ModelMap mmap, HttpServletRequest request) { // 取身份信息 SysUser user = getSysUser(); @@ -82,6 +83,8 @@ public class SysIndexController extends BaseController } } String webIndex = "topnav".equalsIgnoreCase(indexStyle) ? "index-topnav" : "index"; + // CSRF Token + request.getSession().setAttribute(ShiroConstants.CSRF_TOKEN, ServletUtils.generateToken()); return webIndex; } diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index ffb65e8bd..44b90f2af 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -136,6 +136,13 @@ xss: # 匹配链接 urlPatterns: /system/*,/monitor/*,/tool/* +# 防止csrf攻击 +csrf: + # 过滤开关 + enabled: true + # 白名单(多个用逗号分隔) + whites: + # Swagger配置 swagger: # 是否开启swagger diff --git a/ruoyi-admin/src/main/resources/static/ruoyi/js/common.js b/ruoyi-admin/src/main/resources/static/ruoyi/js/common.js index e55151458..e8acf3d4e 100644 --- a/ruoyi-admin/src/main/resources/static/ruoyi/js/common.js +++ b/ruoyi-admin/src/main/resources/static/ruoyi/js/common.js @@ -573,6 +573,12 @@ function _stopIt(e) { /** 设置全局ajax处理 */ $.ajaxSetup({ + beforeSend: function (xhr, settings) { + var csrftoken = $('meta[name=csrf-token]').attr('content') + if (($.common.equalsIgnoreCase(settings.type, "POST"))) { + xhr.setRequestHeader("csrf_token", csrftoken) + } + }, complete: function(XMLHttpRequest, textStatus) { if (textStatus == 'timeout') { $.modal.alertWarning("服务器超时,请稍后再试!"); diff --git a/ruoyi-admin/src/main/resources/static/ruoyi/js/ry-ui.js b/ruoyi-admin/src/main/resources/static/ruoyi/js/ry-ui.js index 0784ff31d..76171899c 100644 --- a/ruoyi-admin/src/main/resources/static/ruoyi/js/ry-ui.js +++ b/ruoyi-admin/src/main/resources/static/ruoyi/js/ry-ui.js @@ -277,6 +277,7 @@ var table = { } else if ($.common.equals("open", target)) { top.layer.alert(input.val(), { title: "信息内容", + area: ['400px', ''], shadeClose: true, btn: ['确认'], btnclass: ['btn btn-primary'], @@ -1049,7 +1050,11 @@ var table = { type: type, dataType: dataType, data: data, - beforeSend: function () { + beforeSend: function (xhr, settings) { + var csrftoken = $('meta[name=csrf-token]').attr('content'); + if ($.common.equalsIgnoreCase(settings.type, "POST")) { + xhr.setRequestHeader("csrf_token", csrftoken); + } $.modal.loading("正在处理中,请稍候..."); }, success: function(result) { @@ -1229,7 +1234,11 @@ var table = { type: "post", dataType: "json", data: data, - beforeSend: function () { + beforeSend: function (xhr, settings) { + var csrftoken = $('meta[name=csrf-token]').attr('content'); + if (($.common.equalsIgnoreCase(settings.type, "POST"))) { + xhr.setRequestHeader("csrf_token", csrftoken); + } $.modal.loading("正在处理中,请稍候..."); $.modal.disable(); }, @@ -1249,7 +1258,11 @@ var table = { type: "post", dataType: "json", data: data, - beforeSend: function () { + beforeSend: function (xhr, settings) { + var csrftoken = $('meta[name=csrf-token]').attr('content'); + if (($.common.equalsIgnoreCase(settings.type, "POST"))) { + xhr.setRequestHeader("csrf_token", csrftoken); + } $.modal.loading("正在处理中,请稍候..."); }, success: function(result) { @@ -1275,7 +1288,11 @@ var table = { type: "post", dataType: "json", data: data, - beforeSend: function () { + beforeSend: function (xhr, settings) { + var csrftoken = $('meta[name=csrf-token]').attr('content'); + if (($.common.equalsIgnoreCase(settings.type, "POST"))) { + xhr.setRequestHeader("csrf_token", csrftoken); + } $.modal.loading("正在处理中,请稍候..."); }, success: function(result) { diff --git a/ruoyi-admin/src/main/resources/templates/include.html b/ruoyi-admin/src/main/resources/templates/include.html index d156314c1..464065878 100644 --- a/ruoyi-admin/src/main/resources/templates/include.html +++ b/ruoyi-admin/src/main/resources/templates/include.html @@ -5,6 +5,7 @@ + diff --git a/ruoyi-admin/src/main/resources/templates/lock.html b/ruoyi-admin/src/main/resources/templates/lock.html index ac89d9252..122fc33af 100644 --- a/ruoyi-admin/src/main/resources/templates/lock.html +++ b/ruoyi-admin/src/main/resources/templates/lock.html @@ -3,6 +3,7 @@ + 锁定屏幕 @@ -94,7 +95,9 @@ type: "post", dataType: "json", data: { password: password }, - beforeSend: function() { + beforeSend: function(xhr) { + var csrftoken = $('meta[name=csrf-token]').attr('content'); + xhr.setRequestHeader("csrf_token", csrftoken); index = layer.load(2, {shade: false}); }, success: function(result) { diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/ShiroConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/ShiroConstants.java index 239d36fc7..acbbedab9 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/constant/ShiroConstants.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/ShiroConstants.java @@ -33,9 +33,9 @@ public class ShiroConstants public static final String ERROR = "errorMsg"; /** - * 编码格式 + * csrf key */ - public static final String ENCODING = "UTF-8"; + public static final String CSRF_TOKEN = "csrf_token"; /** * 当前在线会话 diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java index 6214f5643..684d969c5 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java @@ -4,6 +4,8 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; +import java.security.SecureRandom; +import java.util.Base64; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -25,6 +27,8 @@ public class ServletUtils */ private final static String[] agent = { "Android", "iPhone", "iPod", "iPad", "Windows Phone", "MQQBrowser" }; + private static final SecureRandom secureRandom = new SecureRandom(); + /** * 获取String参数 */ @@ -213,4 +217,16 @@ public class ServletUtils return StringUtils.EMPTY; } } + + /** + * 生成CSRF Token + * + * @return 解码后的内容 + */ + public static String generateToken() + { + byte[] bytes = new byte[32]; + secureRandom.nextBytes(bytes); + return Base64.getEncoder().encodeToString(bytes); + } } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java index 2dfa412f1..f2598932e 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java @@ -357,6 +357,18 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils return new HashSet(str2List(str, sep, true, false)); } + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @return list集合 + */ + public static final List str2List(String str, String sep) + { + return str2List(str, sep, true, false); + } + /** * 字符串转list * diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java index 809027036..8437f88c6 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java @@ -33,6 +33,7 @@ import com.ruoyi.framework.shiro.session.OnlineSessionFactory; import com.ruoyi.framework.shiro.web.CustomShiroFilterFactoryBean; import com.ruoyi.framework.shiro.web.filter.LogoutFilter; import com.ruoyi.framework.shiro.web.filter.captcha.CaptchaValidateFilter; +import com.ruoyi.framework.shiro.web.filter.csrf.CsrfValidateFilter; import com.ruoyi.framework.shiro.web.filter.kickout.KickoutSessionFilter; import com.ruoyi.framework.shiro.web.filter.online.OnlineSessionFilter; import com.ruoyi.framework.shiro.web.filter.sync.SyncOnlineSessionFilter; @@ -132,6 +133,18 @@ public class ShiroConfig @Value("${shiro.rememberMe.enabled: false}") private boolean rememberMe; + /** + * 是否开启csrf + */ + @Value("${csrf.enabled: false}") + private boolean csrfEnabled; + + /** + * csrf白名单链接 + */ + @Value("${csrf.whites: ''}") + private String csrfWhites; + /** * 缓存管理器 使用Ehcache实现 */ @@ -263,6 +276,17 @@ public class ShiroConfig return logoutFilter; } + /** + * csrf过滤器 + */ + public CsrfValidateFilter csrfValidateFilter() + { + CsrfValidateFilter csrfValidateFilter = new CsrfValidateFilter(); + csrfValidateFilter.setEnabled(csrfEnabled); + csrfValidateFilter.setCsrfWhites(StringUtils.str2List(csrfWhites, ",")); + return csrfValidateFilter; + } + /** * Shiro过滤器配置 */ @@ -309,13 +333,14 @@ public class ShiroConfig filters.put("onlineSession", onlineSessionFilter()); filters.put("syncOnlineSession", syncOnlineSessionFilter()); filters.put("captchaValidate", captchaValidateFilter()); + filters.put("csrfValidateFilter", csrfValidateFilter()); filters.put("kickout", kickoutSessionFilter()); // 注销成功,则跳转到指定页面 filters.put("logout", logoutFilter()); shiroFilterFactoryBean.setFilters(filters); // 所有请求需要认证 - filterChainDefinitionMap.put("/**", "user,kickout,onlineSession,syncOnlineSession"); + filterChainDefinitionMap.put("/**", "user,kickout,onlineSession,syncOnlineSession,csrfValidateFilter"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/csrf/CsrfValidateFilter.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/csrf/CsrfValidateFilter.java new file mode 100644 index 000000000..978e6639e --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/csrf/CsrfValidateFilter.java @@ -0,0 +1,76 @@ +package com.ruoyi.framework.shiro.web.filter.csrf; + +import java.util.List; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.shiro.web.filter.AccessControlFilter; +import com.ruoyi.common.constant.ShiroConstants; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.ShiroUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * csrf过滤器 + * + * @author ruoyi + */ +public class CsrfValidateFilter extends AccessControlFilter +{ + /** + * 白名单链接 + */ + private List csrfWhites; + + @Override + protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) + throws Exception + { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + if (!isAllowMethod(httpServletRequest)) + { + return true; + } + if (StringUtils.matches(httpServletRequest.getServletPath(), csrfWhites)) + { + return true; + } + return validateResponse(httpServletRequest, httpServletRequest.getHeader(ShiroConstants.CSRF_TOKEN)); + } + + public boolean validateResponse(HttpServletRequest request, String requestToken) + { + Object obj = ShiroUtils.getSession().getAttribute(ShiroConstants.CSRF_TOKEN); + String sessionToken = Convert.toStr(obj, ""); + if (StringUtils.isEmpty(requestToken) || !requestToken.equalsIgnoreCase(sessionToken)) + { + return false; + } + return true; + } + + @Override + protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception + { + ServletUtils.renderString((HttpServletResponse) response, "{\"code\":\"1\",\"msg\":\"当前请求的安全验证未通过,请刷新页面后重试。\"}"); + return false; + } + + private boolean isAllowMethod(HttpServletRequest request) + { + String method = request.getMethod(); + return "POST".equalsIgnoreCase(method); + } + + public List getCsrfWhites() + { + return csrfWhites; + } + + public void setCsrfWhites(List csrfWhites) + { + this.csrfWhites = csrfWhites; + } +}