非Spring环境下Java EE JSF应用CSRF防护实现方案咨询
JSF 2.2及以上版本本身内置了基于ViewState的CSRF防护能力,和PrimeFaces组件全版本兼容,不需要额外引入OWASP CSRF Guard或者Spring生态组件,零额外依赖就能满足防护要求。如果使用JSF 2.2以下的老版本,也可以通过简单的自定义过滤器实现同等防护能力。
基础防护配置(JSF 2.2+ 适用)
首先在项目web.xml中确认/添加如下配置,打开默认的防护开关,不要手动关闭ViewState校验:
<!-- ViewState存在服务端,避免令牌被客户端篡改 --> <context-param> <param-name>javax.faces.STATE_SAVING_METHOD</param-name> <param-value>server</param-value> </context-param> <!-- 显式开启视图参数保护,默认JSF2.2+为true,避免被其他配置覆盖 --> <context-param> <param-name>javax.faces.PROTECT_VIEW_PARAMETERS</param-name> <param-value>true</param-value> </context-param> <!-- 配置所有涉及数据修改的受保护视图路径,这些路径的非GET请求会强制校验ViewState --> <context-param> <param-name>javax.faces.PROTECTED_VIEWS</param-name> <param-value>/admin/*,/user/secure/*,/order/*,/api/modify/*</param-value> </context-param>
配置完成后,所有受保护路径的POST请求、PrimeFaces发起的AJAX请求,JSF都会自动校验请求携带的javax.faces.ViewState参数和当前会话绑定的服务端存储值是否匹配,不匹配会直接抛出ProtectedViewException拒绝请求。PrimeFaces的p:commandButton、p:ajax等组件默认会自动在请求中携带ViewState参数,不需要额外开发前端传参逻辑。
注意:不要在PrimeFaces组件上设置process="@none"且不提交ViewState,会导致校验失败。如果有自定义的原生AJAX请求,从页面DOM中提取ViewState值携带即可:
// 自定义请求携带JSF原生CSRF令牌 const viewState = document.querySelector('input[name="javax.faces.ViewState"]').value; fetch('/custom/submit', { method: 'POST', headers: { 'Faces-Request': 'partial/ajax' }, body: new URLSearchParams({ 'javax.faces.ViewState': viewState, 'bizParam': 'value' }) })
兼容补充方案(JSF 2.2以下版本/非JSF接口适用)
如果项目使用JSF 2.0/2.1等不支持内置受保护视图的老版本,或者项目中存在独立Servlet、REST接口等不走JSF生命周期的请求,不需要集成OWASP CSRF Guard,写一个简单的Servlet Filter即可实现CSRF校验,总代码量不超过100行,逻辑完全可控:
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.UUID; @WebFilter("/*") public class CsrfValidationFilter implements Filter { private static final String CSRF_TOKEN_SESSION_KEY = "APP_CSRF_TOKEN"; private static final String CSRF_TOKEN_HEADER = "X-CSRF-TOKEN"; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; HttpSession session = req.getSession(); String method = req.getMethod(); // GET/HEAD/OPTIONS请求不涉及数据修改,跳过校验,首次访问时生成令牌 if ("GET".equals(method) || "HEAD".equals(method) || "OPTIONS".equals(method)) { if (session.getAttribute(CSRF_TOKEN_SESSION_KEY) == null) { String token = UUID.randomUUID().toString().replace("-", ""); session.setAttribute(CSRF_TOKEN_SESSION_KEY, token); } chain.doFilter(request, response); return; } // POST/PUT/DELETE等修改类请求校验令牌 String expectedToken = (String) session.getAttribute(CSRF_TOKEN_SESSION_KEY); String actualToken = req.getHeader(CSRF_TOKEN_HEADER); if (actualToken == null) { actualToken = req.getParameter(CSRF_TOKEN_SESSION_KEY); } if (expectedToken == null || !expectedToken.equals(actualToken)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF校验失败"); return; } chain.doFilter(request, response); } }
前端通过JSF EL表达式直接取出Session中存储的令牌,配置全局AJAX请求自动携带即可:
// 全局配置所有AJAX请求自动带CSRF令牌 const csrfToken = "#{sessionScope.APP_CSRF_TOKEN}"; // fetch全局拦截 const originalFetch = window.fetch; window.fetch = function(url, options = {}) { options.headers = options.headers || {}; options.headers[CSRF_TOKEN_HEADER] = csrfToken; return originalFetch(url, options); } // 如果项目用jQuery,加一行全局配置即可 $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': csrfToken } });
方案优势
- 零额外依赖:不需要引入Spring、OWASP CSRF Guard等第三方组件,完全基于Java EE原生API实现,不会出现依赖冲突
- 兼容性好:JSF内置ViewState校验和PrimeFaces生命周期完全适配,不会出现第三方过滤器拦截AJAX请求导致组件失效的问题
- 逻辑可控:自定义Filter代码量极小,可以根据业务需求灵活配置拦截路径、校验规则,不需要额外学习复杂的第三方配置规则
有效性验证
配置完成后可以通过两个简单步骤确认防护生效:
- 构造跨域测试页面,写一个不带ViewState/CSRF令牌的POST表单提交到受保护接口,确认返回403状态码
- 抓包查看正常业务请求,确认每个POST/AJAX请求都携带了和当前会话绑定的令牌,且不同会话的令牌值不重复
内容的提问来源于stack exchange,提问作者user13079485

