Spring Security + Thymeleaf - 第二次登录后的 TemplateInputException

Posted

技术标签:

【中文标题】Spring Security + Thymeleaf - 第二次登录后的 TemplateInputException【英文标题】:Spring Security + Thymeleaf - TemplateInputException after a second login 【发布时间】:2021-12-20 17:54:39 【问题描述】:

我有一个带有 LOGINPROFILELOGOUT 的导航栏。 我的目标是删除 LOGIN 并仅显示 PROFILE 和 LOGOUT 用户登录后。 我让控制器检查经过身份验证的用户:

@Controller
public class LoginController 

    @GetMapping("/login")
    private String loginRender()
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication instanceof AnonymousAuthenticationToken)
            return "login";
        else 
            return "redirect:/";
        
    

Thymeleaf 模板如下所示:

<ul class="logout-ul top-links-container">
    <li class="top-links-item  text-center" sec:authorize="!isAuthenticated()"><a href="/login"> <i class="icon-user4" style="color: #e35f5f"></i>Login</a></li>
    <li class="top-links-item text-center" sec:authorize="isAuthenticated()"><a href="/user/profile"><i class="icon-user4" style="color: #e35f5f"></i>Profile</a></li>
    <li class="text-center logout" sec:authorize="isAuthenticated()"><form th:action="@/logout" th:method="post"><button title="LOGOUT" class="btn" type="submit"><i class="icon-off"></i></button></form></li>
</ul>

配置:

 http
            .authorizeRequests()
            .antMatchers("/login", "/", "register",
                    "/custom-transfers/**", "/about-us", "/information/**", "/support/**").permitAll()
            .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
            .antMatchers("/admin").hasRole(UserRoleEnum.ADMIN.name())
            .antMatchers("/**").authenticated()
          .and()
                .formLogin()
                .loginPage("/login")
                .usernameParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY)
                .passwordParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY)
                .defaultSuccessUrl("/user/profile")
                //TODO validation page
                .failureForwardUrl("/login")
          .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID");

问题 当用户第一次登录时一切正常。当注销并再次登录时,Thymleaf 显示 TemplateInputException。

例外:

ERROR 26304 --- [nio-8080-exec-9] s.e.ErrorMvcAutoConfiguration$StaticView : Cannot render error page for request [/login] and exception [An error happened during template parsing (template: "class path resource [templates/login.html]")] as the response has already been committed. As a result, the response may have the wrong status code.

org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates/login.html]")
    at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parse(AbstractMarkupTemplateParser.java:241) ~[thymeleaf-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parseStandalone(AbstractMarkupTemplateParser.java:100) ~[thymeleaf-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.thymeleaf.engine.TemplateManager.parseAndProcess(TemplateManager.java:666) ~[thymeleaf-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1098) ~[thymeleaf-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1072) ~[thymeleaf-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.thymeleaf.spring5.view.ThymeleafView.renderFragment(ThymeleafView.java:366) ~[thymeleaf-spring5-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.thymeleaf.spring5.view.ThymeleafView.render(ThymeleafView.java:190) ~[thymeleaf-spring5-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1400) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1145) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1084) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[tomcat-embed-core-9.0.53.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[tomcat-embed-core-9.0.53.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.53.jar:9.0.53]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.53.jar:9.0.53]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.53.jar:9.0.53]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.53.jar:9.0.53]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.53.jar:9.0.53]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:327) ~[spring-security-web-5.5.2.jar:5.5.2]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:115) ~[spring-security-web-5.5.2.jar:5.5.2]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:81) ~[spring-security-web-5.5.2.jar:5.5.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.2.jar:5.5.2]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:121) ~[spring-security-web-5.5.2.jar:5.5.2]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:115) ~[spring-security-web-5.5.2.jar:5.5.2]

我尝试将 sec:authorize="!isAuthenticated()" 放在 &lt;div&gt; 外部,尝试使用 RedirectAttributeModelboolean 传递给模板,但我仍然无法修复问题。 当我添加时它起作用: &lt;li class="top-links-item" sec:authorize="!isAuthenticated()"&gt;TEST&lt;/li&gt; 所以我假设是因为我使用sec:authorize 作为登录页面,该页面由 Spring Security 管理。有什么关系吗?

更新 我发现注销后 Cookie 会被破坏,并且不会为第二次登录创建新的。此外,当我添加sec:authorize=isAuthenticated() 时,问题仅在于 html 中的注销表单。因此,问题与注销而不是登录页面有关。为什么会发生这种情况?

只有当我在 Thymleaf 中使用 isAuthenticated() 时才会出现这种情况。

我做错了什么? 我将衷心感谢您的帮助。 谢谢。

【问题讨论】:

我没有发现您共享的代码有任何问题,并且我无法重现此问题。一些提示,请确保您在 HTML 文件中设置 xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5 并包括依赖项 org.thymeleaf.extras:thymeleaf-extras-springsecurity5 @EleftheriaStein-Kousathana 谢谢你的回答。是的,设置了依赖项和 Thymeleaf 标记,但仅当我“隐藏和显示”注销 &lt;li&gt; 行时才会出现问题。目前,注销按钮对所有用户始终可见,没有问题,但我无法为登录的用户隐藏它。你知道还有什么地方可能是问题吗? 我在想 Spring Security 是否会阻止使用 csrf 令牌隐藏表单,但它似乎并不准确。 尝试简化配置以缩小问题范围。如果登录页面只有一个 Thymeleaf 模板会发生什么?如果您从“/login”端点删除逻辑并简单地返回页面会发生什么? @EleftheriaStein-Kousathana 我刚刚尝试了所有这些。从控制器中删除了逻辑,只返回了登录模板,并更改了安全配置中的一些内容,但仍然登录页面中断并出现相同的错误。但是,如果用户登录,我在控制器中使用 th:hidden="$logged" 和 Model 解决了这个问题。它现在可以工作,但我仍然想知道可能是什么原因。再次感谢您的帮助。 【参考方案1】:

我认为这里发生的情况如下:

您的注销表单由 Thymeleaf 管理,这意味着 Thymeleaf 将尝试在内部创建一个带有 CSRF 令牌的隐藏字段。这通常有效,并且需要现有的或新的会话。

问题在于 Thymeleaf 旨在通过在处理您的模板时生成模板输出来最小化延迟。因此,当引擎到达您的注销 &lt;form&gt; 标记时,当页面大小较长(如您的情况)时,一些输出可能已经流式传输到客户端和服务器开始将您的回复视为已提交。一旦响应被标记为已提交,Thymeleaf 将无法访问/创建会话。我想这是一个架构约束。

所以我不确定在这里给你什么建议。也许尝试在模板中更早地输出注销按钮,或者尝试急切地初始化 CSRF 令牌。 HttpSecurity 中的这个设置可能会这样做:

.and()
   .csrf()
   .csrfTokenRepository(new HttpSessionCsrfTokenRepository())

【讨论】:

非常感谢您的详细解释。 .csrfTokenRepository(new HttpSessionCsrfTokenRepository()) 确实解决了这个问题。

以上是关于Spring Security + Thymeleaf - 第二次登录后的 TemplateInputException的主要内容,如果未能解决你的问题,请参考以下文章

Spring 中的 spring-security-oauth2 与 spring-security-oauth2-core

Spring mvc / security:从spring security中排除登录页面

Spring Security:2.4 Getting Spring Security

没有 JSP 的 Spring Security /j_spring_security_check

Spring-Security

Spring Security 登录错误:HTTP 状态 404 - /j_spring_security_check