【问题标题】:HttpSession null after replacing AuthorizationRequest替换 AuthorizationRequest 后的 HttpSession null
【发布时间】:2016-09-28 19:51:36
【问题描述】:

Complete code 和快速重现问题的说明如下。


问题:
DefaultOAuth2RequestFactory 的自定义实现将当前AuthorizationRequest 替换为保存的AuthorizationRequest 后,HttpSession 变为null这会导致后续请求失败/oauth/token 因为 /oauth/token 端点之前的 Spring Security 过滤器链中的 CsrfFilter 无法在 null 中找到 session Csrf token sessionrequest 的比较Csrf token


错误期间的控制流程:

以下流程图说明了 Step 14Step 15 以某种方式 null-ify HttpSession。 (或者可能与JSESSIONID 不匹配。)在第14 步CustomOAuth2RequestFactory.java 开头的SYSO 表明确实有一个HttpSession 实际上包含正确的CsrfToken .然而,不知何故,当 Step 15 触发来自客户端的调用时,HttpSession 已变为 nulllocalhost:8080/login url 返回localhost:9999/oauth/token 端点。

在下面的调试日志中提到的HttpSessionSecurityContextRepository 的每一行都添加了断点。 (它位于authserver eclipse 项目的Maven Dependencies 文件夹中。)当在下面的流程图中发出对/oauth/token 的最终请求时,这些断点确认HttpSessionnull。 (流程图的左下角。) null HttpSession 可能是由于自定义 DefaultOAuth2RequestFactory 代码运行后保留在浏览器中的 JSESSIONID 已过期。

如何解决此问题,以便在流程图中第 15 步结束后,在对/oauth/token 端点的最终调用期间保持相同的HttpSession


相关代码和日志:

CustomOAuth2RequestFactory.javacan be viewed at a file sharing site by clicking on this link.的完整代码我们可以猜测nullsession是由于1.)JSESSIONID没有在浏览器中被@中的代码更新987654370@,或 2.) HttpSession 实际上是 null-ified。

Step 15之后调用/oauth/token的Spring Boot调试日志清楚地表明此时没有HttpSession,可以这样解读:

2016-05-30 15:33:42.630 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy        : /oauth/token at position 1 of 12 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy        : /oauth/token at position 2 of 12 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] w.c.HttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: null. A new one will be created.
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy        : /oauth/token at position 3 of 12 in additional filter chain; firing Filter: 'HeaderWriterFilter'
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@2fe29f4b
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy        : /oauth/token at position 4 of 12 in additional filter chain; firing Filter: 'CsrfFilter'
2016-05-30 15:33:42.644 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:9999/uaa/oauth/token
2016-05-30 15:33:42.644 DEBUG 13897 --- [io-9999-exec-10] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2016-05-30 15:33:42.644 DEBUG 13897 --- [io-9999-exec-10] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed


在您的计算机上重现问题:

按照这些简单的步骤,您只需几分钟即可在任何计算机上重现问题:

1.) 下载zipped version of the app from a file sharing site by clicking on this link

2.) 输入解压应用程序:tar -zxvf oauth2.tar(4).gz

3.) 导航至oauth2/authserver,然后输入mvn spring-boot:run,启动authserver 应用程序。

4.) 导航至oauth2/resource,然后输入mvn spring-boot:run,启动resource 应用程序

5.) 导航至oauth2/ui,然后输入mvn spring-boot:run 启动ui 应用程序

6.) 打开网络浏览器并导航到http : // localhost : 8080

7.) 点击Login,然后输入Frodo作为用户,MyRing作为密码,然后点击提交。

8.) 输入5309 作为Pin Code,然后单击提交。 这将触发上面显示的错误。

Spring Boot 调试日志将显示 A LOT of SYSO,其中在流程图所示的每个步骤中给出了变量的值,例如 XSRF-TOKENHttpSessionSYSO 有助于对调试日志进行分段,以便更容易解释。并且所有SYSO 都是由其他类调用的一个类完成的,因此您可以操纵SYSO-generating 类来更改控制流中各处的报告。 SYSO-generating 类的名称是TestHTTP,它的源代码可以在同一个demo 包中找到。


使用调试器:

1.) 选择正在运行authserver 应用程序的终端窗口并键入Ctrl-C 以停止authserver 应用程序。

2.) 将三个应用程序(authserverresourceui)作为现有的 maven 项目导入 eclipse。

3.) 在 authserver 应用程序的 eclipse 项目资源管理器中,单击展开 Maven Dependencies 文件夹,然后在其中向下滚动以单击展开 Spring-Security-web... jar 如下图橙色圆圈所示。然后滚动查找并展开org.springframework.security.web.context 包。然后双击打开下面屏幕截图中以蓝色突出显示的HttpSessionSecurityContextRepository 类。向此类中的每一行添加断点。您可能希望对同一包中的 SecurityContextPersistenceFilter 类执行相同的操作。 这些断点可以让你看到HttpSession的值,目前在控制流结束前变成null,但需要有一个可以映射到@的有效值987654415@ 以解决此 OP。

4.) 在应用程序的 demo 包中,在 CustomOAuth2RequestFactory.java 内添加断点。然后Debug As... Spring Boot App 启动调试器。

5.) 然后重复上面的步骤 6 到 8。您可能希望在每次新尝试之前清除浏览器的缓存。您可能希望打开浏览器开发工具的“网络”选项卡。

【问题讨论】:

    标签: spring spring-mvc spring-security spring-boot spring-oauth2


    【解决方案1】:

    会话不为空在您的 authserver 应用程序中,在最后一次调用 localhost :9999/uaa/oauth/token 时。 不仅存在会话,而且有效会话的 JSESSIONIDcsrf 令牌与控制流中存在的值相匹配/oauth/token 的请求失败。

    问题是JSESSIONID的值有两个,选错了两个值进入/oauth/token的调用。因此,解决方案应该来自修改过滤器以删除错误的JSESSIONID,以便可以发送正确的值。

    以下将总结:


    HttpSessionListener 识别出有效的JSESSIONID

    为了隔离问题,我创建了HttpSessionListener 的实现,然后从HttpLListener 的自定义实现中调用它,如下所示:

    public class HttpSessionCollector implements HttpSessionListener, ServletContextListener {
    
        private static final Set<HttpSession> sessions = ConcurrentHashMap.newKeySet();
    
        public void sessionCreated(HttpSessionEvent event) {
            sessions.add(event.getSession());
        }
    
        public void sessionDestroyed(HttpSessionEvent event) {
            sessions.remove(event.getSession());
        }
    
        public static Set<HttpSession> getSessions() {
            return sessions;
        }
    
        public void contextCreated(ServletContextEvent event) {
            event.getServletContext().setAttribute("HttpSessionCollector.instance", this);
        }
    
        public static HttpSessionCollector getCurrentInstance(ServletContext context) {
            return (HttpSessionCollector) context.getAttribute("HttpSessionCollector.instance");
        }
    
        @Override
        public void contextDestroyed(ServletContextEvent arg0) {
        }
    
        @Override
        public void contextInitialized(ServletContextEvent arg0) {
        }
    
    }
    

    然后我在OncePerRequestFilter 的自定义实现中调用了上面的HttpSessionListener,我将其插入到您的authserver 应用程序的Spring 安全过滤器链中以提供诊断信息,如下所示:

    @Component
    public class DiagnoseSessionFilter extends OncePerRequestFilter implements ServletContextAware {
    
        @Override
        protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain fc) throws ServletException, IOException {
    
        System.out.println("...........///////////// START OF DiagnoseSessionFilter.doFilterInternal() ///////////...........");
        //start of request stuff
        System.out.println("\\\\\\\\\\ REQUEST ATTRIBUTES ARE: ");
        if(req.getAttribute("_csrf")!=null){
            System.out.println("_csrf is: " + req.getAttribute("_csrf").toString());
        }
        if(req.getAttribute("org.springframework.security.web.csrf.CsrfToken")!=null){
            CsrfToken ucsrf = (CsrfToken) req.getAttribute("org.springframework.security.web.csrf.CsrfToken");
            System.out.println("ucsrf.getToken() is: " + ucsrf.getToken());
        }
        String reqXSRF = req.getHeader("XSRF-TOKEN");
        System.out.println("request XSRF-TOKEN header is: " + reqXSRF);
        String reqCookie = req.getHeader("Cookie");
        System.out.println("request Cookie header is: " + reqCookie);
        String reqSetCookie = req.getHeader("Set-Cookie");
        System.out.println("request Set-Cookie header is: " + reqSetCookie);
        String reqReferrer = req.getHeader("referrer");
        System.out.println("request referrer header is: " + reqReferrer);
        HttpSession rsess = req.getSession(false);
        System.out.println("request.getSession(false) is: " + rsess);
        if(rsess!=null){
            String sessid = rsess.getId();
            System.out.println("session.getId() is: "+sessid);
        }
        System.out.println("/////////// END OF REQUEST ATTRIBUTES ");
    
        //end of request stuff
        ServletContext servletContext = req.getServletContext();
        System.out.println("\\\\\\\\\\ START OF SESSION COLLECTOR STUFF ");
    
        HttpSessionCollector collector = HttpSessionCollector.getCurrentInstance(servletContext);
        Set<HttpSession> sessions = collector.getSessions();
    
        System.out.println("sessions.size() is: " + sessions.size());
        for(HttpSession sess : sessions){
            System.out.println("sess is: " + sess);
            System.out.println("sess.getId() is: " + sess.getId());
            CsrfToken sessCsrf = (CsrfToken) sess.getAttribute("org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN");
            System.out.println("csrf is: " + sessCsrf);
            if(sessCsrf!=null){
                if(sessCsrf.getToken()!=null){
                    System.out.println("sessCsrf.getToken() is: " + sessCsrf.getToken());
                } else { System.out.println("sessCsrf.getToken() is: null "); }
            } else { System.out.println("sessCsrf is: null "); }
    
            System.out.println("sess.getAttribute(SPRING_SECURITY_SAVED_REQUEST) is: " + sess.getAttribute("SPRING_SECURITY_SAVED_REQUEST") );
            if(sess.getAttribute("SPRING_SECURITY_SAVED_REQUEST") instanceof DefaultSavedRequest){
                System.out.println("_____ START PRINTING SAVED REQUEST");
                DefaultSavedRequest savedReq = (DefaultSavedRequest) sess.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
                List<Cookie> savedCookies = savedReq.getCookies();
                for(Cookie cook : savedCookies){
                    String name = cook.getName();String value = cook.getValue();
                    System.out.println("cookie name, value are: " + name + " , " + value);
                }
                Collection<String> savedHeaderNames = savedReq.getHeaderNames();
                for(String headerName : savedHeaderNames){
                    System.out.println("headerName is: " + headerName);
                }
                List<Locale> savedLocales = savedReq.getLocales();
                for(Locale loc : savedLocales){
                    System.out.println("loc.getLanguage() is: " + loc.getLanguage());
                }
                String savedMethod = savedReq.getMethod();
                System.out.println("savedMethod is: " + savedMethod);
                Map<String, String[]> savedParamMap = savedReq.getParameterMap();
                Iterator<Entry<String, String[]>> it = savedParamMap.entrySet().iterator();
                while (it.hasNext()) {
                    Entry<String, String[]> pair = it.next();
                    System.out.println("savedParamMap: " + pair.getKey() + " = " + pair.getValue());
                    it.remove(); // avoids a ConcurrentModificationException
                }
                Collection<String> savedParamNames = savedReq.getParameterNames();
                for(String savedParamName : savedParamNames){
                    System.out.println("savedParamName: " + savedParamNames);
                }
                System.out.println("_____ DONE PRINTING SAVED REQUEST");
    
            }
    
    //      System.out.println("sess.getAttribute(SPRING_SECURITY_CONTEXT) is: " + sess.getAttribute("SPRING_SECURITY_CONTEXT") );
            if(sess.getAttribute("SPRING_SECURITY_CONTEXT") instanceof SecurityContextImpl){
                SecurityContext ctxt = (SecurityContext) sess.getAttribute("SPRING_SECURITY_CONTEXT");
                Authentication auth = ctxt.getAuthentication();
    
                if(auth.getDetails() instanceof WebAuthenticationDetails){
                    WebAuthenticationDetails dets = (WebAuthenticationDetails) auth.getDetails();
                    System.out.println( "dets.getSessionId() is: " + dets.getSessionId() );
                }
                System.out.println("auth.getAuthorities() is: " + auth.getAuthorities() );
                System.out.println("auth.isAuthenticated() is: " + auth.isAuthenticated() );
            }
        }
    
        SecurityContext context = SecurityContextHolder.getContext();
        System.out.println("...........///////////// END OF DiagnoseSessionFilter.doFilterInternal() ///////////...........");
        fc.doFilter(req, res);
    
        }
    }
    


    隔离问题代码:

    以下内容结合并总结了来自HttpSessionListener 的诊断数据与网络浏览器的开发人员工具,用于用户在提交密码视图上单击提交和浏览器从/oauth/token 端点返回拒绝之间的步骤。

    如您所见,有两个 JSESSIONID 值浮动。其中一个值是正确的,而另一个值不正确。不正确的值被传递到/oauth/token 的请求中,并导致拒绝,即使传递的csrf 是正确的。因此,此问题的解决方案可能来自更改以下步骤以停止将坏JSESSIONID 替换好:

    1.) POST http://localhost:9999/uaa/secure/two_factor_authentication
        request headers:
            Referer: 9999/uaa/secure/two_factor_authentication
            Cookie: 
                JSESSIONID: ....95CB77     
                            ....918636
                XSRF-TOKEN: ....862a73
        filter chain:
            DiagnoseSessionFilter:
                request stuff:
                    Cookie header:
                        JSESSIONID: ....95CB77
                                    ....918636
                        XSRF-TOKEN: ....862a73
                    request.getSession(false).getId(): ....95CB77
                session collector stuff:
                    JSESSIONID: ....95CB77
                    csrf: ....862a73
                    SPRING_SECURITY_SAVED_REQUEST is null
                user details (from Authentication object with user/request
                    JSESSIONID: ....ED927C
                    Authenticated = true, with roles
            Complete the filter chain
            DiagnoseSessionFilter (again)
                request stuff:
                    csrf attribute: ....862a73
                    Cookie header: 
                        JSESSIONID: ....95CB77 
                                    ....918636
                        XSRF-TOKEN: ....862a73
                    request.getSession(false).getId(): 95CB77
                session collector stuff:
                    JSESSIONID: ....95CB77
                    csrf is: 862a73
                    SPRING_SECURITY_SAVED_REQUEST is null
                user details (Authentication for user/session/request)
                    JSESSIONID: ....ED927C
                    Authenticated = true, with authorities
            POST/secure/two_factor_authenticationControllerMethod
                do some stuff
        response:
            Location: 9999/uaa/oauth/authorize?....
            XSRF-TOKEN: ....862a73
    
    2.) GET http://localhost:9999/uaa/oauth/authorize?...
        request headers:
            Host: localhost:9999
            Referer: 9999/uaa/secure/two_factor_authentication
            Cookie: 
                JSESSIONID: ....95CB77    
                            ....918636
                XSRF-TOKEN: ....862a73
        FilterChain
            DiagnoseSessionFilter
                request stuff:
                    Cookie header is: JSESSIONID: ....95CB77
                                                  ....918636
                                      XSRF-TOKEN: ....862a73
                    request.getSession(false).getId(): 95CB77
                session collector stuff: 
                    JSESSIONID: ....95CB77
                    csrf is: ....862a73
                    SPRING_SECURITY_SAVED_REQUEST is: null
                user details (Authentication object with user/session/req)
                    JSESSIONID: ....ED927C
                    Authenticated = true with ALL roles.
            rest of filter chain
            TwoFactorAuthenticationFilter
                request stuff:
                    csrf request attribute is: ....862a73
                    cookie header:
                        JSESSIONID: ....95CB77
                                    ....918636
                        XSRF-TOKEN: ....862a73
                    request.getSession(false).getId() is: ....95CB77
                    updateCsrf is: ....862a73
                response stuff:
                    XSRF-TOKEN header (after manual update): ....862a73
            DiagnoseSessionFilter:
                request stuff:
                    _csrf request attribute: ....862a73
                    Cookie header:
                        JSESSIONID: ....95CB77
                                    ....918636
                        XSRF-TOKEN: ....862a73
                        request.getSession(false).getId() is: ....95CB77
                session collector stuff: 
                    JSESSIONID: ....95CB77
                    csrf is: ....862a73
                    SPRING_SECURITY_SAVED_REQUEST is: null
                user details (Authentication for user/session/request) 
                    JSESSIONID: ....ED927C
                    Authenticated is true, with ALL roles.
            CustomOAuth2RequestFactory
                request stuff:  
                    _csrf request parameter is: ....862a73
                    Cookie header: 
                        JSESSIONID: ....95CB77
                                    ....918636
                        XSRF-TOKEN: ....862a73
                    request.getSession(false).getId() is: ....95CB77
                    updateCsrf: ....862a73
                response stuff:
                    XSRF-TOKEN header: ....862a73
                session attribute printout
                    csrf: ....862a73
                    SPRING_SECURITY_CONTEXT (not printed, so don't know values)
        response:
            Location: 8080/login?code=myNwd7&state=f6b3Km
            XSRF-TOKEN: ....862a73
    
    3.) GET http://localhost:8080/login?code=myNwd7&state=f6b3Km
        request headers:
            Host: localhost:8080
            Referer: 9999/uaa/secure/two_factor_authentication
            Cookie:  
                JSESSIONID: ....918636
                XSRF-TOKEN: ....862a73
        UiAppFilterChain:
            HttpSessionSecurityContextRepository
                creates new SPRING_SECURITY_CONTEXT to replace null one
            OAuth2ClientAuthenticationProcessingFilter (position 8 of 14)
                AuthorizationCodeAccessTokenProvider
                    Retrieving token from 9999/uaa/oauth/token
        AuthServerFilterChain:
            DiagnoseSessionFilter
                request stuff:
                    XSRF-TOKEN header is: null
                    Cookie header is: null
                    Set-Cookie header is: null
                    referrer header is: null
                    request.getSession(false) is: null
                session collector stuff:
                    JSESSIONID: ....95CB77
                    sessCsrf.getToken() is: 862a73
                    SPRING_SECURITY_SAVED_REQUEST is: null
                    Authenticated is true but with ONLY these roles: 
                        ROLE_HOBBIT, ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED
                SecurityContextPersistenceFilter
                    reports no HttpSession and no SPRING_SECURITY_CONTEXT
                CsrfFilter
                    rejects request to /oauth/token due to no session % csrf
    
        response headers:
            Set-Cookie: 
                XSRF-TOKEN: ....527fbe
                X-Frame-Options: DENY
    

    考虑到您提供的点数,我将尝试多花一点时间来进一步隔离解决方案。但以上应该大大缩小问题。

    我在它完全完成之前发布这个,因为你的赏金期即将到期。

    【讨论】:

      【解决方案2】:

      您的问题解决了吗?我一直在四处寻找 2FA 和 spring-security-oauth2 的完整样本。很高兴您发布了完整的概念和完整的资源。

      我尝试了您的软件包,您的问题只需更改 AuthserverApplication.java 中的 1 行代码即可解决

      @Override
          protected void configure(HttpSecurity http) throws Exception {
              // @formatter:off
              http
                  .formLogin().loginPage("/login").permitAll()
              .and()
                      .requestMatchers().antMatchers("/login", "/oauth/authorize", "/secure/two_factor_authentication", "/pincode")
              .and()
                      .authorizeRequests().anyRequest().authenticated();
              // @formatter:on
          }
      

      你的原始配置通过了 spring security 的认证链,它返回了一个 null 的认证对象。

      我还建议您将 CustomOAuth2RequestFactory 的 Bean 创建更改为以下内容,覆盖链中的所有 OAuth2RequestFactory

      @Bean
          public OAuth2RequestFactory customOAuth2RequestFactory(){
              return new CustomOAuth2RequestFactory(clientDetailsService);
          }
      

      对于您为处理 CSRF 添加的代码,您可以简单地删除它们,例如。 2FA 控制器:

      @Controller
      @RequestMapping(TwoFactorAuthenticationController.PATH)
      public class TwoFactorAuthenticationController {
          private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);
          public static final String PATH = "/secure/two_factor_authentication";
          public static final String AUTHORIZE_PATH = "/oauth/authorize";
          public static final String ROLE_TWO_FACTOR_AUTHENTICATED = "ROLE_TWO_FACTOR_AUTHENTICATED";
      
          private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
      
          @RequestMapping(method = RequestMethod.GET)
          public String auth(HttpServletRequest request, HttpSession session, HttpServletResponse resp/*, ....*/) {
              System.out.println("-------- inside GET /secure/two_factor_authentication --------------");
              if (AuthenticationUtil.isAuthenticatedWithAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
                  LOG.info("User {} already has {} authority - no need to enter code again", ROLE_TWO_FACTOR_AUTHENTICATED);
      //            throw ....;
              }
              else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
      //            LOG.warn("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
      //          throw ....;
              }
              return "pinCode";
          }
      
          @RequestMapping(method = RequestMethod.POST)
          public String auth(FormData formData, HttpServletRequest req, HttpServletResponse resp,
                                                  SessionStatus sessionStatus, Principal principal, Model model)
              throws IOException{
      
              if (formData.getPinVal()!=null) {
                  if(formData.getPinVal().equals("5309")){
                      AuthenticationUtil.addAuthority(ROLE_TWO_FACTOR_AUTHENTICATED);
                      return "redirect:"+AUTHORIZE_PATH;
                  };
              };
      
              return "pinCode";
          }
      }
      

      如果您需要清理后的完整源代码,请告诉我。

      【讨论】:

      • 非常感谢。你解决了问题。我将此标记为已接受的答案。我希望在您给出答案时,我们的时间安排使赏金仍然可用,因为您的答案是问题的真正解决方案。欢迎来到堆栈溢出。
      • 这是双赢的,因为我需要一个完整的样本和清晰的概念图供我的团队使用。非常感谢您的贡献,节省了我创建自己的时间。你的项目是我能在社区中找到的唯一完整的源代码。
      • 这个小改动也可以解决您另一个问题中的问题。 stackoverflow.com/questions/37061697/…
      • 我也面临同样的问题。源代码不再可用,您介意附加代码吗?请告诉我那一行是什么
      猜你喜欢
      • 2014-12-11
      • 2012-03-31
      • 2013-12-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-07-17
      • 1970-01-01
      • 2019-07-17
      相关资源
      最近更新 更多