替换AuthorizationRequest后,HttpSession为空。

24
问题
在自定义实现DefaultOAuth2RequestFactory替换当前AuthorizationRequest为保存的AuthorizationRequest后,HttpSession变为null。这导致后续请求/oauth/token失败,因为Spring Security过滤器链中位于/oauth/token端点之前的CsrfFilter无法在nullsession中找到session Csrf token以与requestCsrf token进行比较。
错误期间的控制流程
以下流程图说明了在第14步和第15步中,某种方式使得HttpSession变为null,或者可能导致JSESSIONID不匹配。在CustomOAuth2RequestFactory.java的第14步的开头处的SYSO显示确实存在一个包含正确CsrfToken的HttpSession。然而,不知何故,在第15步时,当客户端从localhost:8080/login的url回调到localhost:9999/oauth/token的端点时,HttpSession已经变为null。
在下面的调试日志中,每行都添加了断点到{{link1:HttpSessionSecurityContextRepository}}。 (它位于authserver Eclipse项目的Maven Dependencies文件夹中。)这些断点确认了在下面的流程图中进行最终请求/oauth/token时,HttpSessionnull nullHttpSession可能是由于在自定义的DefaultOAuth2RequestFactory代码运行后,浏览器中的JSESSIONID变得过时所致。 如何解决这个问题,使得在流程图中的第15步结束后,相同的HttpSession在最终调用/oauth/token端点时仍然存在?

相关代码和日志

我们可以猜测,null session 是由以下两种情况之一引起的:1.) CustomOAuth2RequestFactory 中的代码未在浏览器中更新 JSESSIONID,或者 2.) HttpSession 实际上被设置为 null

第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. 下载应用程序的压缩版本(编辑注:很遗憾,此版本已不再提供)。

  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. 点击登录,然后输入Frodo作为用户名,MyRing作为密码,然后点击提交。

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

Spring Boot的调试日志会显示很多SYSO,它会给出变量的值,比如XSRF-TOKEN和HttpSession,以及流程图中每一步的值。SYSO有助于将调试日志分段,使其更容易解释。所有的SYSO都是由一个名为TestHTTP的类调用其他类来完成的,因此您可以操作SYSO生成类来改变控制流中的报告。SYSO生成类的名称是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,但需要一个有效值,可以将其映射到XSRF-TOKEN以解决此问题。

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

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


这个问题看起来很棒。不幸的是,源代码不再可用(在文件锁定器上出现404错误),所以这个问题现在无法再现。它需要被暂时搁置,因为缺少一个最小化可复现的示例([mcve]),但如果你能再次找到文件并上传(到一个永久的地方),它可以重新开放。 - undefined
2个回答

4
当您的authserver应用在最后一次调用localhost:9999/uaa/oauth/token时,“会话不为空”。不仅有一个会话存在,而且有效会话的JSESSIONIDcsrf token值与用户提交正确PIN和发生/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 Security过滤器链中以提供诊断信息,如下所示:

@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代替正确的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

鉴于您提供的点数数量,我会花更多时间来进一步分离解决方案,但以上内容应该已经大大缩小了问题的范围。

由于您的悬赏期即将结束,我在完成前发布此贴。


1
你解决了你的问题吗?我一直在寻找一个完整的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的认证链,返回了一个空的身份验证对象。
我还建议你将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";
    }
}

请问您需要清理后的完整源代码吗?

非常感谢您。您解决了这个问题。我将把它标记为已接受的答案。但愿我们的时间安排能够让悬赏在您回答时仍然可用,因为您的答案是真正的解决方案。欢迎来到Stack Overflow。 - CodeMed
这是一个双赢的局面,因为我需要一个完整的样例和清晰的概念图表供我的团队使用。非常感谢你的贡献,节省了我自己创建所需的时间。在社区中,你的项目是我能找到的唯一已完成的源。 - Alan Chow
这个小改动也可以解决你另一个问题中的问题。https://dev59.com/JOk6XIcBkEYKwwoYBvfx - Alan Chow
我也遇到了同样的问题。源代码不再可用,您能否附上代码?请告诉我那是哪一行。 - user1047873

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接