mirror of https://gitee.com/y_project/RuoYi.git
新增CSRF防护功能
parent
407f9f46d8
commit
ea9976575a
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,6 +136,13 @@ xss:
|
||||||
# 匹配链接
|
# 匹配链接
|
||||||
urlPatterns: /system/*,/monitor/*,/tool/*
|
urlPatterns: /system/*,/monitor/*,/tool/*
|
||||||
|
|
||||||
|
# 防止csrf攻击
|
||||||
|
csrf:
|
||||||
|
# 过滤开关
|
||||||
|
enabled: true
|
||||||
|
# 白名单(多个用逗号分隔)
|
||||||
|
whites:
|
||||||
|
|
||||||
# Swagger配置
|
# Swagger配置
|
||||||
swagger:
|
swagger:
|
||||||
# 是否开启swagger
|
# 是否开启swagger
|
||||||
|
|
|
@ -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("服务器超时,请稍后再试!");
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前在线会话
|
* 当前在线会话
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
*
|
*
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue