CsrfGuardFilter分析
一、介绍
前面两章 已经对 Listener 进行了分析, 本章主要 分析一下CsrfGuardFilter
二、CsrfGuardFilter 分析
CsrfGuardFilter 主要 就是对 请求进行拦截, 然后根据配置 设置要不要校验, 校验时 根据 ajax , page, session 等
不同情况分别处理 ,最后再更新 token.
public final class CsrfGuardFilter implements Filter {
private FilterConfig filterConfig = null;
// 初始化时
@Override
public void init(@SuppressWarnings("hiding") FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
// 首先判断 配置文件里面 org.owasp.csrfguard.Enabled 是否 开启, 默认是开启的
// 如果为 false, 那就直接跳过
if (!CsrfGuard.getInstance().isEnabled()) {
filterChain.doFilter(request, response);
return;
}
/** 仅适用于HttpServletRequest对象 **/
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 获取session
HttpSession session = httpRequest.getSession(false);
/**如果session 为null 需要直接跳过,可以将 配置文件里面 org.owasp.csrfguard.ValidateWhenNoSessionExists 设置为 false(默认为 true) ,即可跳过
**/
if (session == null && !CsrfGuard.getInstance().isValidateWhenNoSessionExists()) {
// If there is no session, no harm can be done
filterChain.doFilter(httpRequest, (HttpServletResponse) response);
return;
}
CsrfGuard csrfGuard = CsrfGuard.getInstance();
// 打印log 下面有对log 详细介绍,
csrfGuard.getLogger().log(String.format("CsrfGuard analyzing request %s", httpRequest.getRequestURI()));
// 转换成InterceptRedirectResponse 类型
InterceptRedirectResponse httpResponse = new InterceptRedirectResponse((HttpServletResponse) response, httpRequest, csrfGuard);
// if(MultipartHttpServletRequest.isMultipartRequest(httpRequest)) {
// httpRequest = new MultipartHttpServletRequest(httpRequest);
// }
// 如果session 不为null ,但是是新的,并且 使用新Token 登录页面 ,默认是false,这里的if 逻辑走不到
if ((session != null && session.isNew()) && csrfGuard.isUseNewTokenLandingPage()) {
// 创建新Token 并 建 页面
csrfGuard.writeLandingPage(httpRequest, httpResponse);
// 进行验证
} else if (csrfGuard.isValidRequest(httpRequest, httpResponse)) {
filterChain.doFilter(httpRequest, httpResponse);
} else {
/** invalid request - nothing to do - actions already executed **/
}
/** 最后 再更新 token **/
csrfGuard.updateTokens(httpRequest);
} else {
// 打印log
filterConfig.getServletContext().log(String.format("[WARNING] CsrfGuard does not know how to work with requests of class %s ", request.getClass().getName()));
filterChain.doFilter(request, response);
}
}
// 销毁
@Override
public void destroy() {
filterConfig = null;
}
}
三、 CsrfGuard 类分析
3.1 isValidRequest 分析
public boolean isValidRequest(HttpServletRequest request, HttpServletResponse response) {
// 首先判断是否是需要保护的页面和方法
boolean valid = !isProtectedPageAndMethod(request);
HttpSession session = request.getSession(true);
String tokenFromSession = (String) session.getAttribute(getSessionKey());
/** 需要保护的页面资源, token 不为null, 进行校验Token **/
if (tokenFromSession != null && !valid) {
try {
// 如果开启了ajax 请求, org.owasp.csrfguard.Ajax 对应的值
// 并且是 ajax 请求, request 头部的 X-Requested-With 不为null, 有一个 值为XMLHttpRequest ,这个是在 csrfguard.js 里面 配置的
if (isAjaxEnabled() && isAjaxRequest(request)) {
verifyAjaxToken(request);
// 如果是 page 验证
} else if (isTokenPerPageEnabled()) {
verifyPageToken(request);
} else {
// session 验证
verifySessionToken(request);
}
// 修改flag 标志
valid = true;
} catch (CsrfGuardException csrfe) {
// 如果验证不通过, 调用对应的 action , action 对应的 操作 在下面有介绍
callActionsOnError(request, response, csrfe);
}
/** 旋转 session 和 page tokens
如果 不是ajax 请求, 并且开启了 旋转替换token ,
那就对 session 和page 对应的tokens 替换掉
**/
if (!isAjaxRequest(request) && isRotateEnabled()) {
rotateTokens(request);
}
/** expected token in session - bad state and not valid **/
} else if (tokenFromSession == null && !valid) {
try {
throw new CsrfGuardException("CsrfGuard expects the token to exist in session at this point");
} catch (CsrfGuardException csrfe) {
callActionsOnError(request, response, csrfe);
}
} else {
/** unprotected page - nothing to do **/
}
return valid;
}
3.2 isProtectedPage 分析
public boolean isProtectedPage(String uri) {
// 如果请求路径在 javascript 对应的Set<> 集合里面,直接返回false
if (JavaScriptServlet.getJavascriptUris().contains(uri)) {
return false;
}
// 从配置文件里面获取 org.owasp.csrfguard.Protect 对应的配置
boolean retval = !isProtectEnabled();
// 对需要保护的页面进行 遍历
for (String protectedPage : getProtectedPages()) {
//精确匹配
if (isUriExactMatch(protectedPage, uri)) {
return true;
} else if (isUriMatch(protectedPage, uri)) {
retval = true;
}
}
// 对不需要保护的进行 匹配
for (String unprotectedPage : getUnprotectedPages()) {
if (isUriExactMatch(unprotectedPage, uri)) {
return false;
} else if (isUriMatch(unprotectedPage, uri)) {
retval = false;
}
}
return retval;
}
private boolean isUriExactMatch(String testPath, String requestPath) {
// 粗略判断一下 是不是 正则表达式, 以 "^" 开头, 以 "$" 结尾
// 如果是正则表达式, 直接返回false
if (isTestPathRegex(testPath)) {
return false;
}
boolean retval = false;
/** 精确匹配 **/
if (testPath.equals(requestPath)) {
retval = true;
}
return retval;
}
3.2.1 模糊匹配
这段代码是 拷贝的 tomcat 源码,tomcat 里面ApplicationFilterFactory 类
private boolean isUriMatch(String testPath, String requestPath) {
// 如果是正则表达式
if (isTestPathRegex(testPath)) {
// 获取对应的 pattern
Pattern pattern = this.regexPatternCache.get(testPath);
// 如果为null, 新建并放入缓存
if (pattern == null) {
pattern = Pattern.compile(testPath);
this.regexPatternCache.put(testPath, pattern);
}
// 返回是否匹配
return pattern.matcher(requestPath).matches();
}
boolean retval = false;
/** Case 1: 精确匹配 **/
if (testPath.equals(requestPath)) {
retval = true;
}
/** Case 2 - Path Match ("/.../*") **/
if (testPath.equals("/*")) {
retval = true;
}
if (testPath.endsWith("/*")) {
if (testPath
.regionMatches(0, requestPath, 0, testPath.length() - 2)) {
if (requestPath.length() == (testPath.length() - 2)) {
retval = true;
} else if ('/' == requestPath.charAt(testPath.length() - 2)) {
retval = true;
}
}
}
/** Case 3 - Extension Match **/
if (testPath.startsWith("*.")) {
int slash = requestPath.lastIndexOf('/');
int period = requestPath.lastIndexOf('.');
if ((slash >= 0)
&& (period > slash)
&& (period != requestPath.length() - 1)
&& ((requestPath.length() - period) == (testPath.length() - 1))) {
retval = testPath.regionMatches(2, requestPath, period + 1,
testPath.length() - 2);
}
}
return retval;
}
3.3 isProtectedMethod 分析
判断是否是需要保护的方法
public boolean isProtectedMethod(String method) {
boolean isProtected = true;
{
Set<String> theProtectedMethods = getProtectedMethods();
if (!theProtectedMethods.isEmpty() && !theProtectedMethods.contains(method)) {
isProtected = false;
}
}
{
Set<String> theUnprotectedMethods = getUnprotectedMethods();
if (!theUnprotectedMethods.isEmpty() && theUnprotectedMethods.contains(method)) {
isProtected = false;
}
}
return isProtected;
}
3.4 verifyAjaxToken、verifyPageToken、verifySessionToken分析
ajax ,page, session 请求校验 , 这三块的逻辑差不多
private void verifyAjaxToken(HttpServletRequest request) throws CsrfGuardException {
HttpSession session = request.getSession(true);
String tokenFromSession = (String) session.getAttribute(getSessionKey());
String tokenFromRequest = request.getHeader(getTokenName());
// 如果请求头部没有携带 Token, 抛错
if (tokenFromRequest == null) {
/** FAIL: token is missing from the request **/
throw new CsrfGuardException("required token is missing from the request");
} else {
// 如果session 里面的token 和 request 里面携带的token 不相等 ,做一个进一步的判断
if (!tokenFromSession.equals(tokenFromRequest)) {
// 如果request 里面的token 包含 "," , 进行split, 并取第一个
if (tokenFromRequest.contains(",")) {
tokenFromRequest = tokenFromRequest.substring(0, tokenFromRequest.indexOf(',')).trim();
}
// 如果还不相等,抛错
if (!tokenFromSession.equals(tokenFromRequest)) {
/** FAIL: the request token does not match the session token **/
throw new CsrfGuardException("request token does not match session token");
}
}
}
}
//
private void verifyPageToken(HttpServletRequest request) throws CsrfGuardException {
HttpSession session = request.getSession(true);
@SuppressWarnings("unchecked")
Map<String, String> pageTokens = (Map<String, String>) session.getAttribute(CsrfGuard.PAGE_TOKENS_KEY);
String tokenFromPages = (pageTokens != null ? pageTokens.get(request.getRequestURI()) : null);
String tokenFromSession = (String) session.getAttribute(getSessionKey());
String tokenFromRequest = request.getParameter(getTokenName());
if (tokenFromRequest == null) {
/** FAIL: token is missing from the request **/
throw new CsrfGuardException("required token is missing from the request");
} else if (tokenFromPages != null) {
if (!tokenFromPages.equals(tokenFromRequest)) {
/** FAIL: request does not match page token **/
throw new CsrfGuardException("request token does not match page token");
}
} else if (!tokenFromSession.equals(tokenFromRequest)) {
/** FAIL: the request token does not match the session token **/
throw new CsrfGuardException("request token does not match session token");
}
}
private void verifySessionToken(HttpServletRequest request) throws CsrfGuardException {
HttpSession session = request.getSession(true);
String tokenFromSession = (String) session.getAttribute(getSessionKey());
String tokenFromRequest = request.getParameter(getTokenName());
if (tokenFromRequest == null) {
/** FAIL: token is missing from the request **/
throw new CsrfGuardException("required token is missing from the request");
} else if (!tokenFromSession.equals(tokenFromRequest)) {
/** FAIL: the request token does not match the session token **/
throw new CsrfGuardException("request token does not match session token");
}
}
页面请求校验
四、ILogger 分析
定义了一个 ILogger 接口, 并给出了 两种实现方式: JavaLooger 和 ConsoleLogger
可以通过 配置 文件里面 org.owasp.csrfguard.Logger=org.owasp.csrfguard.log.ConsoleLogger 选择 具体的某一种打印日志的方式,
4.1 ConsoleLogger
ConsoleLogger : 就是 System.out.println 输出
4.2 JavaLogger
JavaLogger: java.util.logging.Logger 是JDK自带的日志工具,其简单实现了日志的功能,不是很完善,所以在实际应用中使用的比较少。
Logger的默认配置,位置在JRE安装目录下lib中的logging.properties中,大致如下图:
其实这两种都不太好.
五、InterceptRedirectResponse
InterceptRedirectResponse主要是对 sendRedirect 这个方法重写了, 主要逻辑是更新了 token , 对 url 进行了重新的拼装, 还是调用 原先的response(response.sendRedirect(xx))
六、 IAction
下图为 IAction 的 关系,
IAction 主要是出现异常之后的 处理和跳转相关, 这里可以配置多个 action进行处理. IAction 接口被 AbstractAction 实现,被 如下方法 继承, 主要看一下 每一个方法对应的 execute() 方法, 这里也可以自定义 action.
- Empty: do nothing
- Error : 向客户端发送错误信息, code和message 自己定义
- Forward : 请求转发到 对应的页面, page 自己定义
- Invalidate : 将session 置位无效
- Log : 打印log ,这里 可以打印 一些 ip, host, port , 等等
- Redirect: 重定向
- RequestAttribute : 设置 request 属性
- Rotate : 更新Token
- SessionAttribute : 设置session 属性
七、小结
本章主要介绍了 CsrfGuardFilter 这块的逻辑 以及设计到的 类,方法,整体都是比较好理解的.