@@ -41,3 +41,54 @@ spring: | |||
- bpm | |||
caffeine: | |||
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 | |||
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, "内部服务错误,请稍后再试!"); | |||
@@ -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(); | |||
} | |||
} | |||