草庐IT

java - 如何避免过滤器后在 dispatcherServlet 中关闭输入流

coder 2024-03-14 原文

我需要保留有关为应用程序发送的请求/响应的所有信息,如 http 状态、当前时间、 token 、请求 URI 等。它是一个 API,资源是:

  • POST localhost:8080/v1/auth/login 使用电子邮件和密码请求身份验证。响应是一个 JWT token 。

  • GET localhost:8080/v1/auth/rules 在请求 header 中带有 token 。响应是包含有关 token 所有者的信息的正文,例如电子邮件和姓名。

为了实现这一点,我的方法覆盖了 doDispatch 方法:

LogDispatcherServlet

@Component
public class LogDispatcherServlet extends DispatcherServlet {

    @Autowired
    private LogRepository logRepository;

    private static final Logger logger = LoggerFactory.getLogger(LogDispatcherServlet.class);

    @Override
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (!(request instanceof ContentCachingRequestWrapper)) {
            request = new ContentCachingRequestWrapper(request);
        }
        if (!(response instanceof ContentCachingResponseWrapper)) {
            response = new ContentCachingResponseWrapper(response);
        }
        HandlerExecutionChain handler = getHandler(request);

        try {
            super.doDispatch(request, response);
        } finally {
            try {
                ApiLog log = ApiLog.build(request, response, handler, null);
                logRepository.save(log);
                updateResponse(response);
            } catch (UncheckedIOException e) {
                logger.error("UncheckedIOException", e);
            } catch (Exception e) {
                logger.error("an error in auth", e);
            }
        }
    }

    private void updateResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        responseWrapper.copyBodyToResponse();
    }

}

ApiLog.build 负责获取有关请求的样本信息,LogDispatcherServletlocalhost:8080/v1/auth/rules.

API日志

public static ApiLog build(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain handler, Authentication auth) {
        ApiLog log = new ApiLog();
        log.setHttpStatus(response.getStatus());
        log.setHttpMethod(request.getMethod());
        log.setPath(request.getRequestURI());
        log.setClientIp(request.getRemoteAddr());
        try {
            if (request.getReader() != null) {
                log.setBodyRequest(getRequestPayload(request));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (handler != null) {
            log.setJavaMethod(handler.toString());
        }
        if (request.getHeader("Authorization") != null) {
            log.setToken(request.getHeader("Authorization"));
        } else if (response.getHeader("Authorization") != null) {
            log.setToken(response.getHeader("Authorization"));
        }
        log.setResponse(getResponsePayload(response));
        log.setCreated(Instant.now());
        logger.debug(log.toString());
        return log;
    }

    @NotNull
    private static String getRequestPayload(HttpServletRequest request) {
        ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        try {
            return wrapper
                    .getReader()
                    .lines()
                    .collect(Collectors.joining(System.lineSeparator()));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "{}";
    }

    @NotNull
    private static String getResponsePayload(HttpServletResponse responseToCache) {
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(responseToCache, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                int length = Math.min(buf.length, 5120);
                try {
                    return new String(buf, 0, length, wrapper.getCharacterEncoding());
                } catch (UnsupportedEncodingException ex) {
                    logger.error("An error occurred when tried to logging request/response");
                }
            }
        }
        return "{}";
    }

我最大的问题是:我正在使用 Spring Security 生成 JWT token ,因此所有发送到 /v1/auth/login 的请求都会重定向到过滤器。

应用安全

@Configuration
@EnableWebSecurity
public class AppSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable().authorizeRequests()
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                .and()
                .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
                        UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsService);
    }

}

认证成功后,filter 必须调用 LogDispatcherServlet 来保存请求内容和响应内容。 /login 没有Controller,只有 JWTLoginFilter。

JWTLoginFilter

public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {

    @Autowired
    JWTLoginFilter(String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {

        AccountCredentials credentials = new ObjectMapper()
                .readValue(request.getInputStream(), AccountCredentials.class);

        return getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(
                        credentials.getUsername(),
                        Md5.getHash(credentials.getPassword()),
                        Collections.emptyList()
                )
        );
    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain,
            Authentication auth) throws IOException, ServletException {

        TokenAuthenticationService.addAuthentication(response, auth.getName());
        //Must call LogDispatcherServlet
        filterChain.doFilter(request, response);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        super.unsuccessfulAuthentication(request, response, failed);

        //Must call LogDispatcherServlet
    }

}

但它不适用于/login。当 ApiLog 尝试在 getRequestPayload 中获取请求正文时,我得到一个 java.io.IOException: Stream closed

我该怎么做才能避免这种情况? JWTLoginFilter 需要知道用于身份验证的请求正文和 LogDispatcherServlet,但 request.getInputStream()attemptAuthentication 中调用。还有其他更简单的解决方案吗?

最佳答案

我认为您不需要更新 Spring 的 DispatcherServlet。我只想创建一个过滤器(在链中的第一个位置),它将原始请求/响应包装在一个允许缓存的对象中(例如 ContentCachingRequestWrapper/ContentCachingResponseWrapper )。

在您的过滤器中,您只需要执行以下操作:

doFilter(chain, req, res) {

   ServletRequest wrappedRequest = ...
   ServletResponse wrappedResponse = ...   

   chain.doFilter(wrappedRequest, wrappedResponse);

}

然后你可以注册一个HandlerInterceptor

public class YourHandlerIntercepter extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) 

            ApiLog log = ApiLog.build(request, response, handler, null);
            logRepository.save(log);
        }
        return true;
    }
}

您最终需要更改 ApiLog 方法,使其使用 MethodHandler 而不是 HandlerExecutionChain

关于java - 如何避免过滤器后在 dispatcherServlet 中关闭输入流,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50931876/

有关java - 如何避免过滤器后在 dispatcherServlet 中关闭输入流的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  8. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  9. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

  10. ruby - 如何使用文字标量样式在 YAML 中转储字符串? - 2

    我有一大串格式化数据(例如JSON),我想使用Psychinruby​​同时保留格式转储到YAML。基本上,我希望JSON使用literalstyle出现在YAML中:---json:|{"page":1,"results":["item","another"],"total_pages":0}但是,当我使用YAML.dump时,它不使用文字样式。我得到这样的东西:---json:!"{\n\"page\":1,\n\"results\":[\n\"item\",\"another\"\n],\n\"total_pages\":0\n}\n"我如何告诉Psych以想要的样式转储标量?解

随机推荐