RuoYi преди 1 месец
родител
ревизия
ea9976575a

+ 4 - 1
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;
     }
 

+ 7 - 0
ruoyi-admin/src/main/resources/application.yml

@@ -136,6 +136,13 @@ xss:
   # 匹配链接
   urlPatterns: /system/*,/monitor/*,/tool/*
 
+# 防止csrf攻击
+csrf:
+  # 过滤开关
+  enabled: true
+  # 白名单(多个用逗号分隔)
+  whites: 
+
 # Swagger配置
 swagger:
   # 是否开启swagger

+ 6 - 0
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("服务器超时,请稍后再试!");

+ 21 - 4
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) {

+ 1 - 0
ruoyi-admin/src/main/resources/templates/include.html

@@ -5,6 +5,7 @@
 	<meta http-equiv="X-UA-Compatible" content="IE=edge">
 	<meta name="keywords" content="">
 	<meta name="description" content="">
+	<meta th:content="${session.csrf_token}" name="csrf-token"/>
 	<title th:text="${title}"></title>
 	<link th:href="@{/css/bootstrap.min.css?v=3.3.7}" rel="stylesheet"/>
 	<link th:href="@{/css/font-awesome.min.css?v=4.7.0}" rel="stylesheet"/>

+ 4 - 1
ruoyi-admin/src/main/resources/templates/lock.html

@@ -3,6 +3,7 @@
 <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta th:content="${session.csrf_token}" name="csrf-token"/>
     <!--360浏览器优先以webkit内核解析-->
     <title>锁定屏幕</title>
     <link th:href="@{favicon.ico}" rel="shortcut icon"/>
@@ -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) {

+ 2 - 2
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";
 
     /**
      * 当前在线会话

+ 16 - 0
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);
+    }
 }

+ 12 - 0
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<String>(str2List(str, sep, true, false));
     }
 
+    /**
+     * 字符串转list
+     * 
+     * @param str 字符串
+     * @param sep 分隔符
+     * @return list集合
+     */
+    public static final List<String> str2List(String str, String sep)
+    {
+        return str2List(str, sep, true, false);
+    }
+
     /**
      * 字符串转list
      * 

+ 26 - 1
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;

+ 76 - 0
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<String> 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<String> getCsrfWhites()
+    {
+        return csrfWhites;
+    }
+
+    public void setCsrfWhites(List<String> csrfWhites)
+    {
+        this.csrfWhites = csrfWhites;
+    }
+}