Spring Security - 快速指南
Spring Security - 使用数据库表单登录
内容
- 简介和概述
- Spring Security 的基本组件
- 认证过滤器
- 认证管理器
- 认证提供者
- 用户详情服务
- 密码编码器
- Spring安全上下文
- 表单登录
- 使用数据库登录
- 登录尝试限制
- 入门(实用指南)
简介和概述
除了提供各种内置的身份验证和授权选项之外,Spring Security 还允许我们根据需要自定义身份验证过程。从自定义登录页面到我们自己的自定义身份验证提供程序和身份验证过滤器,我们几乎可以自定义身份验证过程的各个方面。我们可以定义自己的身份验证过程,范围从使用用户名和密码的基本身份验证到复杂的身份验证(例如使用令牌和 OTP 的双因素身份验证)。此外,我们可以使用各种数据库 - 关系数据库和非关系数据库,使用各种密码编码器,将恶意用户锁定在其帐户之外,等等。
今天,我们将讨论三种此类自定义,即自定义表单登录、数据库提供的身份验证以及限制登录尝试。尽管这些都是非常基本的用例,但它们仍然可以让我们更仔细地了解 Spring Security 的身份验证和授权过程。我们还将建立一个注册页面,用户可以通过该页面在我们的应用程序中进行注册。
首先我们看一下Spring Security的架构。它从 servlet 过滤器开始。这些过滤器拦截请求,对其执行操作,然后将请求传递到过滤器链中的下一个过滤器或请求处理程序,或者在不满足某些条件时阻止它们。正是在这个过程中,Spring Security 可以对请求进行身份验证并对请求执行各种身份验证检查。它还可以通过不允许未经身份验证或恶意请求访问我们受保护的资源来阻止它们通过。因此我们的应用程序和资源受到保护。
Spring Security 架构的组件
正如我们在上图中看到的那样,Spring Security 的基本组件如下所示。我们将在讨论过程中简要讨论它们。我们还将讨论它们在身份验证和授权过程中的角色。
认证过滤器
这是拦截请求并尝试对其进行身份验证的过滤器。在 Spring Security 中,它将请求转换为身份验证对象并将身份验证委托给 AuthenticationManager。
认证管理器
它是身份验证的主要策略接口。它使用单独的方法authenticate()来验证请求。authenticate() 方法执行身份验证,并在身份验证成功时返回 Authentication 对象,或者在身份验证失败时抛出 AuthenticationException。如果该方法无法决定,它将返回 null。这个过程中的认证过程委托给了我们接下来要讨论的AuthenticationProvider。
认证提供者
AuthenticationManager 由 ProviderManager 实现,后者将流程委托给一个或多个 AuthenticationProvider 实例。任何实现 AuthenticationProvider 接口的类都必须实现两个方法——authenticate() 和supports()。首先,我们来谈谈supports()方法。它用于检查我们的 AuthenticationProvider 实现类是否支持特定的身份验证类型。如果支持则返回 true,否则返回 false。接下来是authenticate() 方法。这是身份验证发生的地方。如果支持该认证类型,则启动认证过程。这里这个类可以使用UserDetailsService实现的 loadUserByUsername() 方法。如果未找到用户,则会抛出 UsernameNotFoundException。
另一方面,如果找到用户,则使用该用户的身份验证详细信息来验证该用户。例如,在基本认证场景中,可以将用户提供的密码与数据库中的密码进行核对。如果发现它们彼此匹配,则说明成功。然后我们可以从此方法返回一个 Authentication 对象,该对象将存储在安全上下文中,我们将在稍后讨论。
用户详情服务
它是Spring Security的核心接口之一。任何请求的身份验证主要取决于 UserDetailsService 接口的实现。它最常用于数据库支持的身份验证中以检索用户数据。通过单独的 loadUserByUsername() 方法的实现来检索数据,我们可以在其中提供逻辑来获取用户的用户详细信息。如果未找到用户,该方法将抛出 UsernameNotFoundException。
密码编码器
在 Spring Security 4 之前,PasswordEncoder 的使用是可选的。用户可以使用内存中身份验证来存储纯文本密码。但Spring Security 5 强制使用PasswordEncoder 来存储密码。这使用其多种实现之一对用户的密码进行编码。最常见的实现是 BCryptPasswordEncoder。此外,我们还可以使用 NoOpPasswordEncoder 的实例来进行开发。它将允许密码以纯文本形式存储。但它不应该用于生产或现实世界的应用程序。
Spring安全上下文
这是在成功验证后存储当前已验证用户的详细信息的位置。然后,身份验证对象在会话的整个应用程序中可用。因此,如果我们需要用户名或任何其他用户详细信息,我们需要首先获取 SecurityContext。这是通过 SecurityContextHolder(一个帮助程序类)完成的,它提供对安全上下文的访问。我们可以使用 setAuthentication() 和 getAuthentication() 方法分别存储和检索用户详细信息。
继续,现在让我们讨论将用于我们的应用程序的三个自定义实现。
表单登录
当我们将 Spring Security 添加到现有 Spring 应用程序时,它会添加一个登录表单并设置一个虚拟用户。这是自动配置模式下的 Spring Security。在此模式下,它还设置默认过滤器、身份验证管理器、身份验证提供程序等。此设置是内存中身份验证设置。我们可以覆盖此自动配置来设置我们自己的用户和身份验证过程。我们还可以设置自定义登录方法,例如自定义登录表单。Spring Security 只需要了解登录表单的详细信息,例如登录表单的 URI、登录处理 URL 等。然后它将为应用程序呈现我们的登录表单并执行身份验证过程其他提供的配置或Spring自己的实现。
此自定义表单设置只需遵守某些规则即可与 Spring Security 集成。我们需要有一个用户名参数和一个密码参数,参数名称应该是“用户名”和“密码”,因为这些是默认名称。如果我们在自定义中对这些字段使用我们自己的参数名称,我们必须使用 usernameParameter() 和 passwordParameter() 方法通知 Spring Security 这些更改。同样,对于我们对登录表单或表单登录方法所做的每一次更改,我们都必须使用适当的方法通知 Spring Security 这些更改,以便它可以将它们与身份验证过程集成。
使用数据库登录
正如我们所讨论的,Spring Security 默认情况下自动提供内存中身份验证实现。我们可以通过验证其详细信息存储在数据库中的用户来覆盖这一点。在这种情况下,在对用户进行身份验证时,我们可以根据数据库中的凭据验证用户提供的凭据以进行身份验证。我们还可以让新用户在我们的应用程序中注册并将他们的凭据存储在同一数据库中。此外,我们还可以提供更改或更新其密码、角色或其他数据的方法。因此,这为我们提供了可以使用更长时间的持久用户数据。
登录尝试限制
为了限制应用程序中的登录尝试,我们可以使用 Spring Security 的 isAccountNonLocked 属性。Spring Security 的 UserDetails 为我们提供了该属性。我们可以设置一种身份验证方法,如果任何用户或其他人提供不正确的凭据超过一定次数,我们可以锁定他们的帐户。即使用户提供了正确的凭据,Spring Security 也会禁用锁定用户的身份验证。这是 Spring Security 提供的内置功能。我们可以将错误登录尝试的次数存储在数据库中。然后,针对每次错误的身份验证尝试,我们可以更新并检查数据库表。当此类尝试的次数超过给定数量时,我们可以将用户锁定在其帐户之外。因此,在帐户解锁之前,用户将无法再次登录。
入门(实用指南)
现在让我们开始我们的应用程序。下面列出了此应用程序所需的工具 -
Java IDE - 最好是 STS 4,但 Eclipse、IntelliJ Idea 或任何其他 IDE 都可以。
MySql Server Community Edition - 我们需要在我们的系统中下载并安装 MySql Community Server。我们可以点击这里进入官方网站。
MySql Workbench - 它是一个 GUI 工具,我们可以用来与 MySql 数据库交互。
数据库设置
我们首先设置数据库。我们将为此应用程序使用 MySql 数据库实例。MySql Server 社区版可供免费下载和使用。我们将使用MySql Workbench与 MySql 服务器连接,并创建一个名为“spring”的数据库以与我们的应用程序一起使用。
然后我们将创建两个表 - 用户和尝试 - 来保存我们的用户和登录尝试。如前所述,注册我们的应用程序的用户的详细信息将存储在 users 表中。任何用户的登录尝试次数将根据其用户名存储在 attempts 表中。这样我们就可以跟踪尝试并采取必要的行动。
让我们看一下设置用户表和尝试表的 SQL。
CREATE TABLE users ( username VARCHAR(45) NOT NULL , password VARCHAR(45) NOT NULL , account_non_locked TINYINT NOT NULL DEFAULT 1 , PRIMARY KEY (username) ); CREATE TABLE attempts ( id int(45) NOT NULL AUTO_INCREMENT, username varchar(45) NOT NULL, attempts varchar(45) NOT NULL, PRIMARY KEY (id) );
我们现在可以向我们的应用程序添加一个虚拟用户。
INSERT INTO users(username,password,account_non_locked) VALUES ('user','12345', true);
项目设置
像往常一样,我们将使用 Spring 初始化程序来设置我们的项目。我们将使用 Spring Boot 版本 2.3.2 创建一个 Maven 项目。让我们将项目命名为 formlogin(我们可以选择任何我们想要的名称)并将组 id 命名为 com.tutorial.spring.security。此外,我们将在该项目中使用 Java 版本 8。
依赖关系
现在,谈到依赖项,我们将在此演示中使应用程序尽可能简单。我们将继续关注今天要探索的功能。因此,我们将选择最少数量的依赖项,这将有助于我们设置应用程序并快速启动和运行。让我们看一下依赖关系 -
Spring Web - 它捆绑了与 Web 开发相关的所有依赖项,包括 Spring MVC、REST 和嵌入式 Tomcat 服务器。
Spring Security - 用于实现 Spring Security 提供的安全功能。
Thymeleaf - 用于 HTML5/XHTML/XML 的服务器端 Java 模板引擎。
Spring Data JPA - 除了使用 JPA 规范定义的所有功能之外,Spring Data JPA 还添加了自己的功能,例如存储库模式的无代码实现以及从方法名称创建数据库查询。
Mysql 驱动程序- 用于 MySQL 数据库驱动程序。
有了这五个依赖项,我们现在就可以设置我们的项目了。让我们点击生成按钮。这会将我们的项目下载为 zip 文件。我们可以将其解压到我们选择的文件夹中。然后我们在 IDE 中打开该项目。为此,我们将使用 Spring Tool Suite 4。例子。
让我们将项目加载到 STS 中。我们的 IDE 需要一些时间来下载依赖项并验证它们。让我们看一下 pom.xml 文件。
pom.xml
<?xml version="1.0" encoding="ISO-8859-1"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.tutorial.spring.security</groupId> <artifactId>formlogin</artifactId> <version>0.0.1-SNAPSHOT</version> <name>formlogin</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</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-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime<scope> <optional>true</optional> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test<artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
我们可以看到我们的项目详细信息以及我们的依赖项都列在这里。
数据源
我们将在 application.properties 文件中配置数据源。由于我们将使用本地 MySQL 数据库作为数据源,因此我们在此处提供本地数据库实例的 URL、用户名和密码。我们将我们的数据库命名为“spring”。
spring.datasource.url=jdbc:mysql://localhost:3306/spring spring.datasource.username=root spring.datasource.password=root
实体
现在让我们创建我们的实体。我们从 User 实体开始,它包含三个字段 - 用户名、密码和 accountNonLocked。该 User 类还实现了 Spring Security 的 UserDetails 接口。此类提供核心用户信息。它用于存储用户数据,稍后可以将其封装到 Authentication 对象中。不建议直接实现接口。但对于我们的例子,由于这是一个简单的应用程序来演示数据库登录,因此我们直接在这里实现了这个接口以保持简单。我们可以通过在 User 实体周围使用包装类来实现此接口。
用户.java
package com.tutorial.spring.security.formlogin.model; import java.util.Collection; import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @Entity @Table(name = "users") public class User implements UserDetails { /** * */ private static final long serialVersionUID = 1L; @Id private String username; private String password; @Column(name = "account_non_locked") private boolean accountNonLocked; public User() { } public User(String username, String password, boolean accountNonLocked) { this.username = username; this.password = password; this.accountNonLocked = accountNonLocked; } @Override public Collection< extends GrantedAuthority> getAuthorities() { return List.of(() -> "read"); } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } public void setAccountNonLocked(Boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; } public boolean getAccountNonLocked() { return accountNonLocked; } }
这里要注意accountNonLocked字段。Spring Security 中的每个用户的帐户默认都是解锁的。为了覆盖该属性并在用户超过允许的尝试次数后将用户锁定在其帐户之外,我们将使用该属性。如果用户超过允许的无效尝试次数,我们将使用此属性将他锁定在帐户之外。此外,在每次身份验证尝试期间,我们将使用 isAccountNonLocked() 方法检查此属性以及凭据以对用户进行身份验证。任何帐户被锁定的用户都将不允许通过身份验证进入应用程序。
对于 UserDetails 接口的其他方法,我们现在可以简单地提供一个返回 true 的实现,因为我们不会为此应用程序探索这些属性。
对于该用户的权限列表,我们现在为他分配一个虚拟角色。我们也不会将此属性用于此应用程序。
尝试.java
继续,让我们创建 Attempts 实体来保存无效尝试计数。在数据库中创建时,我们将在此处包含三个字段 - 用户名、一个名为 attempts 的整数(用于记录尝试次数)和一个标识符。
package com.tutorial.spring.security.formlogin.model; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class Attempts { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String username; private int attempts; /** * @return the id */ public int getId() { return id; } /** * @param id the id to set */ public void setId(int id) { this.id = id; } /** * @return the username */ public String getUsername() { return username; } /** * @param username the username to set */ public void setUsername(String username) { this.username = username; } /** * @return the attempts */ public int getAttempts() { return attempts; } /** * @param attempts the attempts to set */ public void setAttempts(int attempts) { this.attempts = attempts; } }
存储库
我们已经创建了实体,让我们创建存储库来存储和检索数据。我们将有两个存储库,每个实体类一个。对于这两个存储库接口,我们将扩展 JpaRepository,它为我们提供了内置实现,用于保存和检索 application.properties 文件中配置的数据库中的数据。除了提供的方法或查询之外,我们还可以在此处添加我们的方法或查询。
用户存储库.java
package com.tutorial.spring.security.formlogin.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.tutorial.spring.security.formlogin.model.User; @Repository public interface UserRepository extends JpaRepository<User, String> { Optional<User> findUserByUsername(String username); }
正如所讨论的,我们在此处添加了通过用户名检索用户的方法。这将返回我们的用户详细信息,包括用户名、密码和帐户锁定状态。
AttemptsRepository.java
package com.tutorial.spring.security.formlogin.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.tutorial.spring.security.formlogin.model.Attempts; @Repository public interface AttemptsRepository extends JpaRepository<Attempts, Integer> { Optional<Attempts> findAttemptsByUsername(String username); }
类似地,对于 Attempts,在 AttemptsRepository 中,我们添加了一个自定义方法 findAttemptsByUsername(String username) 来获取有关使用用户名的用户尝试的数据。这将返回一个 Attempts 对象,其中包含用户名和用户尝试身份验证失败的次数。
配置
由于我们将使用自定义登录表单,因此我们必须覆盖 Spring Security 的默认配置。为此,我们创建配置类,该类扩展了 Spring Security 的 WebSecurityConfigurerAdapter 类。
package com.tutorial.spring.security.formlogin.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class ApplicationConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests().antMatchers("/register**") .permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .invalidateHttpSession(true) .clearAuthentication(true) .permitAll(); } }
在这里我们做了两件事 -
- 首先,我们指定了要使用的PasswordEncoder 接口的实现。我们使用 BCryptPasswordEncoder 的实例来对本示例的密码进行编码。PasswordEncoder 接口有很多实现,我们可以使用其中任何一个。我们在这里选择 BCryptPasswordEncoder 作为最常用的实现。它使用非常强大的 BCrypt 哈希算法对密码进行编码。它通过加入盐来防止彩虹表攻击来实现这一点。除此之外,bcrypt 是一个自适应函数:随着时间的推移,迭代次数可以增加以使其变慢,因此即使计算能力不断增加,它仍然可以抵抗暴力搜索攻击。
- 其次,我们重写了configure()方法来提供登录方法的实现。
- 每当我们使用自定义表单代替 Spring Security 提供的表单进行身份验证时,我们都必须使用 formLogin() 方法通知 Spring Security。
- 然后我们还指定登录 URL – /login。稍后我们会将 URL 映射到控制器中的自定义登录页面。
- 我们还指定以 /register、/login 开头的端点和注销页面不需要受到保护。我们使用 PermitAll() 方法来做到这一点。这允许每个人访问这些端点。除了这些端点之外,所有端点都需要进行身份验证()。也就是说,用户必须登录才能访问所有其他端点。
- 注销时,我们指定会话将失效,并清除存储在应用程序 SecurityContext 中的身份验证。
安全设置
现在,我们将设置身份验证过程。我们将使用数据库设置身份验证并锁定用户帐户。
让我们首先创建 UserDetailsService 的实现。正如我们之前讨论的,我们需要提供使用数据库进行身份验证的自定义实现。这是因为,正如我们所知,Spring Security 默认情况下仅提供内存中的身份验证实现。因此,我们需要使用基于数据库的流程来覆盖该实现。为此,我们需要重写 UserDetailsService 的 loadUserByUsername() 方法。
用户详情服务
package com.tutorial.spring.security.formlogin.security; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.provisioning.UserDetailsManager; import org.springframework.stereotype.Service; import com.tutorial.spring.security.formlogin.model.User; import com.tutorial.spring.security.formlogin.repository.UserRepository; @Service public class SecurityUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findUserByUsername(username) .orElseThrow(() -< new UsernameNotFoundException("User not present")); return user; } public void createUser(UserDetails user) { userRepository.save((User) user); } }
正如我们在这里看到的,我们在这里实现了 loadUserByUsername() 方法。在这里,我们使用 UserRepository 接口从数据库中获取用户。如果未找到用户,则会抛出 UsernameNotFoundException。
我们还有一个 createUser() 方法。我们将使用此方法将已使用 UserRepository 在我们的应用程序中注册的用户添加到我们的数据库中。
认证提供者
我们现在将实现我们的自定义身份验证提供程序。它将实现 AuthenticationProvider 接口。我们这里有两个必须重写和实现的方法。
package com.tutorial.spring.security.formlogin.security; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import com.tutorial.spring.security.formlogin.model.Attempts; import com.tutorial.spring.security.formlogin.model.User; import com.tutorial.spring.security.formlogin.repository.AttemptsRepository; import com.tutorial.spring.security.formlogin.repository.UserRepository; @Component public class AuthProvider implements AuthenticationProvider { private static final int ATTEMPTS_LIMIT = 3; @Autowired private SecurityUserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Autowired private AttemptsRepository attemptsRepository; @Autowired private UserRepository userRepository; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); import com.tutorial.spring.security.formlogin.repository.UserRepository; @Component public class AuthProvider implements AuthenticationProvider { private static final int ATTEMPTS_LIMIT = 3; @Autowired private SecurityUserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Autowired private AttemptsRepository attemptsRepository; @Autowired private UserRepository userRepository; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); Optional<Attempts> userAttempts = attemptsRepository.findAttemptsByUsername(username); if (userAttempts.isPresent()) { Attempts attempts = userAttempts.get(); attempts.setAttempts(0); attemptsRepository.save(attempts); } } private void processFailedAttempts(String username, User user) { Optional<Attempts> userAttempts = attemptsRepository.findAttemptsByUsername(username); if (userAttempts.isEmpty()) { Attempts attempts = new Attempts(); attempts.setUsername(username); attempts.setAttempts(1); attemptsRepository.save(attempts); } else { Attempts attempts = userAttempts.get(); attempts.setAttempts(attempts.getAttempts() + 1); attemptsRepository.save(attempts); if (attempts.getAttempts() + 1 > ATTEMPTS_LIMIT) { user.setAccountNonLocked(false); userRepository.save(user); throw new LockedException("Too many invalid attempts. Account is locked!!"); } } } @Override public boolean supports(Class<?> authentication) { return true; } }
- authenticate() - 此方法返回一个经过完全身份验证的对象,包括成功身份验证时的凭据。然后将该对象存储在 SecurityContext 中。为了执行身份验证,我们将使用应用程序的 SecurityUserDetailsService 类的 loaduserByUsername() 方法。在这里我们执行多项操作 -
- 首先,我们从身份验证请求对象中提取用户凭据,该对象作为参数传递给我们的函数。该身份验证对象由 AuthenticationFilter 类准备,并通过 AuthenticationManager 向下传递到 AuthenticationProvider。
- 我们还使用 loadUserByUsername() 方法从数据库中获取用户详细信息。
- 现在,首先,我们检查用户帐户是否由于之前失败的身份验证尝试而被锁定。如果我们发现账户被锁定,我们会抛出LockedException,用户将无法进行身份验证,除非账户再次解锁。
- 如果帐户未锁定,我们会将提供的密码与数据库中针对该用户存储的密码进行匹配。这是使用 PasswordEncoder 接口的 matches() 方法完成的。
- 如果密码匹配,并且帐户尚未被锁定,我们将返回一个经过完全身份验证的对象。这里我们使用了一个实例 UsernamePasswordAuthenticationToken 类(因为它是用户名密码身份验证)来实现身份验证。同时,我们还将尝试计数器重置为 0。
- 另一方面,如果密码不匹配,我们会检查一些条件 -
- 如果这是用户第一次尝试,那么他的名字可能不会出现在数据库中。我们使用 AttemptsRepository 中的 findAttemptsByUsername() 方法来检查这一点。
- 如果未找到,我们会在数据库中为该用户创建一个条目,并将尝试次数设置为 1。
- 如果找到用户,那么我们将尝试次数增加 1。
- 然后,我们使用之前定义的常量值检查允许的最大失败尝试次数。
- 如果该次数超过允许的尝试次数,则用户将被锁定应用程序并引发 LockedException。
- supports() - 我们还有supports方法来检查我们的AuthenticationProvider实现类是否支持我们的身份验证类型。如果匹配、不匹配或无法决定,则分别返回 true、false 或 null。目前我们已将其硬编码为 true。
控制器
现在让我们创建控制器包。它将包含我们的 HelloController 类。使用这个控制器类,我们将把视图映射到端点,并在命中相应的端点时提供这些视图。我们还将自动装配该组件中的PasswordEncoder 和UserDetailsService 类。这些注入的依赖项将用于创建我们的用户。现在让我们创建端点。
package com.tutorial.spring.security.formlogin.controller; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import com.tutorial.spring.security.formlogin.model.User; import com.tutorial.spring.security.formlogin.security.SecurityUserDetailsService; @Controller public class HelloController { @Autowired private SecurityUserDetailsService userDetailsManager; @Autowired private PasswordEncoder passwordEncoder; @GetMapping("/") public String index() { return "index"; } @GetMapping("/login") public String login(HttpServletRequest request, HttpSession session) { session.setAttribute( "error", getErrorMessage(request, "SPRING_SECURITY_LAST_EXCEPTION") ); return "login"; } @GetMapping("/register") public String register() { return "register"; } @PostMapping( value = "/register", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = { MediaType.APPLICATION_ATOM_XML_VALUE, MediaType.APPLICATION_JSON_VALUE } ) public void addUser(@RequestParam Map<String, String> body) { User user = new User(); user.setUsername(body.get("username")); user.setPassword(passwordEncoder.encode(body.get("password"))); user.setAccountNonLocked(true); userDetailsManager.createUser(user); } private String getErrorMessage(HttpServletRequest request, String key) { Exception exception = (Exception) request.getSession().getAttribute(key); String error = ""; if (exception instanceof BadCredentialsException) { error = "Invalid username and password!"; } else if (exception instanceof LockedException) { error = exception.getMessage(); } else { error = "Invalid username and password!"; } return error; } }
- 索引(“/”) – 该端点将为我们的应用程序的索引页面提供服务。正如我们之前配置的那样,我们将保护此页面并仅允许经过身份验证的用户能够访问此页面。
- login ("/login") – 如前所述,这将用于服务我们的自定义登录页面。任何未经身份验证的用户都将被重定向到此端点进行身份验证。
- register("/register") (GET) – 我们的应用程序将有两个“注册”端点。其中之一是提供注册页面。另一项任务是处理注册过程。因此,前者将使用 Http GET,后者将使用 POST 端点。
- register("/register") (POST) – 我们将使用此端点来处理用户注册过程。我们将从参数中获取用户名和密码。然后我们将使用@Autowired 到该组件中的passwordEncoder 对密码进行编码。此时我们还将用户帐户设置为解锁。然后,我们将使用 createUser() 方法将此用户数据保存在用户表中。
除了上面的方法之外,我们还有 getErrorMessage() 方法。它用于确定最后抛出的异常以在我们的登录模板中添加消息。这样,我们就可以意识到身份验证错误并显示正确的消息。
资源
我们已经创建了端点,唯一剩下的就是创建视图。
首先,我们将创建索引页面。只有成功验证后,用户才能访问此页面。该页面可以访问 Servlet 请求对象,使用该对象我们可以显示登录用户的用户名。
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <title> Hello World! </title> </head> <body> <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1> <form th:action="@{/logout}" method="post"> <input type="submit" value="Sign Out"/> </form> </body> <html>
接下来,我们创建登录视图。这将显示我们的自定义登录表单,其中包含用户名和密码字段。在注销或身份验证失败的情况下也会呈现此视图,并将针对每种情况显示适当的消息。
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <title>Spring Security Example</title> </head> <body> <div th:if="${param.error}"> <p th:text="${session.error}" th:unless="${session == null}">[...]</p> </div> <div th:if="${param.logout}">You have been logged out.</div> <form th:action="@{/login}" method="post> <div> <label> User Name : <input type="text" name="username" /> </label> </div> <div> <label> Password: <input type="password" name="password" /> </label> </div> <div> <input type="submit" value="Sign In" /> </div> </form> </body> </html>
接下来,我们创建所需的视图,即寄存器视图。该视图将允许用户在应用程序中注册自己。该用户数据将存储在数据库中,然后用于身份验证。
<!DOCTYPE html> <html> <head> <meta charset="ISO-8859-1"> <title>Insert title here</title> </head> <body> <form action="/register" method="post"> <div class="container"> <h1>Register</h1> <p>Please fill in this form to create an account.</p> <hr> <label for="username"> <b>Username</b> </label> <input type="text" placeholder="Enter Username" name="username" id="username" required> <label for="password"><b>Password</b></label> <input type="password" placeholder="Enter Password" name="password" id="password" required> <button type="submit" class="registerbtn">Register</button> </div> </form> </body> </html>
最终项目结构
我们最终的项目结构应该与此类似。
运行应用程序
然后我们可以将应用程序作为 SpringBootApp 运行。当我们在浏览器上访问 localhost:8080 时,它会将我们重定向回登录页面。
身份验证成功后,它将带我们进入带有问候语的索引视图。
因为,在帐户被锁定之前,我们只允许三次失败的尝试,因此在第三次失败的身份验证中,用户将被锁定,并且该消息会显示在屏幕上。
在点击 /register 端点时,我们还可以注册一个新用户。
结论
从今天的文章中,我们学习了如何使用自定义表单通过基于注释的配置使用数据库进行登录。我们还学习了如何防止多次登录尝试失败。在这样做的过程中,我们已经看到了如何实现我们自己的AuthenticationProvider和UserDetailsService来使用我们的自定义身份验证流程对用户进行身份验证。
Spring Security - 表单登录、记住我和注销
内容
- 简介和概述
- 入门(实用指南)
简介和概述
Spring Security 附带了大量内置功能和工具,为我们提供方便。在这个例子中,我们将讨论其中三个有趣且有用的功能 -
- 表单登录
- 记住账号
- 登出
表单登录
基于表单的登录是 Spring Security 提供支持的一种用户名/密码身份验证形式。这是通过 Html 表单提供的。
每当用户请求受保护的资源时,Spring Security 都会检查请求的身份验证。如果请求未经过身份验证/授权,用户将被重定向到登录页面。登录页面必须由应用程序以某种方式呈现。Spring Security 默认提供该登录表单。
此外,如果需要,任何其他配置都必须明确提供,如下所示 -
protected void configure(HttpSecurity http) throws Exception { http // ... .formLogin( form -> form .loginPage("/login") .permitAll() ); }
此代码要求模板文件夹中存在一个 login.html 文件,该文件将在点击 /login 时返回。此 HTML 文件应包含登录表单。此外,该请求应该是对 /login 的 post 请求。参数名称应分别为用户名和密码的“username”和“password”。除此之外,表单中还需要包含 CSRF 令牌。
一旦我们完成了代码练习,上面的代码片段就会更加清晰。
记住账号
这种类型的身份验证需要将记住我的 cookie 发送到浏览器。该cookie存储用户信息/身份验证主体,并存储在浏览器中。因此,网站可以在下次会话启动时记住用户的身份。Spring Security 已为此操作准备了必要的实现。一种使用散列来保护基于 cookie 的令牌的安全性,而另一种使用数据库或其他持久存储机制来存储生成的令牌。
登出
默认 URL /logout 通过以下方式注销用户:
- 使 HTTP 会话失效
- 清除配置的所有 RememberMe 身份验证
- 清除SecurityContextHolder
- 重定向到/login?logout
WebSecurityConfigurerAdapter自动将注销功能应用于 Spring Boot 应用程序。
入门(实用指南)像往常一样,我们首先访问 start.spring.io。这里我们选择一个maven项目。我们将项目命名为“formlogin”并选择所需的 Java 版本。我在此示例中选择 Java 8。我们还继续添加以下依赖项 -
- 春季网
- 春季安全
- 百里香叶
- Spring Boot 开发工具
Thymeleaf是 Java 的模板引擎。它允许我们快速开发静态或动态网页以在浏览器中呈现。它具有极强的可扩展性,允许我们详细定义和自定义模板的处理。除此之外,我们还可以通过点击此链接了解有关 Thymeleaf 的更多信息。
让我们继续生成项目并下载它。然后,我们将其解压到我们选择的文件夹中,并使用任何 IDE 将其打开。我将使用Spring Tools Suite 4。它可以从https://spring.io/tools网站免费下载,并针对 Spring 应用程序进行了优化。
让我们看一下 pom.xml 文件。它应该看起来与此类似 -
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId> com.spring.security</groupId> <artifactId>formlogin</artifactId> <version>0.0.1-SNAPSHOT</version> <name>formlogin</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
让我们在默认包下的文件夹 /src/main/java 中创建一个包。我们将其命名为 config,因为我们会将所有配置类放置在这里。因此,名称应该类似于 - com.tutorial.spring.security.formlogin.config。
配置类
package com.tutorial.spring.security.formlogin.config; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.provisioning.UserDetailsManager; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import com.spring.security.formlogin.AuthFilter; @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean protected UserDetailsService userDetailsService() { UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager(); UserDetails user = User.withUsername("abby") .password(passwordEncoder().encode("12345")) .authorities("read") .build(); userDetailsManager.createUser(user); return userDetailsManager; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests().anyRequest() .authenticated() .and() .formLogin() .and() .rememberMe() .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login") .deleteCookies("remember-me"); } }
代码分解
在我们的配置包中,我们创建了 WebSecurityConfig 类。该类扩展了 Spring Security 的 WebSecurityConfigurerAdapter。我们将使用此类进行安全配置,因此让我们使用 @Configuration 注释来注释它。因此,Spring Security 知道将此类视为配置类。正如我们所看到的,Spring 使应用程序的配置变得非常容易。
让我们看一下我们的配置类。
- 首先,我们将使用 userDetailsService() 方法创建 UserDetailsService 类的 bean。我们将使用此 bean 来管理此应用程序的用户。在这里,为了简单起见,我们将使用 InMemoryUserDetailsManager 实例来创建用户。该用户以及我们给定的用户名和密码将包含一个简单的“读取”权限。
- 现在,让我们看看我们的密码编码器。在本例中,我们将使用 BCryptPasswordEncoder 实例。因此,在创建用户时,我们使用passwordEncoder对我们的明文密码进行编码,如下所示
.password(passwordEncoder().encode("12345"))
- 完成上述步骤后,我们继续进行下一个配置。这里,我们重写WebSecurityConfigurerAdapter类的configure方法。该方法将 HttpSecurity 作为参数。我们将对其进行配置以使用我们的表单登录和注销以及记住我功能。
HTTP安全配置
我们可以观察到所有这些功能在 Spring Security 中都可用。让我们详细研究以下部分 -
http.csrf().disable() .authorizeRequests().anyRequest().authenticated() .and() .formLogin() .and() .rememberMe() .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login") .deleteCookies("remember-me");
这里有几点需要注意 -
- 我们已经禁用了csrf或跨站点请求伪造保护,因为这是一个仅用于演示目的的简单应用程序,所以我们现在可以安全地禁用它。
- 然后我们添加需要对所有请求进行身份验证的配置。正如我们稍后将看到的,为了简单起见,我们将为此应用程序的索引页使用一个“/”端点。
- 之后,我们将使用上面提到的 Spring Security 的 formLogin() 功能。这会生成一个简单的登录页面。
- 然后,我们使用 Spring Security 的 RememberMe() 功能。这将执行两件事。
- 首先,它会在我们使用 formLogin() 生成的默认登录表单中添加一个“记住我”复选框。
- 其次,勾选复选框会生成记住我的 cookie。cookie 存储用户的身份,浏览器存储它。Spring Security 在将来的会话中检测 cookie 以自动登录。
- 最后,我们有 logout() 功能。为此,Spring security 也提供了默认功能。它在这里执行两个重要的功能 -
- 使 Http 会话无效,并取消绑定到会话的对象。
- 它会清除“记住我”cookie。
- 从 Spring 的安全上下文中删除身份验证。
因此,用户无需再次登录即可再次访问该应用程序。
我们还提供了一个 logoutSuccessUrl(),以便应用程序在注销后返回到登录页面。这样就完成了我们的应用程序配置。
受保护的内容(可选)
我们现在将创建一个虚拟索引页面,供用户登录时查看。它还将包含一个注销按钮。
在我们的/src/main/resources/templates中,我们添加一个index.html文件。然后向其中添加一些Html内容。
<!doctype html> <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> <title>Hello, world!</title> </head> <body> <h1>Hello, world!</h1> <a href="logout">logout</a> <!-- Optional JavaScript --> <!-- jQuery first, then Popper.js, then Bootstrap JS --> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script> </body> </html>
此内容来自Bootstrap 4 入门模板。
我们还添加
<a href="logout">logout</a>
到我们的文件,以便用户可以使用此链接注销应用程序。
资源控制器
我们已经创建了受保护的资源,现在添加控制器来服务该资源。
package com.tutorial.spring.security.formlogin.controllers; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class AuthController { @GetMapping("/") public String home() { return "index"; } }
正如我们所看到的,这是一个非常简单的控制器。它只有一个 get 端点,在启动我们的应用程序时为我们的 index.html 文件提供服务。
运行应用程序
让我们将该应用程序作为 Spring Boot 应用程序运行。当应用程序启动时,我们可以在浏览器上访问http://localhost:8080 。它应该要求我们提供用户名和密码。此外,我们还可以看到“记住我”复选框。
登录页面
现在,如果我们提供在 WebSecurity 配置文件中配置的用户信息,我们将能够登录。此外,如果我们勾选“记住我”复选框,我们将能够在我们的 WebSecurity 配置文件中看到“记住我”cookie浏览器的开发者工具部分。
正如我们所看到的,cookie 是与我们的登录请求一起发送的。
此外,网页中还包含一个用于注销的链接。单击该链接后,我们将退出我们的应用程序并返回到我们的登录页面。
Spring Security - Taglib
内容
- 简介和概述
- 春季安全标签
- 授权标签
- 认证标签
- csrfInput 标签
- csrfMetaTags 标签
- 入门(实用指南)
简介和概述
在使用 JSP 的 Spring MVC 应用程序中,我们可以使用 Spring Security 标签来应用安全约束以及访问安全信息。Spring Security Tag库为此类操作提供了基本支持。使用这样的标签,我们可以控制