在处理创建文章表单的Servlet中,创建了一个Post
对象,它的成员变量User
被设置为当前Session中的user
属性。从业务逻辑的角度来说,那么在创建文章时必须保证当前用户已经登录,对于没有登录的用户,不应该让它看到创建文章的表单页面。
为了实现这样的需求,我们可以直接在Servlet中编写类似逻辑:
User user = (User) req.getSession().getAttribute("user");
if (user == null) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no user in session");
}
SC_FORBIDDEN
是HttpServletResponse
中定义的一个常量,它的值是403
,这里的sendError
函数相当于向浏览器返回一个403 Forbidden
的状态码以及权限错误信息。
这一类需求在Web应用中非常的常见,尤其是多用户系统中用户身份的认证以及权限控制愈发重要。例如用户A不能访问用户B的文章管理页面并篡改B的文章。当我们的Web应用页面越来越多,需要类似权限控制的逻辑愈发复杂时,如果还在每一个Servlet中去编写这样的逻辑,不仅会出现大量重复的代码(可能很多页面的控制逻辑是相同的——例如用户已经登录才能访问),而且也违背了软件设计模式中的单一职责原则,Servlet既需要考虑处理HTTP请求,同时还需要考虑权限控制。
在Filter中实现权限控制逻辑
解决上面提到问题的办法其实也很简单——把权限控制逻辑从Servlet组件中提取出来,另外作为一种可复用的组件——不仅Servlet的权限控制可以使用,而且今后其它需要用到权限控制的地方同样可以用到。这样做的好处就是把Servlet的职责清晰化,并且和权限控制逻辑解耦。
Filter
是Servlet规范中非常有用的组件。Filter
被用于在Servlet/JSP等资源处理HTTP请求之前,对请求进行拦截处理,下图是Filter拦截HTTP请求的过程:
Filter在Servlet/JSP之前对HTTP请求进行拦截,可以同时存在多个Filter组成Filter链(Chain),在任意Filter中可以决定继续执行Filter链中的下一个Filter,还是把response
直接返回到浏览器。Filter链全部执行完成后,HTTP请求才会到达相应的Servlet/JSP中。以下是Servlet规范中Filter
接口的定义:
public interface Filter {
public void init(FilterConfig filterConfig) throws ServletException;
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
public void destroy();
}
和Servlet
类似,Filter
接口也需要实现生命周期中的init()
和destroy()
方法。它最重要的方法是doFilter()
,在该方法中可以拿到request
和response
对象并执行相应的逻辑。
下面我们把创建博文需要登录的逻辑放在Filter
中:
@WebFilter(filterName = "createPostFilter", urlPatterns = "/createPost")
public class CreatePostFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
User user = (User) request.getSession().getAttribute("user");
if (user == null) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no user in session");
return;
}
filterChain.doFilter(req, resp);
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
}
@WebFilter(filterName = "createPostFilter", urlPatterns = "/createPost")
表示CreatePostFilter
应该被Servlet容器作为一个Filter加载,它拦截所有URL路径为/createPost
的请求(包括GET和POST请求)。- 如果用户不存在,直接返回HTTP状态码403;如果存在,调用
filterChain.doFilter(req,resp)
继续执行Filter链中的下一个Filter。
这样我们把处理HTTP请求的业务逻辑放在了Servlet中,把权限控制的逻辑放在了Filter中,二者各司其职并且都是可以被复用的组件。
优化登录流程
在上一节描述的保护页面中,如果用户没有登录,直接通过URL(虽然无法从页面直接点击过去,但是可以在浏览器中直接输入)访问文章创建页面,那么它会得到一个Tomcat容器默认的错误提示页面,提示用户无权限访问。当用户看到这个错误提示页面,它可能会有多种选择——可能会回退到上一个页面,可能会关闭页面重新打开,无论哪种情况体验似乎都不友好。
其实这个过程可以优化为:如果用户尚未登录却访问了一个需要登录的页面,就先跳转到登录页面,用户登录成功后再跳转到他之前想要访问的目标页面,整个过程示意如下:
整个过程的重点就是在重定向到登录页面时在登录页面的url后面戴上了一个名字为next
的参数,这个参数代表着目标页面的url。一旦登录成功,处理登录请求的Servlet就会重定向到该url。
实现:Filter
在Filter中只需要通过request.getRequestURI()
获取当前受保护页面的url,将其作为参数重定向:
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
User user = (User) request.getSession().getAttribute("user");
if (user == null) {
HttpServletResponse response = (HttpServletResponse) resp;
response.sendRedirect("/ServletBlogDemo/account/login?next=" + request.getRequestURI());
return;
}
filterChain.doFilter(req, resp);
}
实现:Servlet
在LoginController
中需要获取next
参数并进行跳转:
//如果登录成功
String next = req.getParameter("next");
if (next == null || next.isEmpty()) {
resp.sendRedirect("/ServletBlogDemo/userPosts?username=" + user.getUsername());
} else {
resp.sendRedirect(next);
}