新增CSRF防护功能

pull/552/head
RuoYi 2025-04-15 16:24:42 +08:00
parent 407f9f46d8
commit ea9976575a
11 changed files with 175 additions and 9 deletions

View File

@ -3,6 +3,7 @@ package com.ruoyi.web.controller.system;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
@ -45,7 +46,7 @@ public class SysIndexController extends BaseController
// 系统首页 // 系统首页
@GetMapping("/index") @GetMapping("/index")
public String index(ModelMap mmap) public String index(ModelMap mmap, HttpServletRequest request)
{ {
// 取身份信息 // 取身份信息
SysUser user = getSysUser(); SysUser user = getSysUser();
@ -82,6 +83,8 @@ public class SysIndexController extends BaseController
} }
} }
String webIndex = "topnav".equalsIgnoreCase(indexStyle) ? "index-topnav" : "index"; String webIndex = "topnav".equalsIgnoreCase(indexStyle) ? "index-topnav" : "index";
// CSRF Token
request.getSession().setAttribute(ShiroConstants.CSRF_TOKEN, ServletUtils.generateToken());
return webIndex; return webIndex;
} }

View File

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

View File

@ -573,6 +573,12 @@ function _stopIt(e) {
/** 设置全局ajax处理 */ /** 设置全局ajax处理 */
$.ajaxSetup({ $.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) { complete: function(XMLHttpRequest, textStatus) {
if (textStatus == 'timeout') { if (textStatus == 'timeout') {
$.modal.alertWarning("服务器超时,请稍后再试!"); $.modal.alertWarning("服务器超时,请稍后再试!");

View File

@ -277,6 +277,7 @@ var table = {
} else if ($.common.equals("open", target)) { } else if ($.common.equals("open", target)) {
top.layer.alert(input.val(), { top.layer.alert(input.val(), {
title: "信息内容", title: "信息内容",
area: ['400px', ''],
shadeClose: true, shadeClose: true,
btn: ['确认'], btn: ['确认'],
btnclass: ['btn btn-primary'], btnclass: ['btn btn-primary'],
@ -1049,7 +1050,11 @@ var table = {
type: type, type: type,
dataType: dataType, dataType: dataType,
data: data, 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.loading("正在处理中,请稍候...");
}, },
success: function(result) { success: function(result) {
@ -1229,7 +1234,11 @@ var table = {
type: "post", type: "post",
dataType: "json", dataType: "json",
data: data, 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.loading("正在处理中,请稍候...");
$.modal.disable(); $.modal.disable();
}, },
@ -1249,7 +1258,11 @@ var table = {
type: "post", type: "post",
dataType: "json", dataType: "json",
data: data, 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.loading("正在处理中,请稍候...");
}, },
success: function(result) { success: function(result) {
@ -1275,7 +1288,11 @@ var table = {
type: "post", type: "post",
dataType: "json", dataType: "json",
data: data, 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.loading("正在处理中,请稍候...");
}, },
success: function(result) { success: function(result) {

View File

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

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta th:content="${session.csrf_token}" name="csrf-token"/>
<!--360浏览器优先以webkit内核解析--> <!--360浏览器优先以webkit内核解析-->
<title>锁定屏幕</title> <title>锁定屏幕</title>
<link th:href="@{favicon.ico}" rel="shortcut icon"/> <link th:href="@{favicon.ico}" rel="shortcut icon"/>
@ -94,7 +95,9 @@
type: "post", type: "post",
dataType: "json", dataType: "json",
data: { password: password }, 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}); index = layer.load(2, {shade: false});
}, },
success: function(result) { success: function(result) {

View File

@ -33,9 +33,9 @@ public class ShiroConstants
public static final String ERROR = "errorMsg"; public static final String ERROR = "errorMsg";
/** /**
* * csrf key
*/ */
public static final String ENCODING = "UTF-8"; public static final String CSRF_TOKEN = "csrf_token";
/** /**
* 线 * 线

View File

@ -4,6 +4,8 @@ import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.security.SecureRandom;
import java.util.Base64;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; 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 final static String[] agent = { "Android", "iPhone", "iPod", "iPad", "Windows Phone", "MQQBrowser" };
private static final SecureRandom secureRandom = new SecureRandom();
/** /**
* String * String
*/ */
@ -213,4 +217,16 @@ public class ServletUtils
return StringUtils.EMPTY; return StringUtils.EMPTY;
} }
} }
/**
* CSRF Token
*
* @return
*/
public static String generateToken()
{
byte[] bytes = new byte[32];
secureRandom.nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes);
}
} }

View File

@ -357,6 +357,18 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
return new HashSet<String>(str2List(str, sep, true, false)); 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 * list
* *

View File

@ -33,6 +33,7 @@ import com.ruoyi.framework.shiro.session.OnlineSessionFactory;
import com.ruoyi.framework.shiro.web.CustomShiroFilterFactoryBean; import com.ruoyi.framework.shiro.web.CustomShiroFilterFactoryBean;
import com.ruoyi.framework.shiro.web.filter.LogoutFilter; import com.ruoyi.framework.shiro.web.filter.LogoutFilter;
import com.ruoyi.framework.shiro.web.filter.captcha.CaptchaValidateFilter; 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.kickout.KickoutSessionFilter;
import com.ruoyi.framework.shiro.web.filter.online.OnlineSessionFilter; import com.ruoyi.framework.shiro.web.filter.online.OnlineSessionFilter;
import com.ruoyi.framework.shiro.web.filter.sync.SyncOnlineSessionFilter; import com.ruoyi.framework.shiro.web.filter.sync.SyncOnlineSessionFilter;
@ -132,6 +133,18 @@ public class ShiroConfig
@Value("${shiro.rememberMe.enabled: false}") @Value("${shiro.rememberMe.enabled: false}")
private boolean rememberMe; private boolean rememberMe;
/**
* csrf
*/
@Value("${csrf.enabled: false}")
private boolean csrfEnabled;
/**
* csrf
*/
@Value("${csrf.whites: ''}")
private String csrfWhites;
/** /**
* 使Ehcache * 使Ehcache
*/ */
@ -263,6 +276,17 @@ public class ShiroConfig
return logoutFilter; return logoutFilter;
} }
/**
* csrf
*/
public CsrfValidateFilter csrfValidateFilter()
{
CsrfValidateFilter csrfValidateFilter = new CsrfValidateFilter();
csrfValidateFilter.setEnabled(csrfEnabled);
csrfValidateFilter.setCsrfWhites(StringUtils.str2List(csrfWhites, ","));
return csrfValidateFilter;
}
/** /**
* Shiro * Shiro
*/ */
@ -309,13 +333,14 @@ public class ShiroConfig
filters.put("onlineSession", onlineSessionFilter()); filters.put("onlineSession", onlineSessionFilter());
filters.put("syncOnlineSession", syncOnlineSessionFilter()); filters.put("syncOnlineSession", syncOnlineSessionFilter());
filters.put("captchaValidate", captchaValidateFilter()); filters.put("captchaValidate", captchaValidateFilter());
filters.put("csrfValidateFilter", csrfValidateFilter());
filters.put("kickout", kickoutSessionFilter()); filters.put("kickout", kickoutSessionFilter());
// 注销成功,则跳转到指定页面 // 注销成功,则跳转到指定页面
filters.put("logout", logoutFilter()); filters.put("logout", logoutFilter());
shiroFilterFactoryBean.setFilters(filters); shiroFilterFactoryBean.setFilters(filters);
// 所有请求需要认证 // 所有请求需要认证
filterChainDefinitionMap.put("/**", "user,kickout,onlineSession,syncOnlineSession"); filterChainDefinitionMap.put("/**", "user,kickout,onlineSession,syncOnlineSession,csrfValidateFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean; return shiroFilterFactoryBean;

View File

@ -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;
}
}