将 ThymeLeaf 页面模板引擎与 Spring Security 结合使用
本教程介绍如何设置 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 日:初始版本