Filter——拦截请求保护页面安全

David发表在天码营


在处理创建文章表单的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_FORBIDDENHttpServletResponse中定义的一个常量,它的值是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——拦截请求保护页面安全_第1张图片

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(),在该方法中可以拿到requestresponse对象并执行相应的逻辑。

下面我们把创建博文需要登录的逻辑放在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容器默认的错误提示页面,提示用户无权限访问。当用户看到这个错误提示页面,它可能会有多种选择——可能会回退到上一个页面,可能会关闭页面重新打开,无论哪种情况体验似乎都不友好。

其实这个过程可以优化为:如果用户尚未登录却访问了一个需要登录的页面,就先跳转到登录页面,用户登录成功后再跳转到他之前想要访问的目标页面,整个过程示意如下:

Filter——拦截请求保护页面安全_第2张图片

整个过程的重点就是在重定向到登录页面时在登录页面的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);
}
Filter——拦截请求保护页面安全_第3张图片




你可能感兴趣的:(java,jsp,Web,servlet,表单,filter)