系統(tǒng)日志
系統(tǒng)日志的實(shí)現(xiàn)實(shí)際上比較簡(jiǎn)單暇唾,是通過(guò) Spring 的 AOP ,在需要記錄日志的地方聲明 @SysLog
,然后在 AOP 處理類(lèi)中判斷方法是否聲明了 @SysLog
刑赶,如果聲明了就保存日志推正。
SysLogAspect.java
/**
* 系統(tǒng)日志边酒,切面處理類(lèi)
*
* @author chenshun
* @email sunlightcs@gmail.com
* @date 2017年3月8日 上午11:07:35
*/
@Aspect
@Component
public class SysLogAspect {
@Autowired
private SysLogService sysLogService;
@Pointcut("@annotation(io.renren.common.annotation.SysLog)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
//執(zhí)行方法
Object result = point.proceed();
//執(zhí)行時(shí)長(zhǎng)(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
saveSysLog(point, time);
return result;
}
private void saveSysLog(ProceedingJoinPoint joinPoint, long time) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysLogEntity sysLog = new SysLogEntity();
SysLog syslog = method.getAnnotation(SysLog.class);
if(syslog != null){
//注解上的描述
sysLog.setOperation(syslog.value());
}
//請(qǐng)求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
sysLog.setMethod(className + "." + methodName + "()");
//請(qǐng)求的參數(shù)
Object[] args = joinPoint.getArgs();
try{
String params = new Gson().toJson(args[0]);
sysLog.setParams(params);
}catch (Exception e){
}
//獲取request
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
//設(shè)置IP地址
sysLog.setIp(IPUtils.getIpAddr(request));
//用戶(hù)名
String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername();
sysLog.setUsername(username);
sysLog.setTime(time);
sysLog.setCreateDate(new Date());
//保存系統(tǒng)日志
sysLogService.insert(sysLog);
}
}
從切面處理類(lèi)可以看出钧汹,系統(tǒng)日志的切入點(diǎn)是方法執(zhí)行之后扇苞。通過(guò)反射獲取調(diào)用方法的注解据某,判斷是否具有@SysLog
注解橡娄,如果有就往數(shù)據(jù)庫(kù)插入調(diào)用日志。值得一提的是日志中插入的訪問(wèn)的 ip癣籽,這個(gè) ip 是根據(jù) spring web 的RequestContextHolder
類(lèi)獲取一個(gè) thread local 的 request 對(duì)象挽唉,然后根據(jù) request 對(duì)象的header 來(lái)獲取的滤祖。使用 header 來(lái)獲取 ip 而不是 getRemoteAddr()
的原因是如果使用了反向代理,該方法獲取不到真實(shí)的 ip瓶籽。
/**
* IP地址
*
* @author chenshun
* @email sunlightcs@gmail.com
* @date 2017年3月8日 下午12:57:02
*/
public class IPUtils {
private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
/**
* 獲取IP地址
*
* 使用Nginx等反向代理軟件匠童, 則不能通過(guò)request.getRemoteAddr()獲取IP地址
* 如果使用了多級(jí)反向代理的話(huà),X-Forwarded-For的值并不止一個(gè)棘劣,而是一串IP地址俏让,X-Forwarded-For中第一個(gè)非unknown的有效IP字符串,則為真實(shí)IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = null;
try {
ip = request.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
logger.error("IPUtils ERROR ", e);
}
// //使用代理茬暇,則獲取第一個(gè)IP地址
// if(StringUtils.isEmpty(ip) && ip.length() > 15) {
// if(ip.indexOf(",") > 0) {
// ip = ip.substring(0, ip.indexOf(","));
// }
// }
return ip;
}
}
本系統(tǒng)好像沒(méi)有對(duì)具體的請(qǐng)求 url 進(jìn)行攔截日志首昔。其實(shí)也很簡(jiǎn)單,在 Controller 層進(jìn)行攔截糙俗,然后獲取方法的 Mapping 注解勒奇,拿到里面的值就行了。
XSS腳本過(guò)濾
這個(gè)項(xiàng)目的 XSS過(guò)濾是真的“過(guò)濾” 巧骚,而不是直接拒絕請(qǐng)求赊颠。
從 Springboot 的配置,FilterConfig
可以看到有個(gè)叫做XSSFilter
的自定義過(guò)濾器劈彪。
/**
* XSS過(guò)濾
* @author chenshun
* @email sunlightcs@gmail.com
* @date 2017-04-01 10:20
*/
public class XssFilter implements Filter {
@Override
public void init(FilterConfig config) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(
(HttpServletRequest) request);
chain.doFilter(xssRequest, response);
}
@Override
public void destroy() {
}
}
最重要的一行代碼就是竣蹦,這個(gè) filter 將原始 request 包裝成一個(gè)自定義的XssHttpServletRequestWrapper
類(lèi)。
XssHttpServletRequestWrapper.java
/**
* XSS過(guò)濾處理
* @author chenshun
* @email sunlightcs@gmail.com
* @date 2017-04-01 11:29
*/
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
//沒(méi)被包裝過(guò)的HttpServletRequest(特殊場(chǎng)景沧奴,需要自己過(guò)濾)
HttpServletRequest orgRequest;
//html過(guò)濾
private final static HTMLFilter htmlFilter = new HTMLFilter();
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
orgRequest = request;
}
@Override
public ServletInputStream getInputStream() throws IOException {
//非json類(lèi)型痘括,直接返回
if(!MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(super.getHeader(HttpHeaders.CONTENT_TYPE))){
return super.getInputStream();
}
//為空,直接返回
String json = IOUtils.toString(super.getInputStream(), "utf-8");
if (StringUtils.isBlank(json)) {
return super.getInputStream();
}
//xss過(guò)濾
json = xssEncode(json);
final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes("utf-8"));
return new ServletInputStream() {
@Override
public boolean isFinished() {
return true;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return bis.read();
}
};
}
@Override
public String getParameter(String name) {
String value = super.getParameter(xssEncode(name));
if (StringUtils.isNotBlank(value)) {
value = xssEncode(value);
}
return value;
}
@Override
public String[] getParameterValues(String name) {
String[] parameters = super.getParameterValues(name);
if (parameters == null || parameters.length == 0) {
return null;
}
for (int i = 0; i < parameters.length; i++) {
parameters[i] = xssEncode(parameters[i]);
}
return parameters;
}
@Override
public Map<String,String[]> getParameterMap() {
Map<String,String[]> map = new LinkedHashMap<>();
Map<String,String[]> parameters = super.getParameterMap();
for (String key : parameters.keySet()) {
String[] values = parameters.get(key);
for (int i = 0; i < values.length; i++) {
values[i] = xssEncode(values[i]);
}
map.put(key, values);
}
return map;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(xssEncode(name));
if (StringUtils.isNotBlank(value)) {
value = xssEncode(value);
}
return value;
}
private String xssEncode(String input) {
return htmlFilter.filter(input);
}
/**
* 獲取最原始的request
*/
public HttpServletRequest getOrgRequest() {
return orgRequest;
}
/**
* 獲取最原始的request
*/
public static HttpServletRequest getOrgRequest(HttpServletRequest request) {
if (request instanceof XssHttpServletRequestWrapper) {
return ((XssHttpServletRequestWrapper) request).getOrgRequest();
}
return request;
}
}
我們可以看到滔吠,不論是 header, params, 還是 inputStream纲菌,都調(diào)用了xssEncode()
方法,尤其是 inputStream是先讀取了一遍然后轉(zhuǎn)成 string 調(diào)用了xssEncode()
再讀成一個(gè)輸入流疮绷。至于xssEncode()
翰舌,就是自定義的一個(gè) htmlFilter
的方法了。
HTMLFilter
這個(gè)類(lèi)的代碼太多冬骚,就不全部貼上了椅贱,簡(jiǎn)單的說(shuō)就是將一些可能出現(xiàn)的 XSS 攻擊腳本字符串都寫(xiě)到正則表達(dá)式中。一共有二十多個(gè)正則表達(dá)式
/** regex flag union representing /si modifiers in php **/
private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
private static final Pattern P_COMMENTS = Pattern.compile("<!--(.*?)-->", Pattern.DOTALL);
private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI);
private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL);
private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI);
private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI);
private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI);
private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI);
private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI);
private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?");
private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?");
private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?");
private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))");
private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL);
private static final Pattern P_END_ARROW = Pattern.compile("^>");
private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)");
private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)");
private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)");
private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)");
private static final Pattern P_AMP = Pattern.compile("&");
private static final Pattern P_QUOTE = Pattern.compile("<");
private static final Pattern P_LEFT_ARROW = Pattern.compile("<");
private static final Pattern P_RIGHT_ARROW = Pattern.compile(">");
private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>");
然后將字符串用這些正則表達(dá)式匹配只冻,并且替換字符串夜涕。下面是其中一個(gè)例子
private String balanceHTML(String s) {
if (alwaysMakeTags) {
//
// try and form html
//
s = regexReplace(P_END_ARROW, "", s);
s = regexReplace(P_BODY_TO_END, "<$1>", s);
s = regexReplace(P_XML_CONTENT, "$1<$2", s);
} else {
//
// escape stray brackets
//
s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s);
s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s);
//
// the last regexp causes '<>' entities to appear
// (we need to do a lookahead assertion so that the last bracket can
// be used in the next pass of the regexp)
//
s = regexReplace(P_BOTH_ARROWS, "", s);
}
return s;
}
所以說(shuō)這個(gè)系統(tǒng)的 XSS 腳本過(guò)濾是將一些可能的 XSS 攻擊腳本都替換了。