替换 AuthorizationRequest 后的 HttpSession null
HttpSession null after replacing AuthorizationRequest
Complete code 下面给出了快速重现问题的说明。
问题:
无法找到 /oauth/token
端点之前的 Spring 安全过滤器链中 DefaultOAuth2RequestFactory
replaces the current AuthorizationRequest
with a saved AuthorizationRequest
. This causes failure of the subsequent request to /oauth/token
because the CsrfFilter 的自定义实现后,HttpSession
变为 null
session
Csrf token
在 null
session
中与 request
的 Csrf token
进行比较。
错误期间的控制流程:
下面的流程图说明了步骤 14 和 步骤 15 以某种方式 null
- 化 HttpSession
。 (或者可能与 JSESSIONID
不匹配。)步骤 14 中 CustomOAuth2RequestFactory.java
开头的 SYSO
表明确实存在 HttpSession
实际上包含正确的 CsrfToken
。然而,不知何故,当 步骤 15 触发来自 localhost:8080/login
url 客户端的调用时,HttpSession
已变为 null
回到 localhost:9999/oauth/token
端点。
断点已添加到下面调试日志中提到的 HttpSessionSecurityContextRepository
的每一行。 (它位于 authserver
eclipse 项目的 Maven Dependencies
文件夹中。)这些断点确认 the HttpSession
is null
when the final request to /oauth/token
是在下面的流程图中制作的。 (流程图的左下角。) null
HttpSession
可能是由于在自定义 [=13= 之后保留在浏览器中的 JSESSIONID
变得过时了] 代码运行。
如何解决此问题,以便在流程图中的第 15 步结束后,在对 /oauth/token
端点的最终调用期间保留相同的 HttpSession
?
相关代码和日志:
CustomOAuth2RequestFactory.java
can be viewed at a file sharing site by clicking on this link. 的完整代码我们可以猜测 null
session
是由于 1.) JSESSIONID
未通过 CustomOAuth2RequestFactory
中的代码在浏览器中更新,或 2.) HttpSession
实际上被 null
化。
在 步骤 15 之后调用 /oauth/token
的 Spring 引导调试日志清楚地表明到那时没有 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
启动 resource
应用程序,然后键入 mvn spring-boot:run
5.) 通过导航至 oauth2/ui
启动 ui
应用程序,然后键入 mvn spring-boot:run
6.) 打开网络浏览器并导航至 http : // localhost : 8080
7.) 点击Login
然后输入Frodo
作为用户和MyRing
作为密码,点击提交。
8.) 输入 5309
作为 Pin Code
然后点击提交。 这将触发上面显示的错误。
Spring 引导调试日志将显示很多 SYSO
,它给出了 XSRF-TOKEN
和 HttpSession
等变量的值流程图中显示的每个步骤。 SYSO
有助于对调试日志进行分段,以便它们更易于解释。所有 SYSO
都由一个 class 调用另一个 classes 完成,因此您可以操纵 SYSO
-生成 class 来更改报告中的任何地方控制流程。 SYSO
-生成class的名称是TestHTTP
,其源代码可以在同一个demo
包中找到。
使用调试器:
1.) Select 终端 window 即 运行 authserver
应用程序并键入 Ctrl-C
停止 authserver
应用程序.
2.) 将三个应用程序 (authserver
、resource
和 ui
) 作为 existing maven projects 导入到 eclipse 中。
3.) 在 authserver
应用程序的 eclipse Project Explorer 中,单击以展开 Maven Dependencies
文件夹 ,然后在其中向下滚动以单击以展开Spring-Security-web...
jar 如下图橙色圆圈所示。然后滚动查找并展开 org.springframework.security.web.context
包。然后双击打开下面屏幕截图中以蓝色突出显示的 HttpSessionSecurityContextRepository
class。在此 class 中的每一行添加断点。您可能想对同一包中的 SecurityContextPersistenceFilter
class 执行相同的操作。 这些断点将使您能够看到 HttpSession
的值,该值当前在控制流结束之前变为 null
,但需要具有有效值可以映射到 XSRF-TOKEN
以解决此 OP。
4.) 在应用程序的 demo
包中,在 CustomOAuth2RequestFactory.java
内添加断点。然后 Debug As... Spring Boot App
启动调试器。
5.) 然后重复上面的步骤 6 到 8。您可能希望在每次新尝试之前清除浏览器的缓存。您可能希望打开浏览器开发人员工具的“网络”选项卡。
在最终调用 localhost :9999/uaa/oauth/token
时,您的 authserver
应用中的会话不为空 。 不仅存在会话,而且有效会话的 JSESSIONID
和 csrf
令牌匹配用户提交点之间的控制流 中存在的值正确的 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 安全过滤器链中以提供诊断信息,如下:
@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
的诊断数据和网络浏览器的开发人员工具,用于用户在提交 pin 代码视图上单击提交和浏览器从 [=17 返回拒绝之间的步骤=]端点。
如您所见,有两个 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
考虑到您提供的点数,我会尝试多花一点时间来进一步分离解决方案。但以上内容应该可以大大缩小问题的范围。
我在它完全完成之前发布它,因为您的赏金期即将到期。
你的问题解决了吗?我一直在四处寻找 2FA 的完整样本以及 spring-security-oauth2。很高兴您发布了完整的概念和完整的资源。
我试过你的包裹,你的问题只需更改 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
}
您的原始配置通过了 spring 安全性的身份验证链,返回了一个空的身份验证对象。
我还建议您将 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";
}
}
如果您需要清理后的完整源代码,请告诉我。
Complete code 下面给出了快速重现问题的说明。
问题:
无法找到
/oauth/token
端点之前的 Spring 安全过滤器链中 DefaultOAuth2RequestFactory
replaces the current AuthorizationRequest
with a saved AuthorizationRequest
. This causes failure of the subsequent request to /oauth/token
because the CsrfFilter 的自定义实现后,HttpSession
变为 null
session
Csrf token
在 null
session
中与 request
的 Csrf token
进行比较。
错误期间的控制流程:
下面的流程图说明了步骤 14 和 步骤 15 以某种方式 null
- 化 HttpSession
。 (或者可能与 JSESSIONID
不匹配。)步骤 14 中 CustomOAuth2RequestFactory.java
开头的 SYSO
表明确实存在 HttpSession
实际上包含正确的 CsrfToken
。然而,不知何故,当 步骤 15 触发来自 localhost:8080/login
url 客户端的调用时,HttpSession
已变为 null
回到 localhost:9999/oauth/token
端点。
断点已添加到下面调试日志中提到的 HttpSessionSecurityContextRepository
的每一行。 (它位于 authserver
eclipse 项目的 Maven Dependencies
文件夹中。)这些断点确认 the HttpSession
is null
when the final request to /oauth/token
是在下面的流程图中制作的。 (流程图的左下角。) null
HttpSession
可能是由于在自定义 [=13= 之后保留在浏览器中的 JSESSIONID
变得过时了] 代码运行。
如何解决此问题,以便在流程图中的第 15 步结束后,在对 /oauth/token
端点的最终调用期间保留相同的 HttpSession
?
相关代码和日志:
CustomOAuth2RequestFactory.java
can be viewed at a file sharing site by clicking on this link. 的完整代码我们可以猜测 null
session
是由于 1.) JSESSIONID
未通过 CustomOAuth2RequestFactory
中的代码在浏览器中更新,或 2.) HttpSession
实际上被 null
化。
在 步骤 15 之后调用 /oauth/token
的 Spring 引导调试日志清楚地表明到那时没有 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
启动 resource
应用程序,然后键入 mvn spring-boot:run
5.) 通过导航至 oauth2/ui
启动 ui
应用程序,然后键入 mvn spring-boot:run
6.) 打开网络浏览器并导航至 http : // localhost : 8080
7.) 点击Login
然后输入Frodo
作为用户和MyRing
作为密码,点击提交。
8.) 输入 5309
作为 Pin Code
然后点击提交。 这将触发上面显示的错误。
Spring 引导调试日志将显示很多 SYSO
,它给出了 XSRF-TOKEN
和 HttpSession
等变量的值流程图中显示的每个步骤。 SYSO
有助于对调试日志进行分段,以便它们更易于解释。所有 SYSO
都由一个 class 调用另一个 classes 完成,因此您可以操纵 SYSO
-生成 class 来更改报告中的任何地方控制流程。 SYSO
-生成class的名称是TestHTTP
,其源代码可以在同一个demo
包中找到。
使用调试器:
1.) Select 终端 window 即 运行 authserver
应用程序并键入 Ctrl-C
停止 authserver
应用程序.
2.) 将三个应用程序 (authserver
、resource
和 ui
) 作为 existing maven projects 导入到 eclipse 中。
3.) 在 authserver
应用程序的 eclipse Project Explorer 中,单击以展开 Maven Dependencies
文件夹 ,然后在其中向下滚动以单击以展开Spring-Security-web...
jar 如下图橙色圆圈所示。然后滚动查找并展开 org.springframework.security.web.context
包。然后双击打开下面屏幕截图中以蓝色突出显示的 HttpSessionSecurityContextRepository
class。在此 class 中的每一行添加断点。您可能想对同一包中的 SecurityContextPersistenceFilter
class 执行相同的操作。 这些断点将使您能够看到 HttpSession
的值,该值当前在控制流结束之前变为 null
,但需要具有有效值可以映射到 XSRF-TOKEN
以解决此 OP。
4.) 在应用程序的 demo
包中,在 CustomOAuth2RequestFactory.java
内添加断点。然后 Debug As... Spring Boot App
启动调试器。
5.) 然后重复上面的步骤 6 到 8。您可能希望在每次新尝试之前清除浏览器的缓存。您可能希望打开浏览器开发人员工具的“网络”选项卡。
在最终调用 localhost :9999/uaa/oauth/token
时,您的 authserver
应用中的会话不为空 。 不仅存在会话,而且有效会话的 JSESSIONID
和 csrf
令牌匹配用户提交点之间的控制流 中存在的值正确的 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 安全过滤器链中以提供诊断信息,如下:
@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
的诊断数据和网络浏览器的开发人员工具,用于用户在提交 pin 代码视图上单击提交和浏览器从 [=17 返回拒绝之间的步骤=]端点。
如您所见,有两个 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
考虑到您提供的点数,我会尝试多花一点时间来进一步分离解决方案。但以上内容应该可以大大缩小问题的范围。
我在它完全完成之前发布它,因为您的赏金期即将到期。
你的问题解决了吗?我一直在四处寻找 2FA 的完整样本以及 spring-security-oauth2。很高兴您发布了完整的概念和完整的资源。
我试过你的包裹,你的问题只需更改 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
}
您的原始配置通过了 spring 安全性的身份验证链,返回了一个空的身份验证对象。
我还建议您将 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";
}
}
如果您需要清理后的完整源代码,请告诉我。