开发者

Springboot中使用拦截器、过滤器、监听器的流程分析

开发者 https://www.devze.com 2023-12-22 10:24 出处:网络 作者: 程序三两行
目录一、Servlet、Filter(过滤器)、 Listener(监听器)、Interceptor(拦截器)二、过滤器1、过滤器作用2、过滤器执行流程3、过滤器拦截路径4、过滤链5、过滤器三种实现方式第一种方式:利用Servlet3.0的Web
目录
  • 一、Servlet、Filter(过滤器)、 Listener(监听器)、Interceptor(拦截器)
  • 二、过滤器
    • 1、过滤器作用
    • 2、过滤器执行流程
    • 3、过滤器拦截路径
    • 4、过滤链
    • 5、过滤器三种实现方式
      • 第一种方式:利用Servlet3.0的WebFilter注解配置
      • 第二种方式:利用SpringBoot的配置类来添加过滤器
      • 第三种方式:Configuration+FilterRegistrationBean
    • 6、过滤器实际项目场景使用举例
    • 三、监听器
      • 四、拦截器
        • 1、拦截器作用
          • 2、拦截器执行流程
            • 3、拦截器拦截路径
              • 4、拦截器的使用
                • 5、拦截器使用场景举例

                一、Servlet、Filter(过滤器)、 Listener(监听器)、Interceptor(拦截器)

                Javaweb三大组件:servlet、Filter(过滤器)、 Listener(监听器)

                SpringBoot特有组件:Interceptor(拦截器)

                过滤器、拦截器、监听器、AOP(后续文章介绍)、全局异常处理器(后续文章介绍)是搭建系统框架时,经常用到的部分,全局异常处理器的作用很明显,就是处理接口执行过程中的异常,而过滤器、拦截器和AOP的作用就很丰富了,日志记录、性能监控、安全认证等等可以向上抽取的功能组件,均可以用他们来实现。

                传统基于Servlet容器的程序中,我们可以使用过滤器和监听器,在Java 框架中还可以使用拦截器,而面向切面编程AOP更是作为Spring框架中的核心思想被大家所关注

                Filter和Listener:依赖Servlet容器,基于函数回调实现。可以拦截所有请求,覆盖范围更广,但无法获取ioc容器中的bean。

                Interceptor和aop:依赖spring框架,基于java反射和动态代理实现。只能拦截controller的请求,可以获取ioc容器中的bean,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。。 

                从 Filter -> Interceptor -> aop ,拦截的功能越来越细致、强大,尤其是Interceptor和aop可以更好的结合spring框架的上下文进行开发。但是拦截顺序也是越来越靠后,请求是先进入Servlet容器的,越早的过滤和拦截对系统性能的消耗越少。具体选用哪种方法,就需要开发人员根据实际业务情况综合考虑了。

                二、过滤器

                1、过滤器作用

                Filter过滤器是Servlet容器层面的,在实现上基于函数回调,可以对几乎所有请求进行过滤

                Springboot中使用拦截器、过滤器、监听器的流程分析

                过滤器是对数据进行过滤,预处理过程,当我们访问网站时,有时候会发布一些敏感信息,发完以后有的会用*替代,还有就是登陆权限控制等,一个资源,没有经过授权,肯定是不能让用户随便访问的,这个时候,也可以用到过滤器,主要是对用户的一些请求进行一些预处理,并在服务器响应后再进行预处理,返回给用户。

                过滤器的功能还有很多,例如实现URL级别的权限控制、压缩响应信息、编码格式等等。对web服务器管理所有的web资源,例如JSP,静态图片 过滤敏感词汇,某些信息用*隐藏。

                使用方式有以下三种

                2、过滤器执行流程

                Springboot中使用拦截器、过滤器、监听器的流程分析

                3、过滤器拦截路径

                Springboot中使用拦截器、过滤器、监听器的流程分析

                4、过滤链

                一个javaweb系统中,可以配置多个过滤器、这多个过滤器就形成了一个过滤链,当然可以控制每个过滤器顺序,见过滤器三种实现方式

                Springboot中使用拦截器、过滤器、监听器的流程分析

                5、过滤器三种实现方式

                第一种方式:利用Servlet3.0的WebFilter注解配置

                @WebFilter是Servlet3.0新增加的注解,在servlet3.0之前,我们需要在web.XML文件中进行过滤器的配置,而现在可以通过此注解进行配置,当项目启动时,会自动扫描自动注册

                通过 @WebFilter 注解来标记一个过滤器,这种方式相信大家很容易想到。这是将 Servlet 中的那一套东西直接拿到 Spring Boot 上用。

                具体做法就是通过 @WebFilter 注解来标记一个 Filter,如下:

                //一个是filter的名字,一个是url为什么时用此过滤器,其他的看源码,Filter,必须要有名字,所有的过滤器执行顺序是根据Filter的名字字母顺序来执行的
                @WebFilter(filterName = "filter1",urlPatterns = {"/hello/*"})
                public class TimeFilter implements Filter {
                    @Override
                    public void init(FilterConfig filterConfig) throws ServletException {
                        System.out.println("=======初始化过滤器=========");
                    }
                    @Override
                    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
                            throws IOException, ServletException {
                        long start = System.currentTimeMillis();
                        filterChain.doFilter(request, response);
                        System.out.println("filter 耗时:" + (System.currentTimeMillis() - start));
                    }
                    @Override
                    public void destroy() {
                        System.out.println("=======销毁过滤器=========");
                    }
                }

                这个注解要生效,还需要我们在项目启动类上配置 @ServletComponentScan 注解,

                @ServletComponentScan 注解虽然名字带了 Servlet,但是实际上它不仅仅可以扫描项目中的 Servlet 容器,也可以扫描 Filter 和 Listener。

                这是我们在 Spring Boot 中使用过滤器的第一种方式,在实际项目中,这种方式使用较少,因为这种方式有一个很大的弊端就是无法指定 Filter 的优先级,如果存在多个 Filter 时,无法通过 @Order 指定优先级。

                @SpringBootApplication
                @ServletComponentScan
                public class SysoaApplication {
                    public static void main(String[] args) {
                        SpringApplication.run(SysoaApplication.class, args);
                    }
                }

                第二种方式:利用SpringBoot的配置类来添加过滤器

                这种方式最简单,直接实现Filter接口,并使用@Component注解标注为组件自动注入bean,

                Filter接口有 init、doFilter、destroy 三个方法,但 init、destroy 是有默认方法实现,可以不重写。

                @Component
                public class TimeFilter implements Filter {
                    @Override
                    public void init(FilterConfig filterConfig) throws ServletException {
                        System.out.println("=======初始化过滤器=========");
                    }
                    @Override
                    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
                            throws IOException, ServletException {
                        long start = System.currentTimeMillis();
                        filterChain.doFilter(request, response);
                        System.out.println("filter 耗时:" + (System.currentTimeMillis() - start));
                    }
                    @Override
                    public void destroy() {
                        System.out.println("=======销毁过滤器=========");
                    }
                }

                但是缺点是没办法设置过滤的路径,默认是 /* 过滤所有(当然代码中可以加入相关判断)。 

                public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                        HttpServletRequest request = (HttpServletRequest) servletRequest;
                        HttpServletResponse response = (HttpServletResponse) servletResponse;
                        String requestUri = request.getRequestURI().replaceFirst(contextPath, "");
                        String[] urlList = {"/login","/loginPage","logOut"};
                        for (String uriItem : urlList){
                            if(requestUri.contains(uriItem)){
                                filterChain.doFilter(request,response);
                                return;
                            }
                        }
                        //判断session中是否存在用户,这里还可以做其他业务逻辑 比如ip黑名单等 用户是否被禁,禁止多人在线等
                        if (request.getSession().getAttribute(SystemConstants.USER) == null) {
                            //没有登录跳转到登录页面
                            response.sendRedirect(contextPath+SystemConstants.LOGIN_PAGE_URL);
                            return;
                        }
                        filterChain.doFilter(request, response);
                    }

                这种方式看起来很方便,一个注解将 Filter 纳入到 Spring 容器中即可。而且这种方式还有一个优势,就是如果存在多个 Filter,可以通过 @Order 注解指定多个 Filter 的优先级,像下面这样:

                @Component
                @Order(-1)
                public class MyFilter implements Filter {
                    @Override
                    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
                        System.out.println("-----doFilter-----");
                        chain.doFilter(request, response);
                    }
                }

                第三种方式:Configuration+FilterRegistrationBean

                还是将 Filter 封装成一个 Bean,但这个 Bean 是 FilterRegistrationBean,通过 FilterRegistrationBean 我们既可以配置 Filter 的优先级,也可以配置 Filter 的拦截规则。

                public class AFilter implements Filter {
                    @Override
                    public void init(FilterConfig filterConfig) throws ServletException {
                        System.out.println("=======初始化过滤器A=========");
                    }
                    @Override
                    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
                            throws IOException, ServletException {
                        long start = System.currentTimeMillis();
                        filterChain.doFilter(request, response);
                        System.out.println("filter 耗时:" + (System.currentTimeMillis() - start));
                    }
                    @Override
                    public void destroy() {
                        System.out.println("=======销毁过滤器=========");
                    }
                }

                 然后使用SpringBoot提供的FilterRegistrationBean来对Filter进行配置

                @Configuration
                public class FilterConfig  {
                    /*过滤器注解bean:  FilterRegistrationBean,  注册过滤器, 添加过滤器*/
                    @Bean
                    public FilterRegistrationBean<AFilter> createFilterRegistrationBean() {
                        //1.创建FilterRegistrationBean这个对象, 一个过滤器注册器,注册一个过滤器
                        FilterRegistrationBean<AFilter> filterRegistrationBean  = new FilterRegistrationBean<>();
                        //注册一个过滤器
                        filterRegistrationBean.setFilter(new AFilter());
                        //过滤器的配置, 设置拦截的url
                        filterRegistrationBean.addUrlPatterns("/*");
                        //给过滤器起名字
                        filterRegistrationBean.setName("AFilter");
                        //设置过滤器的执行顺序
                        filterRegistrationBean.setOrder(1);
                        return filterRegistrationBean;
                    }

                当然也可以配置多个并且控制顺序

                @Configuration
                public class FilterConfig  {
                    @Bean
                    FilterRegistrationBean<AFilter> myFilterFilterRegistrationBean() {
                        FilterRegistrationBean<AFilter> bean = new FilterRegistrationBean<>();
                        bean.setFilter(new AFilter());
                        bean.setOrder(-1);
                        bean.setUrlPatterns(Arrays.asList("/*"));
                        return bean;
                    }
                    @Bean
                    FilterRegistrationBean<BFilter> myFilterFilterRegistrationBean2() {
                        FilterRegistrationBean<BFilter> bean = new FilterRegistrationBean<>();
                        bean.setFilter(new BF编程客栈ilter());
                        bean.setOrder(-2);
                        bean.setUrlPatterns(Arrays.asList("/hello"));
                        return bean;
                    }
                }

                FilterRegistrationBean 扩展

                Spring Boot 为了方便大家向 Servlet 容器中注册 Servlet、Filter 以及 Listener,提供了一个 Bean 注册的抽象类 RegistrationBean,如下:

                public abstract class RegistrationBean implements ServletContextInitializer, Ordered {
                	private int order = Ordered.LOWEST_PRECEDENCE;
                	private boolean enabled = true;
                	@Override
                	public final void onStartup(ServletContext servletContext) throws ServletException {
                		String description = getDescription();
                		if (!isEnabled()) {
                			logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
                			return;
                		}
                		register(description, servletContext);
                	}
                	protected abstract String getDescription();
                	protected abstract void register(String description, ServletContext servletContext);
                	public void setEnabled(boolean enabled) {
                		this.enabled = enabled;
                	}
                	public boolean isEnabled() {
                		return this.enabled;
                	}
                	public void setOrder(int order) {
                		this.order = order;
                	}
                	@Override
                	public int getOrder() {
                		return this.order;
                	}
                }

                RegistrationBean 实现了 ServletContextInitializer 接口,在 Servlet 启动时,RegistrationBean#onStartup 方法会被调用,进而完成 Filter、Servlet 以及 Listener 的注册。

                    enabled 属性可以理解为一个开关,设置为 false 相当于关闭组件注册。

                RegistrationBean 有众多的实现类,我们之前使用的 FilterRegistrationBean 只是其中之一:

                Springboot中使用拦截器、过滤器、监听器的流程分析

                实现类的作用一目了然:

                    ServletListenerRegistrationBean 用来注册监听器。

                    ServletRegistrationBean 用来注册 Servlet。

                    DispatcherServletRegistrationBean 用来注册 DispatcherServlet。

                    FilterRegistrationBean 用来注册过滤器。

                    DelegatingFilterProxyRegistrationBean 则用来注册 DelegatingFilterProxy,DelegatingFilterProxy 在 Spring Security、Spring Session、Shiro 等整合时非常有用。

                6、过滤器实际项目场景使用举例

                使用filter实现登录校验

                Springboot中使用拦截器、过滤器、监听器的流程分析

                三、监听器

                Listener监听器也是Servlet层面的,可以用于监听Web应用中某些对象、信息的创建、销毁和修改等动作发生,然后做出相应的响应处理。可以在这些事件发生前和发生后进行处理。

                用途:

                1、用于统计在线人数和在线用户,

                2、系统启动时加载初始化信息,

                3、统计网站访问量,

                4、记录用户访问路径

                根据监听对象,将监听器分为3类:

                第一类:ServletContext:对应application,实现接口ServletContextListener。在整个Web服务中只有一个,在Web服务关闭时销毁。可用于做数据缓存,例如结合Redis,在Web服务创建时从数据库拉取数据到缓存服务器。

                第二类:HttpSession:对应session会话,实现接口HttpSessionListener。在会话起始时创建,一端关闭会话后销毁。可用作获取在线用户数量。

                第三类:ServletRequest:对应request,实现接口ServletRequestListener。request对象是客户发送请求时创建的,用于封装请求数据,请求处理完毕后销毁。可用作封装用户信息。

                在写Listener的类时,有两种方式。

                第一种是只加@Component;

                第二种是 @WebListener 和 @ServletComponentScan 配合使用。

                不过实现接口则根据监听对象区分,如:ServletContextListener、HttpSessionListener和ServletRequestListener。

                import javax.servlet.annotation.WebListener;
                import javax.servlet.http.HttpSessionEvent;
                import javax.servlet.http.HttpSessionListener;
                @WebListener()
                public class MyListener implements HttpSessionListener{
                	 public static int online = 0;
                	    @Override
                	    public void sessionCreated(HttpSessionEvent se) {
                	        System.out.println("创建session,在线用户数:" + (++online));
                	    }
                	    @Override
                	    public void sessionDestroyed(HttpSessionEvent se) {
                	        System.out.println("销毁session,在线用户数:" + (--online));
                	        online--;
                	    }
                }

                启动类

                 
                @SpringBootApplication
                @ServletComponentScan("com.demo.listener") //需要扫描包
                public class SpringBoot1Application {
                	public static void main(String[] args) {
                		SpringApplication.run(SpringBoot1Application.class, args);
                	}
                }

                这里我们再补充一下常用的监听器接口:

                1.ServletContextListener -- 监听servletContext对象的创建以及销毁

                    1.1    contextInitialized(ServletContextEvent arg0)   -- 创建时执行

                    1.2    contextDestroyed(ServletContextEvent arg0)  -- 销毁时执行

                2.HttpSessionListener  -- 监听session对象的创建以及销毁

                    2.2   sessionCreated(HttpSessionEvent se)   -- 创建时执行

                    2.2   sessionDestroyed(HttpSessionEvent se) -- 销毁时执行

                3.ServletRequestListener -- 监听request对象的创建以及销毁

                    3.1    requestInitialized(ServletRequestEvent sre) -- 创建时执行

                    3.2    requestDestroyed(ServletRequestEvent sre) -- 销毁时执行

                4.ServletContextAttributeListener  -- 监听servletContext对象中属性的改变

                    4.1    attributeAdded(ServletContextAttributeEvent event) -- 添加属性时执行

                    4.2    attributeReplaced(ServletContextAttributeEvent event) -- 修改属性时执行

                    4.3    attributeRemoved(ServletContextAttributeEvent event) -- 删除属性时执行

                5.HttpSessionAttributeListener  --监听session对象中属性的改变

                    5.1    attributeAdded(HttpSessionBindingEvent event) -- 添加属性时执行

                    5.2    attributeReplaced(HttpSessionBindingEvent event) -- 修改属性时执行

                    5.3    attributeRemoved(HttpSessionBindingEvent event) -- 删除属性时执行

                6.ServletRequestAttributeListener  --监听request对象中属性的改变

                    6.1    attributeAdded(ServletRequestAttributeEvent srae) -- 添加属性时执行

                    6.2    attributeReplacedixURLvnjK(ServletRequestAttributeEvent srae) -- 修改属性时执行

                    6.3    attributeRemoved(ServletRequestAttributeEvent srae) -- 删除属性时执行

                四、拦截器

                1、拦截器作用

                过滤器是拦截所有请求,而拦截器是拦截在进入到前端控制器之后的请求,

                Interceptor和Filter、Listener有本质上的不同,Filter、Listener都是依赖于Servlet容器,而Interceptor则是依赖于Spring框架,是aop的一种表现,当某个方法或字段被访问时进行拦截,在之前 和 在之后 加入某些操作

                用途:权限验证,判断用户是否登录,或者再做某一操作时要确定满足某一定的条件,基于Java的动态代理实现的。

                2、拦截器执行流程

                Springboot中使用拦截器、过滤器、监听器的流程分析

                3、拦截器拦截路径

                 拦截器可以根据需求配置不同的拦截路径

                Springboot中使用拦截器、过滤器、监听器的流程分析

                Springboot中使用拦截器、过滤器、监听器的流程分析

                4、拦截器的使用

                首先通过实现 HandlerInterceptor接口声明拦截器的类,实现preHandle、postHandle和afterCompletion方法。然后通过配置类WebMvcConfigurer接口,实现addInterceptors方法配置拦截器

                 
                import org.springframework.stereotype.Component;
                import org.springframework.web.servlet.HandlerInterceptor;
                import org.springframework.web.servlet.ModelAndView;
                import javax.servlet.http.HttpServletRequest;
                import javax.servlet.http.HttpServletResponse;
                @Component
                public class InterceptorTest  implements HandlerInterceptor {
                    /**
                     * 在请求处理之前进行调用(Controller方法调用之前)
                     * 预处理回调方法,实现处理器的预处理
                     * 返回值:true表示继续流程;false表示流程中断,不会继续调用其他的拦截器或处理器
                     */
                    @Override
                    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                            throws Exception {
                        System.out.println("开始拦截.........");
                        String name = MyCookie.getCookieByKey(request,response,"name");
                        String password = MyCookie.getCookieByKey(request,response,"password");
                        //如果session中没有user,表示没登陆
                        if (password == null|| name == null){
                            //这个方法返回false表示忽略当前请求,如果一个用户调用了需要登陆才能使用的接口,如果他没有登陆这里会直接忽略掉
                            //当然你可以利用response给用户返回一些提示信息,告诉他没登陆
                            request.getRequestDispatcher("/interceptor/to_login").forward(request, response);
                            return false;
                        }else {
                            return true;//放行
                        }
                    }
                    /**
                     * 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
                     * 后处理回调方法,实现处理器(controller)的后处理,但在渲染视图之前
                     * 此时我们可以通过modelAndView对模型数据进行处理或对视图进行处理
                     */
                    @Override
                    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                                           ModelAndView modelAndView) throws Exception {
                        // TODO Auto-generated method stub
                        System.out.println("return前");
                    }
                    /**
                     * 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
                     * 整个请求处理完毕回调方法,即在视图渲染完毕时编程回调,
                     * 如性能监控中我们可以在此记录结束时间并输出消耗时间,
                     * 还可以进行一些资源清理,类似于try-catch-finally中的finally,
                     * 但仅调用处理器执行链中
                     */
                    @Override
                    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
                            throws Exception {
                        // TODO Auto-generated method stub
                        System.out.println("操作完之后,可以用于资源清理");
                    }
                }

                然后将这个组件加入到boot中,在boot1版本中 通过继承WebmvcConfigureAdapter实现一个web配置,例如我们配置上面的拦截器

                @Configuration //声明这是一个配置
                public class LoginInterceptorConfig extends WebMvcConfigurerAdapter {
                @Resource
                private LoginInterceptor loginInterceptor;
                @Override
                public void addInterceptors(InterceptorRegistry registry) {
                    registry.addInterceptor(loginInt编程客栈erceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin").excludePathPatterns("/admin/login");
                }
                    }

                 或者直接使用匿名类的方式

                import org.springframework.context.annotation.Configuration;
                import org.springframework.web.servlet.HandlerInterceptor;
                import org.springframework.web.servlet.ModelAndView;
                import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
                import org.springframework.web.servlet编程.config.annotation.WebMvcConfigurerAdapter;
                import javax.servlet.http.HttpServletRequest;
                import javax.servlet.http.HttpServletResponse;
                /**
                 * 自定义一个登陆拦截器
                 */
                @Configuration //声明这是一个配置
                public class LoginInterceptor extends WebMvcConfigurerAdapter {
                    /*
                    用来添加拦截器的方法
                    InterceptorRegistry registry拦截器注册
                     */
                    @Override
                    public void addInterceptors(InterceptorRegistry registry) {
                        //使用匿名内部类创建要给拦截器
                        HandlerInterceptor loginInterceptor = new HandlerInterceptor() {
                            @Override
                            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
                                //判断session中是否存在用户
                                if (request.getSession().getAttribute("user") == null) {
                                    response.sendRedirect("/admin");
                                    return false;
                                }
                                return true;
                            }
                            @Override
                            public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
                            }
                            @Override
                            public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
                            }
                        };
                        registry.addInterceptor(loginInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin").excludePathPatterns("/admin/login");
                    }
                }

                 对于Sprinboot2版本,第一步还是定义一个拦截器组件。

                第二不再是通过继承WebmvcConfigureAdapter实现一个web配置,而是实现接口WebMvcConfigurer增加一个配置

                @Configuration
                public class WebConfig implements WebMvcConfigurer {
                //引入我们的拦截器组件
                    @Resource
                    private LoginInterceptor loginInterceptor;
                //实现拦截器配置方法
                    @Override
                    public void addInterceptors(InterceptorRegistry registry) {
                        registry.addInterceptor(loginInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin").excludePathPatterns("/admin/login");
                    }
                }

                踩坑5--springboot2.xl登陆拦截器中排除静态资源的几种情况整理 - 百度文库 (baidu.com) 

                5、拦截器使用场景举例

                Springboot中使用拦截器、过滤器、监听器的流程分析

                Filter与Interceptor区别

                1.接口范围不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。

                2.拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源

                到此这篇关于Springboot中使用拦截器、过滤器、监听器的文章就介绍到这了,更多相关Springboot使用拦截器、过滤器、监听器内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

                0

                精彩评论

                暂无评论...
                验证码 换一张
                取 消

                关注公众号