【问题标题】:How to create my own filter with Spring MVC?如何使用 Spring MVC 创建自己的过滤器?
【发布时间】:2014-10-11 22:36:18
【问题描述】:

我使用 Spring MVC (4.0.1) 作为后端服务和 angularjs 作为前端。

对我的服务器后端的每个请求都有一个带有会话 ID 的 http-header

我可以使用以下代码在我的服务器后端读取此标头:

@Autowired
protected HttpServletRequest request;
String xHeader=request.getHeader("X-Auth-Token"); //returns the sessionID from the header

现在我调用这个方法getPermission(xHeader) 它只返回真或假。如果用户存在于我的数据库中,则返回 true,否则返回 false!

我现在想创建一个具有这种行为的过滤器,如果用户有权访问我的控制器,它会检查每个请求!但是,如果该方法返回 false,它应该发回 401 错误并且不会到达我的控制器!

我怎样才能做到这一点并创建自己的过滤器?我只使用 Java Config,不使用 XML。

我想我必须在这里添加过滤器:

public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Filter[] getServletFilters() {
        MyOwnFilter=new MyOwnFilter();
        return new Filter[] {MyOwnFilter};
    }
}

【问题讨论】:

  • 为什么不改用 Spring Security?
  • 我没有使用 spring security 的经验,使用 java config 也不是那么容易。或者你能给我一个简单的例子,我如何使用 Spring Security 来实现我第一篇文章中的行为?
  • @chrylis 因为整个OAuth 方法对于简单的会话过滤器来说是一个巨大的过度杀伤力。此外,Spring 在这方面缺乏一个中肯的例子。
  • Spring security 不一定就是OAuth,例如可以做Basic Auth。
  • 我知道Spring security有一点学习曲线,但是如果你是一个认真的java后端开发者,这是一个非常好的技术要知道,主要是因为安全并不容易,我知道一个很多关于它,宁愿把它留给别人。如果您没有时间学习 Spring 安全性,我会使用下面 Avinash 的过滤器方法

标签: java spring spring-mvc


【解决方案1】:

过滤器的替代品,您可以使用HandlerInterceptor

public class SessionManager implements HandlerInterceptor{

    // This method is called before the controller
    @Override
    public boolean preHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler) throws Exception {

        String xHeader = request.getHeader("X-Auth-Token");
        boolean permission = getPermission(xHeader);
        if(permission) {
            return true;
        }
        else {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
            // Above code will send a 401 with no response body.
            // If you need a 401 view, do a redirect instead of
            // returning false.
            // response.sendRedirect("/401"); // assuming you have a handler mapping for 401

        }
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request,
            HttpServletResponse response, Object handler, Exception ex)
            throws Exception {

    }
}

然后将此拦截器添加到您的 webmvc 配置中。

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Bean
    SessionManager getSessionManager() {
         return new SessionManager();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(getSessionManager())
        .addPathPatterns("/**")
        .excludePathPatterns("/resources/**", "/login");
     // assuming you put your serve your static files with /resources/ mapping
     // and the pre login page is served with /login mapping
    }

}

【讨论】:

  • 就像在另一个答案Is it possible to create my own Annotation for this Interceptor ? 中询问的操作?
  • 该注释将应用在哪里?在Controllers?
  • 是的,或者最好是在 RequestMapping 方法上。我只是自己做了一些研究,看起来没有本地方法可以做到这一点
  • @ares,你为什么把 SessionManager 做成一个 bean?有必要吗?谢谢你。 +1
  • @AndrewTobilko:不,在这种情况下没有必要。
【解决方案2】:

下面是执行您提到的逻辑的过滤器

@WebFilter("/*")
public class AuthTokenFilter implements Filter {

    @Override
    public void destroy() {
        // ...
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        //
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String xHeader = ((HttpServletRequest)request).getHeader("X-Auth-Token");
        if(getPermission(xHeader)) {
            chain.doFilter(request, response);
        } else {
            request.getRequestDispatcher("401.html").forward(request, response);
        }
    }
}

你没看错,spring 配置应该在后面。

public class MyWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Filter[] getServletFilters() {
        return new Filter[]{new AuthTokenFilter()};
    }
}

【讨论】:

    【解决方案3】:

    Spring可以使用过滤器,但他们建议您使用他们的过滤器版本,称为拦截器

    http://viralpatel.net/blogs/spring-mvc-interceptor-example/

    快速浏览它们的工作原理。它们几乎与过滤器相同,但设计用于在 Spring MVC 生命周期内工作。

    【讨论】:

    • 嗨,好的,我会试试这个,但是如果我的检查返回 false,我如何防止请求到达我的控制器?如果我的检查返回 false,它应该将 401 错误发送回我的前端(angularjs)并且它不应该调用控制器!
    • 我发现了第二个问题,来自我的登录或注册页面的请求没有会话 ID,我怎么能告诉我的拦截器不应该检查这两个请求?
    • 好的,我想我现在已经解决了我的问题。我已经编写了自己的 preHandle 拦截器。并使用 registry.addInterceptor(myInterceptor()).addPathPatterns("/**").excludePathPatterns("/**/login");我可以排除我的登录页面。是否可以为此拦截器创建自己的注释?
    【解决方案4】:

    我假设您正在尝试实现某种基于 jwt 令牌的 OAuth 安全性。

    现在有几种方法可以做到这一点,但这是我最喜欢的一种:

    这是过滤器的样子:

    import java.io.IOException;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    
    import org.springframework.web.filter.GenericFilterBean;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureException;
    
    public class JwtFilter extends GenericFilterBean {
    
        @Override
        public void doFilter(final ServletRequest req,
                             final ServletResponse res,
                             final FilterChain chain) throws IOException, ServletException {
            final HttpServletRequest request = (HttpServletRequest) req;
    
            final String authHeader = request.getHeader("Authorization");
            if (authHeader == null || !authHeader.startsWith("Bearer ")) {
                throw new ServletException("Missing or invalid Authorization header.");
            }
    
            final String token = authHeader.substring(7); // The part after "Bearer "
    
            try {
                final Claims claims = Jwts.parser().setSigningKey("secretkey")
                    .parseClaimsJws(token).getBody();
                request.setAttribute("claims", claims);
            }
            catch (final SignatureException e) {
                throw new ServletException("Invalid token.");
            }
    
            chain.doFilter(req, res);
        }
    
    }
    

    很简单,用户控制器也可以在其中找到登录方法:

    import java.util.Arrays;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import javax.servlet.ServletException;
    
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    
    @RestController
    @RequestMapping("/user")
    public class UserController {
    
        private final Map<String, List<String>> userDb = new HashMap<>();
    
        public UserController() {
            userDb.put("tom", Arrays.asList("user"));
            userDb.put("sally", Arrays.asList("user", "admin"));
        }
    
        @RequestMapping(value = "login", method = RequestMethod.POST)
        public LoginResponse login(@RequestBody final UserLogin login)
            throws ServletException {
            if (login.name == null || !userDb.containsKey(login.name)) {
                throw new ServletException("Invalid login");
            }
            return new LoginResponse(Jwts.builder().setSubject(login.name)
                .claim("roles", userDb.get(login.name)).setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256, "secretkey").compact());
        }
    
        @SuppressWarnings("unused")
        private static class UserLogin {
            public String name;
            public String password;
        }
    
        @SuppressWarnings("unused")
        private static class LoginResponse {
            public String token;
    
            public LoginResponse(final String token) {
                this.token = token;
            }
        }
    }
    

    当然,我们有 Main,您可以在其中看到过滤器 bean:

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
    import org.springframework.boot.context.embedded.FilterRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    @EnableAutoConfiguration
    @ComponentScan
    @Configuration
    public class WebApplication {
        @Bean
        public FilterRegistrationBean jwtFilter() {
            final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
            registrationBean.setFilter(new JwtFilter());
            registrationBean.addUrlPatterns("/api/*");
    
            return registrationBean;
        }
    
        public static void main(final String[] args) throws Exception {
            SpringApplication.run(WebApplication.class, args);
        }
    
    }
    

    最后但并非最不重要的还有一个示例控制器:

    import io.jsonwebtoken.Claims;
    
    import java.util.List;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/api")
    public class ApiController {
        @SuppressWarnings("unchecked")
        @RequestMapping(value = "role/{role}", method = RequestMethod.GET)
        public Boolean login(@PathVariable final String role,
                final HttpServletRequest request) throws ServletException {
            final Claims claims = (Claims) request.getAttribute("claims");
    
            return ((List<String>) claims.get("roles")).contains(role);
        }
    }
    

    Here 是一个指向 GitHub 的链接,感谢 nielsutrecht 所做的出色工作,我以这个项目为基础,它运行良好。

    【讨论】:

      【解决方案5】:

      您还可以使用带有切入点的切面来实现它,该切入点以特定注释为目标。我编写了一个库,使您能够使用基于 JWT 令牌执行授权检查的注释。

      您可以在以下位置找到包含所有文档的项目:https://github.com/nille85/jwt-aspect。我已经多次使用这种方法来保护由单页应用程序使用的 REST 后端。

      我还在我的博客上记录了如何在 Spring MVC 应用程序中使用它:http://www.nille.be/security/creating-authorization-server-using-jwts/

      以下是https://github.com/nille85/auth-server上示例项目的摘录

      下面的示例包含一个受保护的方法 getClient。方面使用的注释 @Authorize 检查来自 "aud jwt claim" 的值是否与使用 @ClaimValue 注释的 clientId 参数匹配.如果匹配,则可以输入该方法。否则抛出异常。

      @RestController
      @RequestMapping(path = "/clients")
      public class ClientController {
      
          private final ClientService clientService;
      
          @Autowired
          public ClientController(final ClientService clientService) {
              this.clientService = clientService;
          }
      
          @Authorize("hasClaim('aud','#clientid')")
          @RequestMapping(value = "/{clientid}", method = RequestMethod.GET, produces = "application/json")
          @ResponseStatus(value = HttpStatus.OK)
          public @ResponseBody Client getClient(@PathVariable(value = "clientid") @ClaimValue(value = "clientid") final String clientId) {
              return clientService.getClient(clientId);
          }
      
          @RequestMapping(value = "", method = RequestMethod.GET, produces = "application/json")
          @ResponseStatus(value = HttpStatus.OK)
          public @ResponseBody List<Client> getClients() {
              return clientService.getClients();
          }
      
      
          @RequestMapping(path = "", method = RequestMethod.POST, produces = "application/json")
          @ResponseStatus(value = HttpStatus.OK)
          public @ResponseBody Client registerClient(@RequestBody RegisterClientCommand command) {
              return clientService.register(command);
      
      
          }
      
      }
      

      Aspect 本身可以这样配置:

      @Bean
      public JWTAspect jwtAspect() {
          JWTAspect aspect = new JWTAspect(payloadService());
          return aspect;
      }
      

      所需的 PayloadService 例如可以这样实现:

      public class PayloadRequestService implements PayloadService {
      
          private final JWTVerifier verifier;
      
          public PayloadRequestService(final JWTVerifier verifier){
              this.verifier = verifier;
          }
      
          @Override
          public Payload verify() {
              ServletRequestAttributes t = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
              HttpServletRequest request = t.getRequest();
      
              final String jwtValue = request.getHeader("X-AUTH");
              JWT jwt = new JWT(jwtValue);
              Payload payload =verifier.verify(jwt);
      
              return payload;
          }
      
      }
      

      【讨论】:

        【解决方案6】:

        您可以按照以下步骤创建和配置自己的过滤器。

        1) 通过实现过滤器接口并覆盖其方法来创建您的类。

        public class MyFilter implements javax.servlet.Filter{
        
        
        public void destroy(){}
        public void doFilter(Request, Response, FilterChain){//do what you want to filter
        }
        ........
        }
        

        2) 现在在 web.xml 中配置您的过滤器

        <filter>
          <filter-name>myFilter</filter-name>
          <filter-class>MyFilter</filter-class>
        </filter>
        

        3) 现在提供过滤器的 url 映射。

        <filter-mapping>
           <filter-name>myFilter</filter-name>
           <url-pattern>*</url-pattern>
        </filter-mapping>
        

        4) 现在重新启动您的服务器并检查所有 Web 请求将首先到达 MyFilter,然后继续到相应的控制器。

        希望这将是必需的答案。

        【讨论】:

          【解决方案7】:

          你的方法看起来是正确的。

          一旦我使用了类似于以下内容的内容(删除了大部分行并保持简单)。

          public class MvcDispatcherServletInitializer  extends AbstractAnnotationConfigDispatcherServletInitializer {
          
              @Override
              public void onStartup(ServletContext servletContext) throws ServletException {
                  super.onStartup(servletContext);
          
                  EnumSet<DispatcherType> dispatcherTypes = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ERROR);
          
                  FilterRegistration.Dynamic monitoringFilter = servletContext.addFilter("monitoringFilter", MonitoringFilter.class);
                  monitoringFilter.addMappingForUrlPatterns(dispatcherTypes, false, "/api/admin/*");
              }
          
              @Override
              protected Class<?>[] getRootConfigClasses() {
                  return new Class[] { WebMvcConfig.class };
              }
          
              @Override
              protected Class<?>[] getServletConfigClasses() {
                  return null;
              }
          
              @Override
              protected String[] getServletMappings() {
                  return new String[] { "/" };
              }
          
          }
          

          您还需要一个自定义过滤器,如下所示。

          public class CustomXHeaderFilter implements Filter {
          
                  @Override
                  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
                      HttpServletRequest request = (HttpServletRequest) req;
                      HttpServletResponse response = (HttpServletResponse) res;
          
                      String xHeader = request.getHeader("X-Auth-Token");
                      if(YOUR xHeader validation fails){
                          //Redirect to a view
                          //OR something similar
                          return;
                      }else{
                          //If the xHeader is OK, go through the chain as a proper request
                          chain.doFilter(request, response);
                      }
          
                  }
          
                  @Override
                  public void destroy() {
                  }
          
                  @Override
                  public void init(FilterConfig arg0) throws ServletException {
                  }
          
              }
          

          希望这会有所帮助。

          此外,如果您使用 Spring Boot,您还可以使用 FilterRegistrationBean。它和FilterRegistration.Dynamic 做的事情一样(我想是的)。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-07-09
            • 2020-09-20
            • 2016-01-28
            • 2015-04-12
            • 1970-01-01
            • 2017-05-19
            相关资源
            最近更新 更多