65.9K
CodeProject 正在变化。 阅读更多。
Home

将 ThymeLeaf 页面模板引擎与 Spring Security 结合使用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2021 年 2 月 16 日

MIT

17分钟阅读

viewsIcon

14812

downloadIcon

203

本教程介绍如何设置 Spring Security 和 Spring MVC 应用程序,并使用 Thymeleaf 进行安全页面渲染。

引言

这是我关于 Thymeleaf 模板引擎的第二篇教程。在本教程中,我想展示如何将 Spring Security 与 Thymeleaf 模板引擎集成。我之前写过一篇关于如何在 Spring MVC 应用程序中添加 Spring Security 的教程。在本教程中,我会回顾一下。我学到了一些新的概念。最重要的一部分是如何将 Spring Security 标签与 Thymeleaf 模板引擎一起使用以保护页面渲染。总的来说,要让所有这些都正常工作并不困难。我基于我之前的 Thymeleaf 教程构建了这个示例。我之前的 Thymeleaf 教程可以在这里找到。本教程也大量借鉴了我关于 Spring Security 集成的另一篇教程。你可以在这里找到这篇教程。你不必去查看这两篇教程。这里会提到重要的概念。请继续阅读。

应用程序架构

本教程中的示例应用程序要求用户先登录,否则用户将无法访问应用程序提供的安全资源。Web 应用程序将有一些新的配置需要添加到启动过程中。为了添加这种配置,我不得不将 `spring-boot-starter-security` jar 添加到项目中。一旦添加了这个 jar,用户就必须登录才能访问页面。这意味着我必须做一些更多的配置更改,以允许某些页面匿名访问,而其他页面则仅限于具有属性访问权限的已登录用户。我将解释如何做到这一点,以及用户身份验证和授权检查是如何工作的。

本教程更重要的概念是用于显示或隐藏网页部分的安全性标签。网页是使用 Thymeleaf 模板引擎渲染的。我关心的一件事是这些标签是什么。我对它们如何使用一点都不担心,因为我大概知道。对于这个示例应用程序,我只使用了基于角色的授权。角色的分配是安全配置的一部分,并将详细解释。然后,还有安全标签。这些是 Thymeleaf 特有的标签,与我在另一篇教程中使用的 JSTL/Spring Security 标签非常相似。

由于本教程混合了 Spring Security 和 Thymeleaf,让我们从 Maven POM 文件开始。关于这个文件有几件事需要讨论。

Maven POM 文件

对于这个示例应用程序,我将把它打包成一个 jar 文件。这与我之前 Thymeleaf 教程的做法相同。使用 Thymeleaf 模板引擎,我无需担心将 Web 应用程序打包成 war 格式。为了支持在 HTML 标记中使用 Spring Security 和安全标签,我必须在 Maven POM 文件中包含这些。首先,我需要为我的项目声明父 POM,即 `spring-boot-starter-parent`。

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.1.RELEASE</version>
	</parent>

接下来,我需要添加所有必需的依赖项。对于 Spring Boot 应用程序,所需的依赖项很少。它们是:

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-thymeleaf</artifactId>
      </dependency>
      <dependency>
         <groupId>org.thymeleaf.extras</groupId>
         <artifactId>thymeleaf-extras-springsecurity5</artifactId>
         <version>3.0.4.RELEASE</version>
      </dependency>
	</dependencies>

关于这一点有几点需要了解。要启用应用程序的安全性,我需要将 `spring-boot-starter-security` 添加为依赖项。添加后,默认情况下,应用程序的所有请求都必须经过身份验证。一些配置可以使请求访问更灵活。我将在后面的部分向您展示如何做。接下来,我需要添加 Thymeleaf 核心的依赖项,即 `spring-boot-starter-thymeleaf`。添加此依赖项可以让我使用 HTML 页面中的 Thymeleaf 标签,并允许我创建可重用组件,称为片段。为了能够为 HTML 页面使用安全标签,我必须添加最后一个依赖项,称为 `thymeleaf-extras-springsecurity5`。

这五个依赖项都对使此应用程序正常工作是必需的。这是第一步。在下一节中,我将向您展示启动类的源代码以及安全配置。安全配置是最重要的,因为它决定了哪些请求可以未经身份验证而访问,哪些不能。

启动和安全配置

启动类非常简单。这是该类:

package org.hanbo.boot.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App
{
   public static void main(String[] args)
   {
      SpringApplication.run(App.class, args);
   }
}

我只需要声明一个名为“`App`”的类。然后用 `@SpringBootApplication` 注解来装饰它。然后将其传递给 `SpringApplication.run()`。Spring Boot 使用依赖注入来查找对象类型的相互依赖关系。这是创建依赖项映射的地方。Spring IOC 容器将扫描包以查找所有类类型。只要类用 `@Configuration`、`@Repository`、`@Service` 或 `@Components` 以及 `Controller` 和 `RestController` 注解,Spring 就会找到它们并为它们创建一个查找表。然后,对象实例化将使用映射来查找要注入的对象。

让我们来看看用于设置 Spring Security 的配置类。这是此示例应用程序中最难的部分。本教程提供了最基本的表单认证与 Web 应用程序集成的配置。这是该类的完整源代码:

package org.hanbo.boot.app.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.
       authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.
       method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.
       web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.
       SavedRequestAwareAuthenticationSuccessHandler;
import org.hanbo.boot.app.security.UserAuthenticationService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter
{
   @Autowired
   private UserAuthenticationService authenticationProvider;
   
   @Autowired
   private AccessDeniedHandler accessDeniedHandler;
   
   @Override
   protected void configure(HttpSecurity http) throws Exception
   {
      http
      .authorizeRequests()
          .antMatchers("/assets/**", "/public/**").permitAll()
          .anyRequest().authenticated()
      .and()
      .formLogin()
          .loginPage("/login")
          .permitAll()
          .usernameParameter("username")
          .passwordParameter("userpass")
          .successHandler(new SavedRequestAwareAuthenticationSuccessHandler())
          .defaultSuccessUrl("/secure/index", true).failureUrl("/public/authFailed")
          .and()
      .logout().logoutSuccessUrl("/public/logout")
          .permitAll()
          .and()
      .exceptionHandling().accessDeniedHandler(accessDeniedHandler);
   }
   
   @Override
   protected void configure(AuthenticationManagerBuilder authMgrBuilder)
      throws Exception
   {
      authMgrBuilder.authenticationProvider(authenticationProvider);
   }
}

这个类有几个有趣的地方。首先,该类声明如下:

...
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter
{
...
}

该类被三个不同的注解装饰。第一个将类标记为配置提供者(`@Configuration`)。第二个为 Web 应用程序启用 Web 安全(`@EnableWebSecurity`)。第三个是一个有趣的注解,它的作用是启用类方法中的安全注解,特别是在控制器或 `RestController` 类的方**法中,并锁定对这些方法的访问。

该类继承自 `WebSecurityConfigurerAdapter`。它可用于提供 Web 安全和 HTTP 安全的自定义。当 Spring 框架获取到这个类时,它将调用这个类的 `configure()` 方法来进行 Web 安全配置。在我的 `WebAppSecurityConfig` 类中,有两个 `configure()` 方法。第一个是安全配置,例如哪些页面可以匿名访问(无需登录),哪些页面只能安全访问(必须先登录才能访问)。它是这样的:

   @Override
   protected void configure(HttpSecurity http) throws Exception
   {
      http
      .authorizeRequests()
          .antMatchers("/assets/**", "/public/**").permitAll()
          .anyRequest().authenticated()
      .and()
      .formLogin()
          .loginPage("/login")
          .permitAll()
          .usernameParameter("username")
          .passwordParameter("userpass")
          .successHandler(new SavedRequestAwareAuthenticationSuccessHandler())
          .defaultSuccessUrl("/secure/index", true).failureUrl("/public/authFailed")
          .and()
      .logout().logoutSuccessUrl("/public/logout")
          .permitAll()
          .and()
      .exceptionHandling().accessDeniedHandler(accessDeniedHandler);
   }

这个方法看起来很吓人。而且它确实很吓人。如果我设置不当,以后会后悔的。我将逐个部分讨论。第一个是这个:

...
http
.authorizeRequests()
    .antMatchers("/assets/**", "/public/**").permitAll()
    .anyRequest().authenticated()
...

这部分很容易,我希望所有传入的请求(除了那些包含“*.../assets/...*”和“*.../public/...*”的部分)在访问之前都经过身份验证。对于包含这些子路径的请求,将公开可访问。下一部分是登录页面的配置,以及登录成功和失败后做什么:

...
.formLogin()
    .loginPage("/login")
    .permitAll()
    .usernameParameter("username")
    .passwordParameter("userpass")
    .successHandler(new SavedRequestAwareAuthenticationSuccessHandler())
    .defaultSuccessUrl("/secure/index", true).failureUrl("/public/authFailed")
...

我将登录页面 URL 路径设置为“<应用程序基础 URL>/login”。此页面也被设置为公开可访问。如果我不这样做(将其设置为公开可访问),则需要身份验证才能访问它,这会产生鸡生蛋还是蛋生鸡的悖论。实际上,没有悖论。如果我不将登录页面设置为公开可访问,用户将无法登录,也无法看到安全页面。

`usernameParameter()` 和 `passwordParameter()` 方法设置了登录表单中可提供用户名和密码的输入字段。如果您不使用这两个方法来设置字段名,则会有默认字段名。我认为它们是“username”和“password”。对于我的应用程序,我将它们设置为“`username`”和“`userpass`”。接下来,我使用 `successHandler()` 方法来设置在有预期子 URL 的情况下重定向到该 URL 的操作。例如,如果我想访问

authenticationProvider:

https://:8080/webapp/secure/page1

并且我还没有登录,我将被首先导向登录页面。通过调用 `.successHandler(new SavedRequestAwareAuthenticationSuccessHandler())`,在我成功登录后,它会将我重定向回原始 URL。如果没有需要重定向回的 URL,我将使用下一个方法来设置用户成功登录时的默认着陆页 `.defaultSuccessUrl("/secure/index", true)`。如果用户登录失败,我将使用最后一个方法来设置登录失败页面 `.failureUrl("/public/authFailed")`。

下一部分是注销行为的配置:

...
logout().logoutSuccessUrl("/public/logout")
.permitAll()
...

所有这些都是为了指定用户注销时将重定向到哪里。注销目标页面将被设置为公开可访问。同样,如果页面不可公开访问,那么注销后,注销页面将无法访问,用户将收到 403 错误。

我想要做的最后一个配置是处理访问被拒绝错误。我会将这种类型的错误重定向到另一个页面。这是我的配置方式:

...
.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
...

`accessDeniedHandler` 对象是一个自定义对象。它在这个类的顶部定义如下:

@Autowired
private AccessDeniedHandler accessDeniedHandler;

定义 `accessDeniedHandler` 的类看起来像这样:

package org.hanbo.boot.app.config;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

// handle 403 page
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler
{
    @Override
    public void handle(HttpServletRequest httpServletRequest,
                       HttpServletResponse httpServletResponse,
                       AccessDeniedException e) throws IOException, ServletException
    {
        Authentication auth
           = SecurityContextHolder.getContext().getAuthentication();

        if (auth != null)
        {
            System.out.println("User '" + auth.getName()
                    + "' attempted to access the protected URL: "
                    + httpServletRequest.getRequestURI());
        }

        httpServletResponse.sendRedirect(httpServletRequest.getContextPath() + 
                                         "/public/accessDenied");
    }
}

这个处理器所做的就是截获发生的 `AccessDeniedException`,而不是显示 Spring Boot 错误页面,而是将响应重定向到我的访问被拒绝页面,它看起来像这样:

当处理器截获访问被拒绝异常时,它会在终端输出字符串:“用户 '<用户名>' 试图访问受保护的 URL:<用户试图访问的 URL>”。这是终端的截图:

最后,问题仍然存在。我如何授权用户?我所说的授权是指在用户身份得到确认后,应用程序将确定用户应获得何种访问权限。大多数情况下,我使用基于角色的授权。这对于我的业余项目来说已经足够了。如果需要不同类型的授权,基于权限的授权,Spring Security 提供了必要的机制。但这不会在本教程中涵盖。

在我的 `WebAppSecurityConfig` 类中,我添加了一个 `authenticationProvider` 对象并进行了配置,以确定用户的授权。这是声明和配置:

...

   @Autowired
   private UserAuthenticationService authenticationProvider;
   
...
   
   @Override
   protected void configure(AuthenticationManagerBuilder authMgrBuilder)
      throws Exception
   {
      authMgrBuilder.authenticationProvider(authenticationProvider);
   }
   
...

`UserAuthenticationService` 类定义如下:

package org.hanbo.boot.app.security;

import java.util.ArrayList;
import java.util.List;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

@Service
public class UserAuthenticationService
implements AuthenticationProvider
{
   @Override
   public Authentication authenticate(Authentication auth) throws AuthenticationException
   {      
      System.out.println("test");
      Authentication retVal = null;
      List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();
      
      if (auth != null)
      {
         String name = auth.getName();
         String password = auth.getCredentials().toString();
         System.out.println("name: " + name);
         System.out.println("password: " + password);
         
         if (name.equals("admin") && password.equals("admin12345"))
         {
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_STAFF"));
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
            
            retVal = new UsernamePasswordAuthenticationToken(
               name, "", grantedAuths
            );
            System.out.println("grant Admin");
         }
         else if (name.equals("staff1") && password.equals("staff12345"))
         {i
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_STAFF"));
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
            
            retVal = new UsernamePasswordAuthenticationToken(
               name, "", grantedAuths
            );
            System.out.println("grant Staff");
         }
         else if (name.equals("user1") && password.equals("user12345"))
         {
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
            
            retVal = new UsernamePasswordAuthenticationToken(
               name, "", grantedAuths
            );
            System.out.println("grant User");
         }
      }
      else
      {
         System.out.println("invalid login");
         retVal = new UsernamePasswordAuthenticationToken(
            null, null, grantedAuths
         );
         System.out.println("bad Login");
      }
      
      System.out.println("return login info");
      return retVal;
   }

   @Override
   public boolean supports(Class<?> tokenType)
   {
      return tokenType.equals(UsernamePasswordAuthenticationToken.class);
   }
}

从此类实例化的对象可用于验证用户名和密码。一旦确认,我会将用户角色分配给用户。管理员用户将拥有管理员、员工和用户角色。员工级别的用户将拥有员工和用户角色。用户级别的用户只能拥有用户角色。如果用户无法通过身份验证,则无法分配用户角色。

以上就是后端 Web 安全配置的全部内容。我只想再提一点。在我之前的教程中,我禁用了表单和 HTTP POST 操作的 CSRF。我认为这样做是错误的。它削弱了应用程序的安全性。在本教程中,我已经启用了 CSRF。在接下来的几节中,我将展示如何利用此功能。

将 Spring Security 与 Thymeleaf 集成

我写这篇教程的最大原因是我想知道如何使用 Thymeleaf 的安全标签。我想在我的下一个项目中使用 Thymeleaf,并且集成 Spring Security 对成功至关重要。在我能够有效地使用它之前,我想先测试一下。因此,本教程应运而生。结果发现,将 Spring Security 标签与 Thymeleaf 集成很容易。在 Maven POM 的部分,我提到需要 `thymeleaf-extras-springsecurity5` 依赖项。这个依赖项为页面提供了安全标签。

接下来,应该为 Thymeleaf 模板引擎设置文件夹结构。在项目文件夹 resources 下,我创建了一个名为“*templates*”的文件夹。这是存储页面模板和可重用片段的地方。对于这个项目,我设置了四个不同的页面:

  • 每个人都可以看到的页面,但不同角色的用户看到的页面部分不同。
  • 只有管理员用户可以访问的页面。
  • 只有员工级别和管理员用户可以看到的页面。
  • 管理员用户、员工级别用户和普通用户都可以看到的页面。但匿名用户看不到。

为了设置这些页面模板,以确保不同级别的用户可以访问页面,我需要在页面模板中使用 Spring Security 标签。让我向您展示安全索引页面的截图,所有用户都可以访问该页面,但不同的用户看到的页面有所不同:

此屏幕截图仅在用户是管理员用户时才可见。如果登录用户是非管理员级别的用户,例如,登录了一个非管理员、非员工级别的用户,将看到与此相同的页面:

这两个屏幕截图的区别是由 Spring Security 标签区分的。这是此页面模板的完整源代码:

<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <title>Login</title>
    <link rel="stylesheet" th:href="@{/assets/bootstrap/css/bootstrap.min.css}"/>
    <link rel="stylesheet" th:href="@{/assets/bootstrap/css/bootstrap-theme.min.css}"/>
    <link rel="stylesheet" th:href="@{/assets/css/index.css}"/>
</head>
<body>
   <div class="container">
      <div th:replace="parts/pieces::logoutForm">
      </div>
		<div th:replace="parts/pieces::header">
		</div>
      
      <div class="row">
         <div class="col-xs-12">
            <div class="panel panel-default">
               <div class="panel-body">
                  <h3>Index Page</h3>
                  <p>You can see this section as long as you are logged in.</p>
               </div>
            </div>
         </div>
      </div>
      
      <div class="row" sec:authorize="hasRole('USER')">
         <div class="col-xs-12">
            <div class="panel panel-default">
               <div class="panel-body">
                  <h3>Only User can See This</h3>
                  <p>If you have the "ROLE_USER". 
                  You will be able to see this section. User Section.</p>
               </div>
            </div>
         </div>
      </div>

      <div class="row" sec:authorize="hasRole('STAFF')">
         <div class="col-xs-12">
            <div class="panel panel-default">
               <div class="panel-body">
                  <h3>Only Staff can See This</h3>
                  <p>If you have the "ROLE_STAFF". 
                  You will be able to see this section. Staff Section.</p>
               </div>
            </div>
         </div>
      </div>
      
      <div class="row" sec:authorize="hasRole('ADMIN')">
         <div class="col-xs-12">
            <div class="panel panel-default">
               <div class="panel-body">
                  <h3>Only Admin can See This</h3>
                  <p>If you have the "ROLE_ADMIN". 
                  You will be able to see this section. Admin Section.</p>
               </div>
            </div>
         </div>
      </div>

   </div>
   
   <script type="text/javascript" th:src="@{/assets/jquery/js/jquery.min.js}"></script>
   <script type="text/javascript" th:src="@{/assets/bootstrap/js/bootstrap.min.js}"></script>
</body>
</html>

这个页面的大部分是三个部分,它们只根据用户的安全角色显示。部分显示被这样包裹起来:

<div class="row" sec:authorize="hasRole('USER')">
...
</div>

<div class="row" sec:authorize="hasRole('STAFF')">
...
</div>

<div class="row" sec:authorize="hasRole('ADMIN')">
...
</div>

`sec:authorize="hasRole('ADMIN')"` 属性的使用就是 Spring Security 标签的使用。分配的表达式(使用 SpEL 编写)非常容易理解,当用户拥有由该表达式定义的角色时,授权将为 true 以显示该部分。这与 Spring Security 使用 taglib 的方式完全相同。令人惊讶!

这是页面标记中一个有趣的部分。另一个是向页面模板添加片段。这些片段也可以使用 Spring Security 标签。在上面的页面标记的顶部,您会看到这个:

<div th:replace="parts/pieces::logoutForm">
</div>
   <div th:replace="parts/pieces::header">
   </div>

这些是我从另一个文件插入片段的地方。第一个 `

` 将添加一个新表单。它用于注销用户。另一个 `
` 是将导航菜单插入到页面顶部。暂时忽略表单,我稍后会解释。使用片段添加导航菜单是最常见的操作之一,因为导航菜单可以在多个页面上共享,最好只创建一次,并在所有需要的地方使用。

我定义的片段文件名为“*pieces.html*”。它存储在子文件夹“*parts*”中。文件定义如下:

<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
   <head>
      <meta charset="UTF-8"/>
      <title>Spring Boot Thymeleaf Application - Fragments</title>
   </head>
   <body>
      <div th:fragment="logoutForm">
         <form id="logoutForm" th:action="@{/logout}" 
          method="post" th:hidden="true" name="logoutForm">
            <input type="submit" value="Logout" />
         </form>
      </div>
      <div th:fragment="header">
         <div class="row">
            <div class="col-xs-12 text-right">
               <a href="/secure/adminPage">Admin Page</a>
               <a href="/secure/staffPage">Staff Page</a>
               <a href="/secure/userPage">User Page</a>
               <a href="javascript: document.logoutForm.submit();" 
                sec:authorize="isAuthenticated()">Logout</a>
            </div>
         </div>
      </div>
      <div th:fragment="logout_header">
         <div class="row">
            <div class="col-xs-12 text-right">
               <a href="javascript: document.logoutForm.submit();" 
                sec:authorize="isAuthenticated()">Logout</a>
            </div>
         </div>
      </div>
   </body>
</html>

在这个文件中,我创建了三个片段。第一个是注销用户的表单。同样,请暂时忽略这一点,我将在下一节中解释它的作用。另外两个是简单的导航菜单。一个有四个链接。另一个只有一个链接。我想展示的是我使用的另一个 Spring Security 标签,并且经常使用:`sec:authorize="isAuthenticated()"`。此标签用于在不考虑用户角色时渲染或不渲染元素,只要用户已登录,此标签将始终返回 `true`,并允许渲染标记的元素。

让我们来看看渲染这些页面的 `controller` 类:

package org.hanbo.boot.app.controllers;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;"

@Controller
public class SecuredPageController
{
   @PreAuthorize("hasRole('USER')")
   @RequestMapping(value="/secure/index", method = RequestMethod.GET)
   public ModelAndView index1()
   {
        ModelAndView retVal = new ModelAndView();
        retVal.setViewName("indexPage");
        return retVal;
   }
   
   @PreAuthorize("hasRole('ADMIN')")
   @RequestMapping(value="/secure/adminPage", method = RequestMethod.GET)
   public ModelAndView adminPage()
   {
        ModelAn      <div th:replace="parts/pieces::logoutForm">
      </div>dView retVal = new ModelAndView();
        retVal.setViewName("AdminPage");
        return retVal;
   }
   
   @PreAuthorize("hasRole('STAFF')")
   @RequestMapping(value="/secure/staffPage", method = RequestMethod.GET)
   public ModelAndView staffPage()
   {
        ModelAndView retVal = new ModelAndView();
        retVal.setViewName("StaffPage");
        return retVal;
   }
   
   @PreAuthorize("hasRole('USER')")
   @RequestMapping(value="/secure/userPage", method = RequestMethod.GET)
   public ModelAndView userPage()
   {
        ModelAndView retVal = new ModelAndView();
        retVal.setViewName("UserPage");
        return retVal;
   }
}

处理用户请求的这些方法都使用了 `@PreAuthorize("hasRole('<用户角色名称>')")` 注解。这意味着,必须经过身份验证并具有关联用户角色才能由后端代码处理请求。您是否注意到在我的 `UserAuthenticationService` 类中,分配给用户的角色都带有“`ROLE_`”前缀?在 Spring Security 5 及更高版本中,在 SpEL `hasRole()` 表达式中不再需要“`ROLE_`”前缀。这是我想要揭示的最后一个要点。

最后,我想谈谈 CSRF 令牌和注销。

CSRF 令牌和注销操作

CSRF 是一种攻击类型。您可以在线搜索并阅读有关它的所有信息。为了防止这种情况,Spring Security 提供了 CSRF 令牌作为一种缓解机制。也就是说,每次使用页面模板引擎(如 Thymeleaf)渲染包含表单的页面时,后端 Spring MVC/Spring Security 都会创建一个 CSRF 令牌,并将其作为隐藏字段附加到表单中。当表单提交时,CSRF 令牌将被发送回后端,然后表单将被成功处理。令牌充当前端和后端之间的握手信号。

我相信从 Spring Security 5 开始,在启用 CSRF 令牌的情况下,注销只能通过 HTTP `POST` 完成,而不是 HTTP `GET`。这有点棘手,但我通过在线搜索找到了答案。在线提出的解决方案是创建一个隐藏表单。它只有一个提交按钮,该按钮调用 HTTP `POST` 进行注销。

由于注销表单在所有四个页面上都是通用的。我将其定义为片段:

<div th:fragment="logoutForm">
    <form id="logoutForm" th:action="@{/logout}" 
     method="post" th:hidden="true" name="logoutForm">
    <input type="submit" value="Logout" />
    </form>
</div>

使用以下方式将其添加到目标页面:

<div th:replace="parts/pieces::logoutForm">
</div>

问题是,我如何调用这个表单?这可以通过一行 JavaScript 代码完成。这是:

<a href="javascript: document.logoutForm.submit();" sec:authorize="isAuthenticated()">Logout</a>

在这里,我调用 `document.logoutForm.submit();`。这将调用表单并执行注销。这是一种非常基础的执行注销的方式。但对于这个简单的教程来说,足够了。问题是,如果我用 AngularJS 或其他 Web 应用程序框架替换它,用一个单页 Web 应用程序怎么办?对此还没有一个很好的答案。

无论如何,POST 操作没有被我的控制器方法处理。Spring Security 内部提供了处理方法。但是,如果注销操作成功,它会将页面重定向到 */public/logout*。这个 URL 由我的控制器方法处理,它看起来像这样:

@RequestMapping(value="/public/logout", method = RequestMethod.GET)
public ModelAndView logout()
{
   ModelAndView retVal = new ModelAndView();
   retVal.setViewName("logoutPage");
   return retVal;
}

至此,关于这个 Web 应用程序的所有内容都已讨论完毕。接下来,我将讨论如何测试它。

如何测试

下载完整源代码并解压后,请将所有名为 `*.sj` 的文件重命名回 `*.js` 文件。

完成后,请确保已安装 JDK 14 或更高版本。此 Web 应用程序已设置为使用 JDK 14 或更高版本进行编译和打包。使用以下命令编译和打包应用程序:

mvn clean install

当构建成功完成时,运行以下命令来启动应用程序:

java -jar target/thymeleaf-security-sample-1.0.0.jar

当命令运行时,它会输出大量日志消息。当您确认应用程序已成功启动后,可以通过导航到以下地址来测试应用程序:

https://:8080

请注意,安全会话可能无法建立。在这种情况下,您可以使用 HTTPS 而不是 HTTP。需要进行一些研究,但这非常容易做到。祝你好运。

摘要

本教程文章讨论了 Thymeleaf 模板引擎和 Spring Security 的集成。示例应用程序使用标准的基于表单的身份验证和基于角色的授权。我为自己写了这篇教程,以便在需要设置类似机制的项目时可以参考。与我之前的一些教程不同,我专注于此示例应用程序最重要的方面。其中一些内容在我之前的教程中已经讨论过。它们很重要。此外,它们对于 Spring Security 5 及更高版本来说是新的。

在我写这篇教程的时候,我想到了用 OAuth 来增强它,也就是说,使用 Google 身份验证来验证用户登录。那将是一篇很好的教程。当我研究这个问题时,它似乎很容易解决。棘手的部分可能是授权部分。此外,我不知道 CSRF 令牌是否也需要。我不知道。我遇到的另一个问题是,我需要做什么才能将 CSRF 令牌添加到 AngularJS 应用程序中。必须有一种方法来获取令牌,然后使用 HTTP `POST` 方法与后端进行交互。但是这种类型的示例应用程序的设置可能会很繁琐。所以请继续关注。

除了这两个想法之外,我还有一个排队的教程。JavaScript 使用 ES6 语法用于 AngularJS 应用程序。这将是今年的下一个教程。我今年有很多想法。这将是一个精彩的教程,请继续关注。祝您运行示例应用程序顺利。

历史

  • 2021 年 2 月 15 日:初始版本
© . All rights reserved.