美文网首页知识点renren-fast项目分析
人人快速开发平台 renren-fast 源码分析(三)系统日志

人人快速开发平台 renren-fast 源码分析(三)系统日志

作者: beetlebum | 来源:发表于2018-09-09 20:51 被阅读82次

    系统日志

    系统日志的实现实际上比较简单,是通过 Spring 的 AOP ,在需要记录日志的地方声明 @SysLog,然后在 AOP 处理类中判断方法是否声明了 @SysLog,如果声明了就保存日志。
    SysLogAspect.java

    /**
     * 系统日志,切面处理类
     * 
     * @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();
            //执行方法
            Object result = point.proceed();
            //执行时长(毫秒)
            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());
            }
    
            //请求的方法名
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = signature.getName();
            sysLog.setMethod(className + "." + methodName + "()");
    
            //请求的参数
            Object[] args = joinPoint.getArgs();
            try{
                String params = new Gson().toJson(args[0]);
                sysLog.setParams(params);
            }catch (Exception e){
    
            }
    
            //获取request
            HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
            //设置IP地址
            sysLog.setIp(IPUtils.getIpAddr(request));
    
            //用户名
            String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername();
            sysLog.setUsername(username);
    
            sysLog.setTime(time);
            sysLog.setCreateDate(new Date());
            //保存系统日志
            sysLogService.insert(sysLog);
        }
    }
    

    从切面处理类可以看出,系统日志的切入点是方法执行之后。通过反射获取调用方法的注解,判断是否具有@SysLog注解,如果有就往数据库插入调用日志。值得一提的是日志中插入的访问的 ip,这个 ip 是根据 spring web 的RequestContextHolder类获取一个 thread local 的 request 对象,然后根据 request 对象的header 来获取的。使用 header 来获取 ip 而不是 getRemoteAddr()的原因是如果使用了反向代理,该方法获取不到真实的 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等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
         * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实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);
            }
            
    //        //使用代理,则获取第一个IP地址
    //        if(StringUtils.isEmpty(ip) && ip.length() > 15) {
    //          if(ip.indexOf(",") > 0) {
    //              ip = ip.substring(0, ip.indexOf(","));
    //          }
    //      }
            
            return ip;
        }
        
    }
    

    本系统好像没有对具体的请求 url 进行拦截日志。其实也很简单,在 Controller 层进行拦截,然后获取方法的 Mapping 注解,拿到里面的值就行了。

    XSS脚本过滤

    这个项目的 XSS过滤是真的“过滤” ,而不是直接拒绝请求。
    从 Springboot 的配置,FilterConfig可以看到有个叫做XSSFilter的自定义过滤器。

    /**
     * XSS过滤
     * @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() {
        }
    
    }
    

    最重要的一行代码就是,这个 filter 将原始 request 包装成一个自定义的XssHttpServletRequestWrapper类。
    XssHttpServletRequestWrapper.java

    /**
     * XSS过滤处理
     * @author chenshun
     * @email sunlightcs@gmail.com
     * @date 2017-04-01 11:29
     */
    public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
        //没被包装过的HttpServletRequest(特殊场景,需要自己过滤)
        HttpServletRequest orgRequest;
        //html过滤
        private final static HTMLFilter htmlFilter = new HTMLFilter();
    
        public XssHttpServletRequestWrapper(HttpServletRequest request) {
            super(request);
            orgRequest = request;
        }
    
        @Override
        public ServletInputStream getInputStream() throws IOException {
            //非json类型,直接返回
            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过滤
            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,都调用了xssEncode()方法,尤其是 inputStream是先读取了一遍然后转成 string 调用了xssEncode()再读成一个输入流。至于xssEncode(),就是自定义的一个 htmlFilter的方法了。
    HTMLFilter这个类的代码太多,就不全部贴上了,简单的说就是将一些可能出现的 XSS 攻击脚本字符串都写到正则表达式中。一共有二十多个正则表达式

        /** 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("<>");
    

    然后将字符串用这些正则表达式匹配,并且替换字符串。下面是其中一个例子

        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, "&lt;$1", s);
                s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2&gt;<", 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;
        }
    

    所以说这个系统的 XSS 脚本过滤是将一些可能的 XSS 攻击脚本都替换了。

    相关文章

      网友评论

        本文标题:人人快速开发平台 renren-fast 源码分析(三)系统日志

        本文链接:https://www.haomeiwen.com/subject/sdmqgftx.html