@@ -41,3 +41,54 @@ spring: | |||||
- bpm | - bpm | ||||
caffeine: | caffeine: | ||||
spec: maximumSize=1024,expireAfterWrite=2h | spec: maximumSize=1024,expireAfterWrite=2h | ||||
# http 规则配置 | |||||
http-config: | |||||
xss: #xss 规则 | |||||
checkHeader: false #是否进行header校验 | |||||
checkParameter: true #是否进行parameter校验 | |||||
logIs: true #是否记录日志 | |||||
chain: true #是否中断请求 | |||||
replace: true #是否开启特殊字符替换 | |||||
checkUrl: true #是否开启特殊url校验 | |||||
regex: | |||||
# 匹配含有字符: alert( ) | |||||
- .*[A|a][L|l][E|e][R|r][T|t](.*).* | |||||
# 匹配含有字符: window.location | |||||
- .*[W|w][I|i][N|n][D|d][O|o][W|w].[L|l][O|o][C|c][A|a][T|t][I|i][O|o][N|n].* | |||||
# 匹配含有字符:style = x:ex pression ( ) | |||||
- .*[S|s][T|t][Y|y][L|l][E|e]\\s*=.*[X|x]:[E|e][X|x].*[P|p][R|r][E|e][S|s]{1,2}[I|i][O|o][N|n]\\s*\\(.*\\).* | |||||
# 匹配含有字符: document.cookie | |||||
- .*[D|d][O|o][C|c][U|u][M|m][E|e][N|n][T|t].[C|c][O|o]{2}[K|k][I|i][E|e].* | |||||
# 匹配含有字符: eval( ) | |||||
- .*[E|e][V|v][A|a][L|l](.*).* | |||||
# 匹配含有字符: unescape() | |||||
- .*[U|u][N|n][E|e][S|s][C|c][A|a][P|p][E|e](.*).* | |||||
# 匹配含有字符: execscript( ) | |||||
- .*[E|e][X|x][E|e][C|c][S|s][C|c][R|r][I|i][P|p][T|t](.*).* | |||||
# 匹配含有字符: msgbox( ) | |||||
- .*[M|m][S|s][G|g][B|b][O|o][X|x](.*).* | |||||
# 匹配含有字符: confirm( ) | |||||
- .*[C|c][O|o][N|n][F|f][I|i][R|r][M|m](.*).* | |||||
# 匹配含有字符: prompt( ) | |||||
- .*[P|p][R|r][O|o][M|m][P|p][T|t](.*).* | |||||
# 匹配含有字符: <script> </script> | |||||
- .*<[S|s][C|c][R|r][I|i][P|p][T|t]>.*.*</[S|s][C|c][R|r][I|i][P|p][T|t]>.* | |||||
# 匹配含有字符: </script> | |||||
- .*</[S|s][C|c][R|r][I|i][P|p][T|t]>.* | |||||
# 匹配含有字符: <script> | |||||
- .*<[S|s][C|c][R|r][I|i][P|p][T|t]>.* | |||||
res: # 响应头规则 | |||||
configs: | |||||
{ | |||||
Set-Cookie: Secure; HttpOnly, | |||||
Referrer-Policy: origin-when-cross-origin, | |||||
X-Frame-Options: SAMEORIGIN, | |||||
X-XSS-Protection: 1;mode=block , | |||||
X-Download-Options: SAMEORIGIN , | |||||
X-Content-Type-Options: nosniff , | |||||
Content-Security-Policy: default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:7102 https://localhost:7102; style-src 'self' 'unsafe-inline'; connect-src 'self';font-src 'self' data:; , | |||||
Strict-Transport-Security: max-age=31536000;includeSubDomains , | |||||
X-Permitted-Cross-Domain-Policies: master-only | |||||
} |
@@ -32,6 +32,9 @@ public class ExceptionMessage { | |||||
// 404 | // 404 | ||||
public static ExceptionMessage NOT_FOUND_ERROR = new ExceptionMessage(404, "没有找到需要资源!"); | public static ExceptionMessage NOT_FOUND_ERROR = new ExceptionMessage(404, "没有找到需要资源!"); | ||||
// 405 | |||||
public static ExceptionMessage NOT_ALLOWED_ERROR = new ExceptionMessage(405, "访问被阻断!"); | |||||
// 通用异常 | // 通用异常 | ||||
public static ExceptionMessage INNER_ERROR = new ExceptionMessage(100100, "内部服务错误,请稍后再试!"); | public static ExceptionMessage INNER_ERROR = new ExceptionMessage(100100, "内部服务错误,请稍后再试!"); | ||||
@@ -0,0 +1,32 @@ | |||||
package cc.smtweb.framework.core.mvc.filter; | |||||
import lombok.Data; | |||||
import org.springframework.boot.context.properties.ConfigurationProperties; | |||||
import org.springframework.context.annotation.Configuration; | |||||
import org.springframework.stereotype.Component; | |||||
import java.util.Map; | |||||
/** | |||||
* @Author yaoq | |||||
* @Date 2022年07月20日 10:38 | |||||
* @Description | |||||
*/ | |||||
@Data | |||||
@Component | |||||
@Configuration | |||||
@ConfigurationProperties(prefix = "http-config.res") | |||||
public class HttpSecurityConfig { | |||||
private Map<String, String> configs; | |||||
@Override | |||||
public String toString() { | |||||
if (configs == null) { | |||||
return ""; | |||||
} | |||||
return configs.toString(); | |||||
} | |||||
} | |||||
@@ -0,0 +1,113 @@ | |||||
package cc.smtweb.framework.core.mvc.filter; | |||||
import cc.smtweb.framework.core.common.R; | |||||
import cc.smtweb.framework.core.exception.ExceptionMessage; | |||||
import cc.smtweb.framework.core.util.JsonUtil; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.springframework.beans.factory.annotation.Autowired; | |||||
import org.springframework.stereotype.Component; | |||||
import org.springframework.web.filter.OncePerRequestFilter; | |||||
import javax.servlet.FilterChain; | |||||
import javax.servlet.ServletException; | |||||
import javax.servlet.http.HttpServletRequest; | |||||
import javax.servlet.http.HttpServletRequestWrapper; | |||||
import javax.servlet.http.HttpServletResponse; | |||||
import java.io.IOException; | |||||
import java.io.PrintWriter; | |||||
/** | |||||
* @Author yaoq | |||||
* @Date 2022年07月20日 9:28 | |||||
* @Description | |||||
*/ | |||||
@Slf4j | |||||
@Component | |||||
public class HttpSecurityFilter extends OncePerRequestFilter { | |||||
@Autowired | |||||
private HttpSecurityConfig httpConfig; | |||||
@Autowired | |||||
private XssSecurityConfig xssConfig; | |||||
@Override | |||||
public void afterPropertiesSet() throws ServletException { | |||||
} | |||||
@Override | |||||
public void initFilterBean() throws ServletException { | |||||
log.info("HttpSecurityFilter init begin "); | |||||
log.info("XssSecurityRegex:" + xssConfig.getRegexStr()); | |||||
log.info("HttpSecurityRegex:" + httpConfig.toString()); | |||||
log.info("HttpSecurityFilter init end "); | |||||
} | |||||
/** | |||||
* 销毁操作 | |||||
*/ | |||||
public void destroy() { | |||||
System.out.println("HttpSecurityFilter destroy"); | |||||
} | |||||
@Override | |||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | |||||
//httpResponse 响应头 | |||||
if (httpConfig.getConfigs() != null && httpConfig.getConfigs().size() > 0) { | |||||
httpConfig.getConfigs().forEach(response::setHeader); | |||||
} | |||||
//Xss 校验 | |||||
XssHttpRequestWrapper xssRequest = new XssHttpRequestWrapper(request, xssConfig); | |||||
// 对request信息进行封装并进行校验工作,若校验失败(含非法字符),根据配置信息进行日志记录和请求中断处理 | |||||
if (xssRequest.validateParameter(response)) { | |||||
log.error("XSS校验:请求参数存在XSS攻击行为!"); | |||||
if (xssConfig.isLogIs()) { | |||||
// 记录攻击访问日志 | |||||
// 可使用数据库、日志、文件等方式 | |||||
} | |||||
if (xssConfig.isChain()) { | |||||
R r = R.error("很抱歉,您提交的信息中含有非法信息,可能会对系统安全带来影响,您的请求被阻断!"); | |||||
if (request.getMethod().equals("GET")) { | |||||
r = R.error(ExceptionMessage.NOT_ALLOWED_ERROR.getCode(), ExceptionMessage.NOT_ALLOWED_ERROR.getMsg()); | |||||
} | |||||
writeResponseJson(response, JsonUtil.encodeString(r)); | |||||
return; | |||||
} | |||||
} | |||||
filterChain.doFilter(xssRequest, response); | |||||
} | |||||
public void writeResponseJson(HttpServletResponse response, String ret) { | |||||
response.setHeader("Pragma", "No-cache"); | |||||
response.setHeader("Cache-Control", "no-cache"); | |||||
response.setDateHeader("Expires", 0L); | |||||
response.setContentType("application/json;charset=UTF-8"); | |||||
PrintWriter writer = null; | |||||
try { | |||||
writer = response.getWriter(); | |||||
writer.write(ret); | |||||
writer.flush(); | |||||
} catch (Exception e) { | |||||
log.error("response error", e); | |||||
} finally { | |||||
if (writer != null) { | |||||
writer.close(); | |||||
} | |||||
} | |||||
} | |||||
class GetRequest extends HttpServletRequestWrapper { | |||||
public GetRequest(HttpServletRequest request) { | |||||
super(request); | |||||
} | |||||
@Override | |||||
public String getMethod() { | |||||
return "GET"; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,115 @@ | |||||
package cc.smtweb.framework.core.mvc.filter; | |||||
import javax.servlet.ServletException; | |||||
import javax.servlet.http.HttpServletRequest; | |||||
import javax.servlet.http.HttpServletRequestWrapper; | |||||
import javax.servlet.http.HttpServletResponse; | |||||
import java.io.IOException; | |||||
import java.util.Enumeration; | |||||
import java.util.Map; | |||||
import java.util.Set; | |||||
public class XssHttpRequestWrapper extends HttpServletRequestWrapper { | |||||
private XssSecurityConfig config; | |||||
/** | |||||
* 封装http请求 | |||||
* | |||||
* @param request | |||||
*/ | |||||
public XssHttpRequestWrapper(HttpServletRequest request, XssSecurityConfig config) { | |||||
super(request); | |||||
this.config = config; | |||||
} | |||||
@Override | |||||
public String getHeader(String name) { | |||||
String value = super.getHeader(name); | |||||
// 若开启特殊字符替换,对特殊字符进行替换 | |||||
if (config.isReplace()) { | |||||
config.securityReplace(name); | |||||
} | |||||
return value; | |||||
} | |||||
@Override | |||||
public String getParameter(String name) { | |||||
String value = super.getParameter(name); | |||||
// 若开启特殊字符替换,对特殊字符进行替换 | |||||
if (config.isReplace()) { | |||||
config.securityReplace(name); | |||||
} | |||||
return value; | |||||
} | |||||
/** | |||||
* 没有违规的数据,就返回false; | |||||
* | |||||
* @return | |||||
*/ | |||||
@SuppressWarnings("unchecked") | |||||
private boolean checkHeader() { | |||||
Enumeration<String> headerParams = this.getHeaderNames(); | |||||
while (headerParams.hasMoreElements()) { | |||||
String headerName = headerParams.nextElement(); | |||||
String headerValue = this.getHeader(headerName); | |||||
if (config.matches(headerValue)) { | |||||
//UtilLogger.debug("XSS校验:检测到Header攻击:[" + headerValue + "]"); | |||||
return true; | |||||
} | |||||
} | |||||
return false; | |||||
} | |||||
/** | |||||
* 没有违规的数据,就返回false; | |||||
* | |||||
* @return | |||||
*/ | |||||
@SuppressWarnings("unchecked") | |||||
private boolean checkParameter() { | |||||
Map<String, String[]> submitParams = this.getParameterMap(); | |||||
Set<String> submitNames = submitParams.keySet(); | |||||
for (String submitName : submitNames) { | |||||
Object submitValues = submitParams.get(submitName); | |||||
if (submitValues instanceof String) { | |||||
if (config.matches((String) submitValues)) { | |||||
//UtilLogger.debug("XSS校验:检测到Parameter攻击:[" + submitValues + "]"); | |||||
return true; | |||||
} | |||||
} else if (submitValues instanceof String[]) { | |||||
for (String submitValue : (String[]) submitValues) { | |||||
if (config.matches((String) submitValue)) { | |||||
//UtilLogger.debug("XSS校验:检测到Parameter攻击:[" + submitValue + "]"); | |||||
return true; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
return false; | |||||
} | |||||
/** | |||||
* 没有违规的数据,就返回false; | |||||
* 若存在违规数据,根据配置信息判断是否跳转到错误页面 | |||||
* | |||||
* @param response | |||||
* @return | |||||
* @throws IOException | |||||
* @throws ServletException | |||||
*/ | |||||
public boolean validateParameter(HttpServletResponse response) throws ServletException, IOException { | |||||
// 开始header校验,对header信息进行校验 | |||||
if (config.isCheckHeader()) { | |||||
return this.checkHeader(); | |||||
} | |||||
// 开始parameter校验,对parameter信息进行校验 | |||||
if (config.isCheckParameter()) { | |||||
return this.checkParameter(); | |||||
} | |||||
return false; | |||||
} | |||||
} |
@@ -0,0 +1,112 @@ | |||||
package cc.smtweb.framework.core.mvc.filter; | |||||
import lombok.Data; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.apache.commons.lang3.StringUtils; | |||||
import org.springframework.beans.factory.InitializingBean; | |||||
import org.springframework.boot.context.properties.ConfigurationProperties; | |||||
import org.springframework.stereotype.Component; | |||||
import java.util.List; | |||||
import java.util.regex.Pattern; | |||||
/** | |||||
* @Author yaoq | |||||
* @Date 2022年07月20日 10:38 | |||||
* @Description | |||||
*/ | |||||
@Slf4j | |||||
@Component | |||||
@Data | |||||
@ConfigurationProperties(prefix = "http-config.xss") | |||||
public class XssSecurityConfig implements InitializingBean { | |||||
private boolean checkHeader; | |||||
private boolean checkParameter; | |||||
private boolean logIs; | |||||
private boolean chain; | |||||
private boolean replace; | |||||
private boolean checkUrl; | |||||
private List<String> regex; | |||||
/** | |||||
* 替换非法字符的字符串 | |||||
*/ | |||||
private String REPLACEMENT = ""; | |||||
/** | |||||
* FILTER_ERROR_PAGE:过滤后错误页面 | |||||
*/ | |||||
private String FILTER_ERROR_PAGE = "/jsp/framework/error/405.jsp"; | |||||
/** | |||||
* REGEX:校验正则表达式 | |||||
*/ | |||||
private String regexStr; | |||||
/** | |||||
* 特殊字符匹配 | |||||
*/ | |||||
private Pattern XSS_PATTERN; | |||||
@Override | |||||
public void afterPropertiesSet() throws Exception { | |||||
StringBuffer tempStr = new StringBuffer("^"); | |||||
regex.forEach(k -> { | |||||
tempStr.append(k); | |||||
tempStr.append("|"); | |||||
}); | |||||
regexStr = tempStr.substring(0, tempStr.length() - 1) + "$"; | |||||
XSS_PATTERN = Pattern.compile(regexStr); | |||||
} | |||||
@Override | |||||
public String toString() { | |||||
return "XssSecurityConfig{" + | |||||
"checkHeader=" + checkHeader + | |||||
", checkParameter=" + checkParameter + | |||||
", logIs=" + logIs + | |||||
", chain=" + chain + | |||||
", replace=" + replace + | |||||
", checkUrl=" + checkUrl + | |||||
", regex=" + regex + | |||||
'}'; | |||||
} | |||||
/** | |||||
* 对非法字符进行替换 | |||||
* | |||||
* @param text | |||||
* @return | |||||
*/ | |||||
public String securityReplace(String text) { | |||||
if (StringUtils.isEmpty(text)) { | |||||
return text; | |||||
} else { | |||||
return text.replaceAll(regexStr, REPLACEMENT); | |||||
} | |||||
} | |||||
/** | |||||
* 匹配字符是否含特殊字符 | |||||
* | |||||
* @param text | |||||
* @return | |||||
*/ | |||||
public boolean matches(String text) { | |||||
if (StringUtils.isEmpty(text)) { | |||||
return false; | |||||
} | |||||
return XSS_PATTERN.matcher(text).matches(); | |||||
} | |||||
} | |||||