2016-05-30 283 views
22

A 500点奖励提供。 Complete code以及如何快速重现问题的说明如下。
替换授权请求后的HttpSession为空


问题:
HttpSession成为null后的DefaultOAuth2RequestFactory自定义实现替换当前AuthorizationRequest使用保存AuthorizationRequest这将导致后续请求到 /oauth/token失败,因为在/oauth/token终点前的春季安全过滤器链的CsrfFilter无法找到sessionCsrf tokennullsession与比较requestCsrf token


控制流在出错过程:

下面的流程图图示了其中步骤14步骤15不知何故null化的HttpSession。 (或可能与JSESSIONID不匹配。)CustomOAuth2RequestFactory.java开始处的SYSO步骤14显示确实存在实际上包含正确的CsrfTokenHttpSession。然而,在某种程度上,HttpSession已变成null步骤15触发从客户端localhost:8080/login URL呼叫回到localhost:9999/oauth/token端点。

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

在流程图中的步骤15结束之后,如何解决此问题,以便在最终呼叫端点/oauth/token期间仍保留相同的HttpSession


相关代码和原木:

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

为后步骤调用/oauth/token春天开机调试日志15明确规定,没有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.se[email protected]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 


重新创建您电脑上的问题:

您可以在几分钟内通过以下简单的步骤重新创建任何计算机上的问题ps:

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

5.)通过导航到oauth2/ui,然后键入mvn spring-boot:run

6.)打开网页浏览器并启动ui应用启动resource应用导航到http : // localhost : 8080

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

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

春季启动调试日志将显示的SYSO很多,其给出的变量,如XSRF-TOKENHttpSession在该流程图中所示的各步骤的值。 SYSO帮助细分调试日志,以便更易于解释。所有SYSO由其他类调用的一个类完成,因此您可以操作SYSO生成的类来更改控制流中的任何地方的报告。 SYSO生成类的名称是TestHTTP,其源代码可以在相同的demo包中找到。


使用调试器:

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

2.)导入三个应用程序(authserverresourceui)到Eclipse作为现有行家项目

3。)在authserver应用程序的Eclipse项目资源管理器,单击展开Maven Dependencies文件夹,然后在它向下滚动到单击展开Spring-Security-web...罐子如以橙色显示圆圈中的形象。然后滚动查找并展开org.springframework.security.web.context包。然后双击打开下面屏幕截图中以蓝色突出显示的HttpSessionSecurityContextRepository类。为此类中的每一行添加断点。您可能想要在同一个包中对SecurityContextPersistenceFilter类执行相同的操作。 这些断点将使您能够看到在控制流程结束之前当前变为nullHttpSession的值,但需要有一个可以映射到XSRF-TOKEN的有效值才能解决此OP。

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

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

回答

1

你解决了你的问题吗?我一直在四处寻找与spring-security-oauth2一起找到2FA的完整样本。你已经发布了完整的概念和完整的资料来源,这真是太好了。

我想你的包,你的问题可以简单地通过传递春季安全的身份验证链,其返回你一个空变化的只有1行代码在你AuthserverApplication.java

@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 
    } 

你原来的配置来解决验证对象。

我也建议您在创建bean CustomOAuth2RequestFactory的改变其下面覆盖所有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"; 
    } 
} 

请在清理完成后请告诉我是否需要完整的源代码。

+0

非常感谢你。你解决了这个问题。我将此标记为接受的答案。我希望我们的时间安排能够让您在给出答案时仍然可以获得赏金,因为您的答案是解决问题的真正方法。欢迎来到堆栈溢出。 – CodeMed

+0

这是一个双赢的过程,因为我需要一个完整的样本和清晰的概念图表来为我的团队合作。非常感谢你的贡献,节省了我创建自己的时间。您的项目是我可以在社区中找到的唯一完成的源。 –

+0

这个小小的改变也可以解决你的另一个问题。 http://stackoverflow.com/questions/37061697/invalid-xsrf-token-at-oauth-token –

3

在最终致电localhost :9999/uaa/oauth/token时,会话不是空的在您的authserver应用程序中。 不仅有会话,而且在控制流中有效会话匹配值的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) { 
    } 

} 

然后我打电话给上面的HttpSessionListener一个定制的实现的OncePerRequestFilter,我插入到您的authserver应用的春季安全过滤器链提供诊断信息,如下所示:

@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中总结了诊断数据,用于点击提交引脚代码视图上的提交和浏览器返回来自端点的拒绝之间的步骤。

正如你所看到的,有两个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 

我会尽量多花一点时间,这进一步隔离解决方案,给出您提供的点数。但上述应该大大缩小问题的范围。

因为您的赏金期即将到期,所以我在发布之前发布此内容。