人人快速開(kāi)發(fā)平臺(tái) renren-fast 源碼分析(三)系統(tǒng)日志和 XSS 腳本過(guò)濾

系統(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, "&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;
    }

所以說(shuō)這個(gè)系統(tǒng)的 XSS 腳本過(guò)濾是將一些可能的 XSS 攻擊腳本都替換了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末属愤,一起剝皮案震驚了整個(gè)濱河市女器,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌住诸,老刑警劉巖驾胆,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涣澡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡丧诺,警方通過(guò)查閱死者的電腦和手機(jī)入桂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)驳阎,“玉大人抗愁,你說(shuō)我怎么就攤上這事『峭恚” “怎么了蜘腌?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)饵隙。 經(jīng)常有香客問(wèn)我撮珠,道長(zhǎng),這世上最難降的妖魔是什么金矛? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任芯急,我火速辦了婚禮,結(jié)果婚禮上驶俊,老公的妹妹穿的比我還像新娘娶耍。我一直安慰自己,他們只是感情好饼酿,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布榕酒。 她就那樣靜靜地躺著,像睡著了一般嗜湃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上澜掩,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天购披,我揣著相機(jī)與錄音,去河邊找鬼肩榕。 笑死刚陡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的株汉。 我是一名探鬼主播筐乳,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼乔妈!你這毒婦竟也來(lái)了蝙云?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤路召,失蹤者是張志新(化名)和其女友劉穎勃刨,沒(méi)想到半個(gè)月后波材,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡身隐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年廷区,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贾铝。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡隙轻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出垢揩,到底是詐尸還是另有隱情玖绿,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布水孩,位于F島的核電站镰矿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏俘种。R本人自食惡果不足惜秤标,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宙刘。 院中可真熱鬧苍姜,春花似錦、人聲如沸悬包。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)布近。三九已至垫释,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間撑瞧,已是汗流浹背棵譬。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留预伺,地道東北人订咸。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像酬诀,于是被迫代替她去往敵國(guó)和親脏嚷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容