文章目录
- 背景
- Jakarta MVC规范
- Eclipse Krazo
- 使用前的思考
- 全局配置
- Controller示例
- 返回View的三种写法
- View中用到的Model如何设值?
- View中如何获取Model中的值?
- 参数校验
- 防止CSRF
- Krazo是如何实现的呢?
- 如何生成csrf的token?
- 如何校验csrf呢?
- 自定义失败页面
- 其他用法以及配置
- 备注
背景
在N年以前,我估计在零几年?对我等95后开发者来说,可能意味着上古时代?提起MVC那时候还是Struts的时代,后来随着Spring的出现,SpringMVC开始渐渐被越来越多的开发者使用。Struts走向没落,中间还出现Vraptor,后来Struts2发布,Struts的生命彻底走到尽头。因为Spring不是Jakarta EE的规范,所以对于Jakarta EE来说一直缺少MVC规范,终于在近几年,社区开始建立MVC规范。
目前MVC框架大概是三家,SpringMVC, Struts2, Eclipse Krazo(Jakarta MVC规范的实现)。
Jakarta MVC规范
最新的规范是2.1.规范内容非常少,不复杂。建议阅读一遍规范,基本就知道怎么用了。Jakarta MVC是建立在Jakarta Restful Web Services(jakarta.ws.rs)之上的,并支持CDI
来自官方规范1.4节
Most of the terminology used in this specification is borrowed from other specifications such as Jakarta RESTful Web Services and Jakarta Contexts and Dependency Injection
关于Jakarta Restful Web Services的实现RestEasy已经在RestEasy的入门与使用这篇文章里做过详细的介绍,这里不做过多展开。
Eclipse Krazo
krazo官方网站,文档入口如下图
文档内容同样不多,估计几分钟就看完了。在一开始的时候,可以和上面的规范文档一起看。
使用前的思考
有使用过MVC框架的同学对MVC整体的开发流程是很清楚的,基本一个典型的业务流程是如下:
- 访问A页面,A.jsp/A.xhtml(jsf)。请求相应的Controller里的方法accessPageA(),将A页面用到的model设置进去,然后在A页面使用EL表达式进行取值。
- 进入A页面,进行一些操作,提交表单。请求相应的Controller里的submit()方法,对参数进行校验,处理业务,根据不同的结果跳转到不同的页面。
下面的例子就以这两步为例,给出一些简单的示例代码
全局配置
@ApplicationPath("mvc")
public class MvcApplication extends Application {/*** 配置文件 https://eclipse-ee4j.github.io/krazo/documentation/latest/index.html#_properties_default_view_file_extension_org_eclipse_krazo_defaultviewfileextension* @return*/@Overridepublic Map<String, Object> getProperties() {final Map<String, Object> properties = new HashMap<>();// 设置页面(View)文件夹properties.put(ViewEngine.DEFAULT_VIEW_FOLDER, "/WEB-INF/views/");properties.put(Properties.DEFAULT_VIEW_FILE_EXTENSION, "jsp");// 设置form表单的method属性,允许除了get和post之外的其他请求properties.put(FormMethodOverwriter.FORM_METHOD_OVERWRITE, Options.ENABLED);properties.put(FormMethodOverwriter.HIDDEN_FIELD_NAME, FormMethodOverwriter.DEFAULT_HIDDEN_FIELD_NAME);return properties;}
}
Controller示例
主要注解就是@Controller, 注意这里我的注解是放在class上的,代表该类下所有的方法都是返回到页面(View)。如果你只需要某个方法返回页面,其他方法是Restful的,那么只需要把Controller注解放到相应的方法上即可。
@RequestScoped
@Controller
@Path("test")
public class MvcController {@Injectprivate Models models;/*** 获得校验结果*/@Injectprivate BindingResult bindingResult;@GET@Path("helloMvc/{path}")public Response helloMvc(@PathParam("path") String path) {models.put("message", "Hello MVC, " + path);return Response.ok("helloMvc.jsp").build();}@DELETE@Path("deleteMvc")public String deleteMvc(@FormParam("message") @MvcBinding @NotBlank String message) {if (bindingResult.isFailed()) {models.put("errors", bindingResult.getAllMessages());return "deleteMvcResult.jsp";}models.put("message", message);return "deleteMvcResult.jsp";}@POST@Path("csrf")@CsrfProtected@View("csrf.jsp")public void csrf() {models.put("message", "csrf");}
}
返回View的三种写法
如上述代码所示
- 通过Response对象设置页面路径
- 返回String,String内容是页面路径
- 使用@View注解,注解内容是页面路径
View中用到的Model如何设值?
This specification supports two kinds of models: the first is based on CDI @Named beans, and the second on the Models interface which defines a map between names and objects. Jakarta MVC provides view engines for Jakarta Server Pages and Facelets out of the box, which support both types.
官方文档提供了Model设值两种方式,这里示例代码使用Models这个接口,直接通过CDI注入进来。
View中如何获取Model中的值?
以JSP为例,可以使用EL表达式或者request.getAttribute()方法
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--jstl 3.0的uri已经改变了--%>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<html>
<head><title>helloMvc</title>
</head>
<body>
<div>这是返回给页面的信息:${message}<br>这是使用jstl标签库的信息:<c:out value="${message}"/><br>
<%-- https://jakarta.ee/specifications/mvc/2.1/jakarta-mvc-spec-2.1.html#view_engines--%>这是使用request.getAttribute()方法获取的信息:<%=request.getAttribute("message")%><br>
</div>
<div><form action="${pageContext.request.contextPath}/mvc/test/deleteMvc" method="POST"><input type="hidden" name="_method" value="DELETE"/><input type="text" name="message" value="${message}"/><input type="submit" value="提交"/></form>
</div>
<div><form action="${pageContext.request.contextPath}/mvc/test/csrf" method="post">
<%-- https://jakarta.ee/specifications/mvc/2.1/jakarta-mvc-spec-2.1.html#mvc_context --%><input type="hidden" name="${mvc.csrf.name}" value="${mvc.csrf.token}"/><input type="submit" value="测试csrf"/></form>
</div>
</body>
</html>
为什么request.getAttribute能够获取到model的值呢?这是因为规范要求,实现此规范时,需要将model通过setAttribute的方式设置进去
在Krazo源码ServletViewEngine中也可以看到这一步
参数校验
使用@MvcBinding注解.使用方法如上述代码所示,关于各种校验注解可以直接使用Jakarta Bean Validation 相关注解。如果想要了解更多可以参考Hibernate-Validator使用这篇博文。
当校验失败时,我们这里的处理方式是,把校验失败的信息放到errors中,并返回一个页面,在页面中展示。
防止CSRF
要集成防止CSRF攻击,官方也提供了具体的规范要求和实现。如上述代码所示,只要在表单里添加一个隐藏域,隐藏域的name和value是EL表达式,在进入JSP页面时,通过EL表达式获得token,和在后端方法上加上@CsrfProtected注解即可
Krazo是如何实现的呢?
如何生成csrf的token?
上述所述,
${mvc.csrf.name}和${mvc.csrf.token}
这两个EL表达式完成了name和value的生成.关键源码如下
入口点是CsrfProtectFilter类
最终放到session里。
这里你可能会疑惑,为什么这里的EL表达式是mvc呢,那是因为
规范规定
规范实现
如何校验csrf呢?
入口点是CsrfValidateFilter
很简单,就是去session里拿一下,验证一下。如果校验不通过则抛出异常
自定义失败页面
虽然官方提供了一个默认的实现CsrfExceptionMapper,但是我们想CSRF校验失败时,自己控制跳到自己项目里的页面
@Provider
@Priority(Priorities.USER)
public class MvcExceptionHandler implements ExceptionMapper<CsrfValidationException> {@Contextprivate HttpServletResponse response;@Contextpublic HttpServletRequest request;@Contextprivate ServletContext servletContext;@SneakyThrows@Overridepublic Response toResponse(final CsrfValidationException e) {request.setAttribute("errors", e.getMessage());
// 两种写法都可以
// servletContext.getRequestDispatcher("/WEB-INF/views/csrf.jsp").forward(request, response);request.getServletContext().getRequestDispatcher("/WEB-INF/views/csrf.jsp").forward(request, response);return null;}
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<html>
<head><title>csrf</title>
</head>
<body>
<c:if test="${not empty errors}">csrf校验失败:<br><ul><c:forEach items="${errors}" var="error"><li>${error}</li></c:forEach></ul><br>
</c:if>
<c:if test="${not empty message}">${message} 校验成功
</c:if>
</body>
</html>
其他用法以及配置
关于本篇博文中没有提到的其他配置以及用法,可以参考Krazo官方文档 Configuration一节
备注
- 示例代码仓库