Spring Security 简明教程
Spring Security - Form Login with Database
Contents
-
Introduction and Overview
-
Spring Security 认证的基本组件 FilterAuthenticationManagerAuthenticationProviderUserDetailsServicePasswordEncoderSpring Security Context 表单登录 使用数据库登录 限制登录尝试次数
-
Getting Started (Practical Guide)
Introduction and Overview
除了提供各种内置认证和授权选项之外,Spring Security 还允许我们尽可能地自定义认证流程,从自定义登录页面到我们自己的自定义认证提供程序和认证过滤器,我们几乎可以自定义认证流程的每一个方面。我们可以定义自己的认证流程,该流程可以从使用用户名和密码的基本认证到使用令牌和 OTP 的复杂认证,例如双因素认证。此外,我们可以使用各种数据库(包括关系型和非关系型数据库),使用各种密码编码器,将恶意用户锁定在账户之外,等等。
今天,我们将讨论三种此类自定义,即自定义表单登录、数据库提供的认证和限制登录尝试次数。虽然这些是很基本的用例,但它们仍然可以让我们更深入地了解 Spring Security 的认证和授权流程。我们还将设置一个注册页面,用户可以通过该页面注册我们应用程序。
首先,让我们了解一下 Spring Security 的架构。它从 servlet 过滤器开始。这些过滤器会拦截请求,对请求执行操作,然后将请求传递给过滤器链中的下一个过滤器或请求处理程序,或者在请求不满足某些条件时会阻止请求。在这一过程中,Spring Security 可以认证请求并在请求上执行各种认证检查。它还可以通过不允许未认证或恶意请求访问受保护的资源来阻止这些请求,从而保护其不受访问。因此,我们的应用程序和资源将受到保护。
Components of Spring Security Architecture
正如我们在上图中看到的,Spring Security 的基本组件如下。我们将在进行过程中对其进行简要讨论。我们还将讨论它们在身份验证和授权过程中的作用。
AuthenticationFilter
这是拦截请求并尝试对其进行身份验证的过滤器。在 Spring Security 中,它将请求转换为一个 Authentication Object,并将身份验证委托给 AuthenticationManager。
AuthenticationManager
这是用于身份验证的主要策略界面。它使用单一方法 authenticate() 来对请求进行身份验证。authenticate() 方法执行身份验证,并在身份验证成功后返回一个 Authentication Object,或者在身份验证失败的情况下抛出 AuthenticationException。如果该方法无法判断,它将返回 null。此过程中的身份验证过程将委托给 AuthenticationProvider,我们将在后面讨论它。
AuthenticationProvider
AuthenticationManager 由 ProviderManager 实现,后者将进程委托给一个或多个 AuthenticationProvider 实例。实现 AuthenticationProvider 接口的任何类都必须实现两个方法——authenticate() 和 supports()。首先,让我们谈谈 supports() 方法。它用于检查我们的 AuthenticationProvider 实现类是否支持特定的身份验证类型。如果支持,则返回 true,否则返回 false。接下来是 authenticate() 方法。身份验证发生在这里。如果支持身份验证类型,则开始身份验证进程。在这里,此类可以使用 UserDetailsService 实现的 loadUserByUsername() 方法。如果找不到用户,它可能会抛出 UsernameNotFoundException。
另一方面,如果找到了用户,那么会使用用户的认证详细信息来对用户进行认证。例如,在基本认证方案中,可能会使用数据库中的密码来检查用户提供的密码。如果发现它们彼此匹配,则是一个成功场景。然后,我们可以从方法中返回一个认证对象,该对象将存储在安全上下文中,我们将在后面讨论安全上下文。
UserDetailsService
它是 Spring Security 的核心接口之一。任何请求的认证主要取决于 UserDetailsService 接口的实现。它最常用于基于数据库的认证中以检索用户数据。数据通过实现唯一的 loadUserByUsername() 方法来检索,在该方法中,我们可以提供逻辑以获取用户的用户详细信息。如果找不到用户,该方法将抛出 UsernameNotFoundException。
PasswordEncoder
在 Spring Security 4 之前,使用 PasswordEncoder 是可选的。用户可以使用基于内存的认证来存储明文密码。但是 Spring Security 5 强制要求使用 PasswordEncoder 来存储密码。这会使用其许多实现之一对用户的密码进行编码。其最常见的实现是 BCryptPasswordEncoder。此外,出于开发目的,我们可以使用 NoOpPasswordEncoder 的一个实例。它将允许密码以明文存储。但它不应该用于生产或实际应用程序。
Spring Security Context
这是在成功认证时存储当前已认证用户详细信息的位置。然后,整个应用程序在会话期间都可以使用认证对象。因此,如果我们需要用户名或任何其他用户详细信息,则需要首先获得 SecurityContext。这是通过 SecurityContextHolder 一个提供程序类来完成的,它提供对安全上下文的访问。我们可以分别使用 setAuthentication() 和 getAuthentication() 方法来存储和检索用户详细信息。
继续,现在让我们讨论我们将用于我们应用程序的三个自定义实现。
Form Login
当我们将 Spring Security 添加到现有的 Spring 应用程序时,它会添加一个登录表单并设置一个虚拟用户。这是自动配置模式下的 Spring Security。在此模式下,它还设置了默认过滤器、身份验证管理器、身份验证提供程序等。此设置是内存中身份验证设置。我们可以覆盖此自动配置以设置我们自己的用户和身份验证流程。我们还可以设置我们自定义的登录方法,例如自定义登录表单。Spring Security 仅需要了解登录表单的详细信息,如 - 登录表单的 URI、登录处理 URL 等。然后,它将为应用程序呈现我们的登录表单,并执行身份验证过程以及其他提供的配置或 Spring 自己的实现。
此自定义表单设置只需遵守特定规则即可集成到 Spring Security 中。我们需要一个用户名参数和一个密码参数,并且这两个参数的名称应分别为“username”和“password”,因为这是默认的名称。在此自定义设置中,如果我们为这些字段使用自己的参数名称,则必须使用 usernameParameter() 和 passwordParameter() 方法通知 Spring Security 这些更改。同样,对于我们对登录表单或表单登录方法所做的任何更改,我们都必须使用适当的方法通知 Spring Security 这些更改,以便它可以将这些更改集成到认证流程中。
Login with a Database
正如我们所讨论的,Spring Security 默认情况下自动提供一种内存认证实现。我们可以通过对存储在数据库中的用户详细信息进行认证来覆盖此认证实现。在这种情况下,在认证用户时,我们可以针对认证检查用户提供的凭据与数据库中的凭据是否一致。我们还可以允许新用户在我们应用程序中注册,并将他们的凭据存储在同一数据库中。此外,我们可以提供更改或更新其密码、角色或其他数据的方法。因此,这为我们提供了持久的用户数据,可长期使用。
Login Attempts Limit
为了限制我们应用程序中的登录尝试次数,我们可以使用 Spring Security 的 isAccountNonLocked 属性。Spring Security 的 UserDetails 为我们提供了该属性。我们可以设置一种认证方法,其中,如果任何用户或其他人多次提供不正确的凭据,我们可以锁定他们的账户。即使用户提供了正确的凭据,Spring Security 也将禁用对已锁定用户的认证。这是 Spring Security 提供的一项内置功能。我们可以将不正确的登录尝试次数存储在我们的数据库中。然后,对于每次不正确的认证尝试,我们都可以更新并检查数据库表。当此类尝试的次数超过给定次数时,我们可以将用户锁定在账户之外。因此,用户在账户解锁之前将无法再次登录。
Getting Started (Practical Guide)
现在让我们从我们的应用程序开始。我们需要为此应用程序的工具如下所列:
-
A Java IDE - 优先使用 STS 4,但 Eclipse、IntelliJ Idea 或任何其他 IDE 都可以。
-
MySql Server Community Edition - 我们需要在我们的系统中下载并安装 MySql Community Server。我们可单击此链接访问官方网站。
-
MySql Workbench - 这是一种 GUI 工具,我们可以使用它与 MySql 数据库交互。
Database Setup
首先,让我们设置数据库。我们将为此应用程序使用一个 MySql 数据库实例。 MySql Server Community Edition 可免费下载和使用。我们将使用 MySql Workbench 连接到我们的 MySql Server 并创建一个名为“spring”的数据库,以与我们的应用程序一起使用。
然后,我们将创建两个表(users 和 attempts),以持久化我们的用户和登录尝试。如前所述,注册我们应用程序的用户详细信息将存储在 users 表中。任何用户登录尝试的次数都将在 attempts 表中针对他的用户名存储。通过这种方式,我们可以跟踪尝试并采取必要的措施。
让我们了解一下用于设置我们的 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);
Project Setup
和往常一样,我们将使用 Spring Initializer 设置我们的项目。我们将创建一个 Maven 项目,使用 Spring Boot 版本 2.3.2。让我们将我们的项目命名为 formlogin(我们可以选择任何我们想要的名称),并将组 ID 命名为 com.tutorial.spring.security。此外,我们将为此项目使用 Java 版本 8。
Dependencies
现在,对于依赖项,我们将为这个演示尽可能地让我们的应用程序保持简单。我们将专注于我们今天想要探索的功能。因此,我们将选择最少的依赖项,以帮助我们快速设置和启动应用程序。让我们了解一下依赖项:
-
Spring Web - 它捆绑了与 Web 开发相关的所有依赖项,包括 Spring MVC、REST 和嵌入式 Tomcat 服务器。
-
Spring Security − 实施 Spring Security 提供的安全功能。
-
Thymeleaf − 用于 HTML5 / XHTML / XML 的服务器端 Java 模板引擎。
-
Spring Data JPA − 除了使用 JPA 规范定义的所有功能之外,Spring Data JPA 还添加了自己的功能,例如存储库模式的无代码实现以及从方法名称创建数据库查询。
-
Mysql Driver − 用于 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>
我们可以看到此处列出了我们的项目详细信息以及依赖项。
Data Source
我们将在 application.properties 文件中配置我们的数据源。由于我们将使用本地 MySQL DB 作为数据源,因此我们在此处提供本地 DB 实例的 url、用户名和密码。我们已将数据库命名为“spring”。
spring.datasource.url=jdbc:mysql://localhost:3306/spring
spring.datasource.username=root
spring.datasource.password=root
Entities
让我们现在创建我们的实体。我们从 User 实体开始,它包含三个字段 - 用户名、密码和 accountNonLocked。此 User 类还实现了 Spring Security 的 UserDetails 接口。此类提供核心用户信息。它用于存储稍后可封装到 Authentication 对象中的用户数据。不建议直接实现接口。但对于我们的案例,由于这是一个演示基于数据库的登录的简单应用程序,我们直接在这里实现了此接口,以保持简单。我们可以通过在我们的 User 实体周围使用包装器类来实现此接口。
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,因为我们不会在此应用程序中探索这些属性。
对于此用户的权限列表,让我们暂时为他分配一个虚拟角色。我们也不会将此属性用于此应用程序。
Attempts.java
继续,让我们创建 Attempts 实体来持久化我们的无效尝试计数。如在数据库中创建的那样,我们在这里有三个字段 - username、一个名为 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;
}
}
Repositories
我们已经创建了实体,让我们创建存储库来存储和检索数据。我们将创建两个存储库,每个实体类一个。对于这两个存储库接口,我们将扩展 JpaRepository,它为我们提供了内置实现来保存和检索来自我们在 application.properties 文件中配置的数据库的数据。我们还可以在此处添加我们的方法或查询(除了提供的查询之外)。
UserRepository.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 对象,其中包含用户名和用户执行过的身份验证尝试的失败数目。
Configuration
鉴于我们准备使用一个自定义登录表单,所以我们必须覆盖 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() 方法,以提供我们的登录方法实现。
Security Setup
现在,我们将设置我们的身份验证过程。我们准备使用数据库和用户账户锁定机制来设置身份验证。
我们首先创建我们的 UserDetailsService 实现。正如我们之前讨论的,我们需要针对使用数据库的身份验证提供我们的自定义实现。这是因为我们了解,Spring Security 默认情况下只提供了一个内存身份验证实现。因此,我们需要使用基于数据库的过程覆盖该实现。要做到这一点,我们需要覆盖 UserDetailsService 的 loadUserByUsername() 方法。
UserDetailsService
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 进行注册的用户。
Authentication Provider
我们现在将实现我们的自定义身份验证提供程序。它将实现 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() 方法。我们在其中执行多项操作:
-
supports() - 我们还有一个 supports 方法,用于检查我们的 AuthenticationProvider 实现类是否支持我们的身份验证类型。如果匹配、不匹配或无法决定,则它分别返回 true、false 或 null。现在我们将其硬编码为 true。
Controller
现在让我们创建我们的控制器包。它将包含我们的 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;
}
}
-
index ("/") - 此端点将提供我们应用程序的索引页。正如我们之前配置的那样,我们将保护此页并且只允许经过身份验证的用户才能访问此页。
-
login ("/login") - 正如前面提到的,此端点将用于提供我们的自定义登录页。任何未经身份验证的用户都将被重定向到此端点以进行身份验证。
-
register("/register") (GET) - 我们的应用程序将有两个“register”端点。一个用于提供注册页。另一个用于处理注册流程。所以,前者将使用 HTTP GET,后者将是一个 POST 端点。
-
register("/register") (POST) - 我们将使用此端点来处理用户注册流程。我们将从参数中获取用户名和口令。然后我们将使用我们已 @Autowired 至此组件中 的 passwordEncoder 对口令进行编码。我们还会在此处设置用户账户为解锁状态。然后我们将使用 createUser() 方法将此用户数据保存在我们的 users 表中。
除上述内容之外,我们还有一个 getErrorMessage() 方法。它用于确定最近抛出的异常,以在我们的登录模版中添加一条消息。这样,我们可以了解身份验证错误并显示适当的消息。
Resources
我们已经创建了我们的端点,现在唯一要做的事情就是创建我们的视图。
首先,我们将创建我们的索引页。该页面只有在验证成功后才允许用户访问。该页面可以访问 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>
Final Project Structure
我们的最终项目结构应与此类似。
Running the Application
然后,我们可以在 SpringBootApp 中将该应用程序作为 SpringBootApp 运行。当我们在浏览器中转到 localhost:8080 时,它会将我们重定向回登录页面。
在验证成功后,它将带我们进入带有问候语的索引视图。
由于我们只允许在帐户被锁定之前尝试三次失败,因此在第三次验证失败后,用户将被锁定,并且消息将显示在屏幕上。
在进入 /register 端点后,我们还可以注册新用户。
Conclusion
从今天的文章中,我们学习了如何使用基于注释的配置使用数据库为登录使用自定义表单。我们还学习了如何防止多次登录尝试失败。在进行此操作时,我们已经了解了如何实现我们自己的 AuthenticationProvider 和 UserDetailsService 以使用我们自定义验证过程验证用户。
Spring Security - Form Login, Remember Me and Logout
Introduction and Overview
Spring Security 带有许多内置功能和工具,方便我们使用。在此示例中,我们将讨论其中三个有趣且有用的功能 −
-
Form-login
-
Remember Me
-
Logout
Form Login
基于表单的登录是 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 令牌。
一旦我们完成练习代码,上面的代码片段将更加清晰。
Remember Me
此类验证需要向浏览器发送 Remember-me cookie。此 cookie 会存储用户的信息/验证负责人,并且存储在浏览器中。因此,当会话开始时,网站可以记住用户身份。Spring Security 为此操作提供了必要的实现。一个使用哈希来保护基于 cookie 的令牌的安全性,而另一个使用数据库或其他持久存储机制来存储生成的令牌。
Logout
默认的 URL /logout 通过下列方式将用户注销 −
-
Invalidating the HTTP Session
-
清除任何已配置的 RememberMe 身份验证
-
Clearing the SecurityContextHolder
-
Redirect to /login?logout
WebSecurityConfigurerAdapter 自动将注销功能应用到 Spring Boot 应用程序。
Getting Started (Practical Guide) 和往常一样,我们先访问 start.spring.io。在这里我们选择一个 maven 项目。我们将项目命名为“formlogin”并选择所需的 Java 版本。针对此示例我选择了 Java 8。我们还继续添加以下依赖关系:
-
Spring Web
-
Spring Security
-
Spring Boot DevTools
Thymeleaf 是 Java 的模板引擎。它使我们能够快速开发用于在浏览器中渲染的静态或动态网页。它已得到极大的扩展,它允许我们定义和定制精细处理的模板。除此之外,我们可以通过点击 link 来了解更多有关 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。
The Configuration Class
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");
}
}
Code Breakdown
在我们的配置包内部,我们创建了 WebSecurityConfig 类。此类扩展了 Spring Security 的 WebSecurityConfigurerAdapter。我们将在安全配置中使用此类,因此,让我们用 @Configuration 注释对此类进行注释。因此,Spring Security 知道将此类视为配置类。正如我们所看到的,Spring 极大地简化了配置应用程序。
让我们看一看我们的配置类。
-
首先,我们使用`userDetailsService()`方法创建一个`UserDetailsService`类的bean。我们将使用这个bean来管理此应用程序的用户。为了简便起见,我们使用`InMemoryUserDetailsManager`实例创建一个用户。此用户将包含一个简单的“读取”权限,以及我们提供的用户名和密码。
-
现在,我们来看看我们的`PasswordEncoder`。我们将使用`BCryptPasswordEncoder`实例作为此示例。因此,在创建用户时,我们使用`passwordEncoder`对其明文密码进行编码,如下所示:
.password(passwordEncoder().encode("12345"))
-
完成上述步骤后,我们转到下一个配置。我们重写`WebSecurityConfigurerAdapter`类的`configure`方法。此方法将`HttpSecurity`作为参数。我们将对其进行配置以使用表单登录和注销,以及“Remember me”功能。
Http安全配置
我们可以观察到所有这些功能均在 Spring Security 中提供。让我们详细研究以下部分 −
http.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.and()
.logout()
.logoutUrl("/logout") .logoutSuccessUrl("/login") .deleteCookies("remember-me");
此处有一些需要说明的点 −
-
我们禁用了 csrf 或 Cross-Site Request Forgery 保护。因为这只是出于演示目的的一个简单应用,我们现在可以安全地禁用它。
-
然后,我们添加配置,要求对所有请求进行身份验证。正如我们稍后将看到的,为了简单起见,我们将为该应用程序的索引页面设置一个“/”端点。
-
之后,我们将使用前面提到的 Spring Security 的`formLogin()`功能。这将生成一个简单的登录页面。
-
然后,我们使用 Spring Security 的`rememberMe()`功能。这将执行两件事。
-
最后,我们有`logout()`功能。对于这个功能,Spring security 也提供了一个默认功能。在此,它执行两个重要功能 −
The Protected Content (Optional)
现在,我们将创建一个虚拟的索引页面,供用户登录后查看。它还将包含一个“注销”按钮。
在`./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>
我们还向文件添加
<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 文件。
Running the application
让我们作为 Spring Boot 应用程序运行该应用程序。当应用程序启动时,我们可以在浏览器中转到 http://localhost:8080 。它应该要求我们输入用户名和密码。此外,我们还应该能够看到记住我复选框。
Spring Security - Taglib
Introduction and Overview
在 JSP 中使用 Spring MVC 应用程序时,我们可以使用 Spring Security 标签来应用安全约束以及访问安全信息。Spring Security 标签库为这样的操作提供了基本支持。使用这样的标签,我们可以根据用户的角色或权限控制向用户显示的信息。此外,我们可以在我们的表单中包含 CSRF 保护功能。
为了使用 Spring 安全标签,我们必须在 JSP 文件中声明安全标记库。
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
现在,我们可以使用带有“sec”前缀的 Spring 安全标签。现在让我们了解一下标签的使用。
The authorize Tag
我们将讨论的第一个标签是 authorize 标签。让我们看一下一些使用示例。
<sec:authorize access="!isAuthenticated()"> Login </sec:authorize>
<sec:authorize access="isAuthenticated()"> Logout </sec:authorize>
<sec:authorize access="hasRole('ADMIN')"> Hello Admin. </sec:authorize>
如我们所见,我们可以使用此标签根据访问或角色来隐藏或显示部分信息。为了评估角色或访问,我们还可以使用以下 Spring Security Expressions −
-
hasRole(“ADMIN”) − 如果当前用户具有 admin 角色,则评估为 true。
-
hasAnyRole(‘ADMIN’,’USER’) − 如果当前用户具有列出的任何角色,则评估为 true
-
isAnonymous() − 如果当前用户是匿名用户,则评估为 true
-
isRememberMe() − 如果当前用户是记住我用户,则评估为 true
-
isFullyAuthenticated() − 如果用户经过认证并且既不是匿名用户也不是记住我用户,则评估为 true
如我们所见,access 属性是指定 web 安全表达式的。然后,Spring Security 会评估表达式。评估通常委托给在应用程序上下文中定义的 SecurityExpressionHandler<FilterInvocation>。如果返回 true,则用户可以访问该部分中给出的信息。
如果我们使用 Spring Security 的 Permission Evaluator 将 authorize 标签与 Spring Security 权限评估器一起使用,我们还可以检查用户权限,如下所示 −
<sec:authorize access="hasPermission(#domain,'read') or hasPermission(#domain,'write')">
This content is visible to users who have read or write permission.
</sec:authorize>
我们还可以允许或限制用户点击内容中的特定链接。
<sec:authorize url="/admin">
This content will only be visible to users who are authorized to send requests to the "/admin" URL.
</sec:authorize>
The authentication tag
当我们需要访问存储在 Spring Security 上下文中的当前身份验证对象时,我们可以使用 authentication 标记。然后,我们可以使用它来直接在我们的 JSP 页面中呈现对象的属性。例如,如果我们想要在页面中呈现 Authentication 对象的 principal 属性,我们可以按照如下所示进行操作 −
<sec:authentication property="principal.username" />
The csrfInput Tag
当启用 CSRF 保护时,我们可以使用 csrfInput 标记插入带 CSRF 保护令牌正确值的隐藏表单字段。如果未启用 CSRF 保护,此标记不输出任何内容。
我们可以将此标记放置在 HTML <form></form> 块中,以及其他输入字段。但是,我们不能将此标记放置在 <form:form></form:form> 块中,因为 Spring Security 会自动在这些标记中插入 CSRF 表单字段,还会自动处理 Spring 表单。
<form method="post" action="/do/something">
<sec:csrfInput />
Username:<br />
<input type="text" username="username" />
...
</form>
The csrfMetaTags Tag
我们可以使用此标记插入包含 CSRF 保护标记表单字段和标题名称及 CSRF 保护标记值的元标记。这些元标记可用于在应用程序的 Javascript 中使用 CSRF 保护。但是,此标记仅在我们已在应用程序中启用了 CSRF 保护时才起作用,否则此标记不输出任何内容。
<html>
<head>
<title>CSRF Protection in Javascript</title>
<sec:csrfMetaTags />
<script type="text/javascript" language="javascript">
var csrfParam = $("meta[name='_csrf_param']").attr("content");
var csrfToken = $("meta[name='_csrf']").attr("content");
</script>
</head>
<body>
...
</body>
</html>
Getting Started (Practical Guide)
现在我们已经讨论了标记,让我们构建一个应用程序来演示如何使用标记。我们将使用 Spring Tool Suite 4 作为我们的 IDE。此外,我们将使用 Apache Tomcat 服务器来服务我们的应用程序。所以,让我们开始吧。
Setting up the Application
让我们在 STS 中创建一个简单的 Maven 项目。我们可以将我们的应用程序命名为 taglibsdemo,并将其打包为 .war 文件。
完成应用程序的设置后,它应该具有类似于此的结构。
The pom.xml file
我们将向我们的应用程序添加以下依赖关系 −
-
Spring Web MVC
-
Spring-Security-Web
-
Spring-Security-Core
-
Spring-Security-Taglibs
-
Spring-Security-Config
-
Javax Servlet Api
-
JSTL
添加这些依赖关系后,我们的 pom.xml 应类似于此 −
<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>
<groupId>com.tutorial.spring.security</groupId>
<artifactId>taglibsdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency> <dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
让我们为此应用程序创建我们的基本包。我们可以将其命名为 com.taglibsdemo。在包中,让我们为我们的配置文件创建另一个包。由于它将保存配置文件,我们可以将其命名为 config。
ApplicationConfig.java
让我们创建我们的第一个配置文件类 ApplicationConfig.java。
package com.taglibsdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
@EnableWebMvc
@Configuration @ComponentScan({ "com.taglibsdemo.controller"} )
public class ApplicationConfig {
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver
viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp"); return viewResolver;
}
}
我们在此分解代码 −
-
@EnableWebMvc − 我们使用 @EnableWebMvc 启用 Spring MVC。因此,我们把此注释添加到 @Configuration 类里,以从 WebMvcConfigurationSupport 导入 Spring MVC 配置。WebMvcConfigurationSupport 是提供 MVC Java 配置的主要类。不使用此注释可能导致诸如内容类型和接收标题的内容协商通常无效的情况。 @EnableWebMvc 注册 RequestMappingHandlerMapping、RequestMappingHandlerAdapter 和 ExceptionHandlerExceptionResolver,以支持使用 WebMvcConfigurationSupport 等注释处理带注释控制器的请求方法,比如 @RequestMapping 和 @ExceptionHandler 等。
-
@ComponentScan − @ComponentScan 注释用于告诉 Spring 扫描注释组件的程序包。@ComponentScan 还用于指定基本程序包和基本程序包类,方法是使用 @ComponentScan 的 basePackageClasses 或 basePackages 属性。
-
InternalResourceViewResolver − 将提供的 URI 解析到格式为前缀 + 视图名称 + 后缀的实际 URI 中。
-
setViewClass() − 设置应用于创建视图的视图类。
-
setPrefix() − 设置在构建 URL 时添加到视图名称前面的前缀。
-
setSuffix() − 设置在构建 URL 时添加到视图名称后面的后缀。
WebSecurityConfig.java
接下来,我们创建 WebSecurityConfig 类,它将扩展 Spring Security 的熟悉的 WebSecurityConfigurerAdapter 类。
package com.taglibsdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
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.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.User.UserBuilder;
@EnableWebSecurity @ComponentScan("com.taglibsdemo")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@SuppressWarnings("deprecation") @Bean
public UserDetailsService userdetailsService() {
UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("rony").password("rony123").roles("USER").build());
manager.createUser(users.username("admin").password("admin123").roles("ADMIN").build());
return manager;
}
@Override protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() .antMatchers("/index", "/").permitAll()
.antMatchers("/admin", "/user").authenticated() .and() .formLogin()
.and() .logout() .logoutRequestMatcher(
new AntPathRequestMatcher("/logout")
);
}
}
我们在此分解代码 −
-
WebSecurityConfigurerAdapter − 实现 WebSecurityConfigurer 和 WebSecurityConfiguraer 的抽象类,允许我们覆盖安全配置的方法。
-
@EnableWebSecurity − 它使 Spring 能够自动查找并对全局 WebSecurity 应用 @Configuration 类。
-
然后,我们使用创建用户的方法使用 InMemoryUserDetailsManager 实例创建 UserDetailsService Bean。我们创建两个用户——一个具有 “USER” 角色,另一个具有 “ADMIN” 角色,并将其添加到 Spring Security 中。
-
在那之后,我们使用 HttpSecurity 作为参数覆盖 configure 方法。我们让所有人可以访问我们的主页或索引页,让已验证的用户可以访问管理页。接下来,我们添加 Spring Security 表单登录和注销。
因此,通过这些步骤,我们的安全配置就完成了。现在,我们准备进入下一步。
SpringSecurityApplicationInitializer.java
继续,现在,我们创建 SpringSecurityApplicationInitializer.java 类,该类扩展 Spring Security 的 AbstractSecurityWebApplicationInitializer 类。
package com.taglibsdemo.config;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SpringSecurityApplicationInitializer extends
AbstractSecurityWebApplicationInitializer { }
AbstractSecurityWebApplicationInitializer 是实现 Spring 的 WebApplicationInitializer 的一个抽象类。因此,如果 classpath 包含 spring-web 模块,SpringServletContainerInitializer 将初始化该类的具体实现。
MvcWebApplicationInitializer.java
package com.taglibsdemo.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class MvcWebApplicationInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override protected Class</?>[] getRootConfigClasses() {
return new Class[] {WebSecurityConfig.class};
}
@Override protected Class</?>[] getServletConfigClasses() {
return null;
}
@Override protected String[] getServletMappings() {
return new String[] {"/"};
}
}
-
AbstractAnnotationConfigDispatcherServletInitializer − 该类扩展了 WebApplicationInitializer。我们需要此类作为在 Servlet 容器环境中初始化 Spring 应用程序的基本类。结果是 AbstractAnnotationConfigDispatcherServletInitializer 的子类将提供使用 @Configuration、Servlet 配置类和 DispatcherServlet 映射模式进行注解的类。
-
getRootConfigClasses() − 此方法必须由扩展 AbstractAnnotationConfigDispatcherServletInitializer 的类实现。它提供了“根”应用程序上下文配置。
-
getServletConfigClasses() − 此方法也必须实现,以提供 DispatcherServlet 应用程序上下文配置。
-
getServletMappings() − 此方法用于指定 DispatcherServlet 的 servlet 映射。
我们已经设置了配置类。现在,我们将创建我们的控制器来服务 JSP 页面。
HelloController.java
package com.taglibsdemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller public class HelloController {
@GetMapping("/")
public String index() { return "index"; }
@GetMapping("/user")
public String user() { return "admin"; }
@GetMapping("/admin")
public String admin() { return "admin"; }
}
这里,我们创建了三个端点 - “/”,“/ user ”和 “/ admin ”。正如之前在我们的配置中指定的,我们将允许对索引页面
“/”进行未授权的访问。另一方面,“/user”和 “/admin”端点只能进行授权访问。
Secure Content to serve
继续前进,我们现在将创建 JSP 页面,该页面将在击中特定端点时提供服务。
为此,在我们 src/main 文件夹内创建一个名为 webapp 的文件夹。在这个文件夹中,我们创建我们的 WEB-INF 文件夹,然后和 ApplicationConfig.java 类中一样,我们添加 views 文件夹。在这里,在这个文件夹中,我们将添加视图。
让我们先添加我们的主页,即 index.jsp。
<%@ page language="java" contentType="text/html;
charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<title>Home Page</title>
</head>
<body>
<a href="user">User</a>
<a href="admin">Admin</a>
<br>
<br> Welcome to the Application!
</body>
</html>
然后,我们来创建一个 admin.jsp 文件。我们来把它加进去。
<%@ page language="java" contentType="text/html;
charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="security"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body> Welcome to Admin Page! <a href="logout"> Logout </a>
<br>
<br>
<security:authorize access="hasRole('ADMIN')"> Hello Admin!
</security:authorize>
</body>
</html>
这里,我们添加了 <%@ taglib uri="http://www.springframework.org/security/tags" prefix="security"%>。正如之前讨论过的,这将让我们可以使用 Spring 安全标记库。正如我们看到的,我们在内容周围添加了 “authorize” 标记。我们的管理员只能访问此内容。任何访问此页面的其他用户都将无法查看此内容。
Running the application
我们现在右键单击项目并选择“在服务器上运行”。当服务器启动并且我们的应用程序正在运行时,我们可以在浏览器中访问 localhost:8080/taglibsdemo/ 来查看页面。
Login page
现在,如果我们单击应用程序中的用户链接,系统将要求我们登录。
这里,正如我们在控制器中看到的,我们为用户和管理员链接提供服务。但是,如果我们的用户不是管理员,他将无法查看受我们的 “authorize” 标记保护的内容。
首先以用户身份登录。
可见,“Hello Admin!” 内容对我们不可见。这是因为当前用户没有管理员角色。
现在,注销并以管理员身份登录。
现在,我们能够看到受保护的内容“Hello Admin!”,因为当前用户拥有管理员角色。
Conclusion
我们学习了如何使用 Spring Security 标记库来保护我们的内容并在 Spring Security 上下文中访问当前的 Authentication 对象。
Spring Security - XML Configuration
Fundamentals
在本部分中,我们将讨论如何使用 XML 配置配置 Spring Security。我们将开发一个带有 Spring Security 的简单 Spring 应用程序。在此过程中,我们将详细讨论我们正在使用的各个组件。
Authentication and Authorization
-
验证——验证确保用户或客户端是他们自称的身份。Spring Security 有多种方法让我们来执行验证。Spring Security 支持基本验证、LDAP 验证、JDBC 验证等。
-
授权——验证用户是否拥有操作的权限。如果我们的应用程序是复杂的,有不同类型的用户,例如管理员、普通用户和其他权限较低的用户,我们需要在应用程序中维护访问控制。例如,访客用户不应该能够访问管理员内容。因此,为了控制对应用程序中的各种资源的访问,我们需要检查用户是否有权访问该资源。
以上主题是 Spring Security 的两个主要组件。Spring Security 为我们提供了实现应用程序中的身份验证和授权的各种内置功能。我们可以根据需要更改这些功能,以便非常快速地保护应用程序。此外,Spring Security 还允许对之前提到的功能进行大量自定义,以实现我们自己复杂的身份验证和授权。
Getting Started (Practical Guide)
让我们看一个使用内置 Spring Security 功能的基本示例。在此示例中,我们将使用 Spring Security 开箱即用的选项保护我们的应用程序。这将让我们大致了解 Spring Security 的各种组件以及如何将它们用于我们的应用程序。我们将使用 XML 来配置我们应用程序的安全功能。
我们将在应用程序中使用的工具是 Spring Tool Suite 4 和 Apache Tomcat Server 9.0 。它们都可免费下载和使用。
首先,在 STS 中启动一个新的简单 Maven 项目。我们可以根据我们的选择来选择组 ID 和项目 ID。之后,单击完成。因此,我们将项目添加到我们的工作区。让我们留出一些时间让 STS 来构建和验证我们的项目。
我们的项目结构最终会类似于此。
接下来,添加依赖项。我们将选择以下依赖项。
-
Spring Web MVC
-
Spring-Security-Web
-
Spring-Security-Core
-
Spring-Security-Config
-
Javax Servlet API
pom.xml
添加这些依赖项后,我们就可以配置项目了。快来看看我们的 pom.xml 文件。
<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>
<groupId>com.tutorial.spring.security</groupId>
<artifactId>xmlconfigurationdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>Spring Security with XML configuration</name> <description>Spring Security with XML configuration</description>
<properties>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.source>1.8</maven.compiler.source> </properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.2.RELEASE<version>
</dependency> <dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
Controller and views
首先,我们将创建我们的控制器。因此,创建一个名为 controller 的包,并将我们的 HomeController 类添加到包中。
package com.tutorial.spring.security.xmlconfigurationdemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller public class HomeController { @GetMapping("/")
public String index() { return "index"; }
@GetMapping("/admin")
public String admin() { return "admin"; } }
此处,我们有两个端点——“index”和“admin”。虽然所有人均可访问索引页,但我们将保护我们的“admin”页。
由于我们已经创建了路由,那么也来添加页面吧。
在我们的 /src/main/webapp 文件夹中,让我们创建一个名为 WEB-INF 的文件夹。然后,在该文件夹内部,我们将创建一个名为 views 的文件夹,在此处我们将创建我们的视图。
让我们创建我们的第一个视图 -
<%@ page language="java" contentType="text/html;
charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1"> <title>Insert title here</title>
</head>
<body>
<h2>Welcome to Spring Security!</h2>
</body>
</html>
然后,我们创建我们的管理视图。
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1"> <title>Insert title here</title>
</head>
<body>
Hello Admin
</body>
</html>
接下来,让我们配置我们的应用程序。
配置。
web.xml
现在,让我们添加我们第一个 xml 文件 - web.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value> /WEB-INF/security-config.xml </param-value>
</context-param>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
代码分解
-
分派程序 Servlet - 我们在此声明的第一个 servlet 是分派程序 servlet。分派程序 servlet 是任何 Spring MVC 应用程序的入口点,是整个 Spring MVC 框架设计的核心。它拦截所有 HTTP 请求并将它们分派到已注册的处理程序以处理 Web 请求。它还提供方便的映射和异常处理工具。servlet 的加载顺序取决于“load-on-startup”值。具有较低“load-on-startup”值的 servlet 将在具有较高值的 servlet 之前加载。
-
contextConfigLocation - 它是一个字符串,指示可以在哪里找到上下文。此字符串表示可加载我们配置的路径。
-
servlet-mapping - 我们使用 Servlet 映射来告知 Spring 容器将哪个请求路由到哪个 servlet。在我们的例证中,我们正在将所有请求路由到我们的“spring”分派程序 servlet。
-
侦听器 - 侦听某些类型事件的类,并在该事件发生时触发适当的功能。每个侦听器都绑定到一个事件。在我们的例证中,我们将使用 ContextLoaderListener 为 Web 应用程序创建一个根 Web 应用程序上下文。然后将其放入可用于加载和卸载弹簧托管 Bean 的 ServletContext。
-
过滤器 - Spring 使用过滤器在将请求移交给分派程序 Servlet 之前对请求进行处理,也用于在分派请求后处理响应。DelegatingFilterProxy 将应用程序上下文链接到 web.xml 文件。进入此应用程序的请求将通过我们的过滤器,我们将其命名为“spring SecurityFilterChain”,然后再到达其控制器。Spring Security 可以在此处获取请求,并对其执行操作,然后再将其传递给下一组过滤器或处理程序。
security-config.xml
接下来,我们将创建我们的 security-config.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<http auto-config="true">
<intercept-url pattern="/admin"
access="hasRole('ROLE_ADMIN')" /> </http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="admin" password="{noop}1234" authorities="ROLE_ADMIN" />
</user-service>
</authentication-provider> </authentication-manager>
<beans:bean id ="passwordEncoder"
class = "org.springframework.security.crypto.password.NoOpPasswordEncoder"
factory-method = "getInstance">
</beans:bean>
</beans:beans>
代码分解
-
http 元素 - 所有 Web 相关命名空间功能的父级。在此处,我们可以配置要拦截的 URL,需要的权限,要使用的登录类型以及所有此类配置。
-
auto-config - 将此属性设置为 true 将自动设置表单登录、基本登录和注销功能。Spring Security 通过使用标准值和启用的功能生成它们。
-
intercept-url - 它设置了我们想要保护的 URL 模式,使用 access 属性。
-
access - 它指定哪些用户允许访问由 pattern 属性指定的 URL。它是根据用户的角色和权限进行的。我们可以使用此属性的 SPEL。
-
authentication-manager - <authentication-manager>用于配置应用中的用户及其密码和角色。这些用户将可以访问应用的受保护部分,前提是他们具有适当的角色。<authentication-provider< 元素将创建一个 DaoAuthenticationProvider bean,<user-service< 元素将创建一个 InMemoryDaoImpl。所有 authentication-provider 元素都将通过向 authentication-manager 提供用户信息来允许用户进行身份验证。
-
password-encoder - 这将注册一个密码编码器 bean。为了简化起见,我们在这里使用了 NoOpPasswordEncoder。
接着,我们创建最后的一个配置文件 - app-config 文件。在这里,我们将添加视图解析器代码并定义我们的基本包。
app-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<mvc:annotation-driven />
<context:component-scan
base-package="com.tutorial.spring.security.xmlconfigurationdemo.controller">
</context:component-scan>
<context:annotation-config>
</context:annotation-config>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
</beans>
在这里,正如我们可以看到的那样,我们正在注册我们之前创建的视图。为此,我们使用 InternalResourceViewResolver 类,它将提供的 URI 映射到实际 URI。
例如,使用上述配置,如果我们请求 URI “/admin”,DispatcherServlet 将请求转发到
prefix + viewname + suffix = /WEB-INF/views/admin.jsp 视图。
Running the application
通过此简单配置,我们的应用已准备就绪。我们可以右键单击该项目并选择在服务器上运行。我们可以选择我们的 Tomcat 服务器。当服务器启动时,我们可以转到 localhost:8080/xmlconfigurationdemo 以与我们的应用交互。
如果我们输入正确的凭据,我们将能够登录并看到所需的那些内容。
Spring Security - OAuth2
OAuth 2.0 Fundamentals
OAuth 2.0 由IETF OAuth 工作组开发,并于 2012 年 10 月发布。它作为一种开放授权协议,允许第三方应用代表资源所有者获取对 HTTP 服务的有限访问权限。它可以做到这一点,同时不会透露用户的身份或长期凭据。第三方应用本身也可以代表其使用它。OAuth 的工作原理包括将用户身份验证委托给承载用户帐户的服务,并授权第三方应用访问用户的帐户。
让我们考虑一个例子。假设我们想登录网站 “clientsite.com”。我们可以通过 Facebook、Github、Google 或 Microsoft 登录。我们选择上述给出的任何选项,并将被重定向到相应网站进行登录。如果登录成功,系统将询问我们是否要允许 clientsite.com 访问其请求的特定数据。我们选择我们所需的选项,然后根据我们对第三方资源的操作,用授权代码或错误代码重定向到 clientsite.com,我们的登录是否成功取决于我们的操作。这是 OAuth 2 的基本工作原理。
OAuth 系统中有五个关键参与者。我们来列出它们:
-
User / Resource Owner - 最终用户,负责身份验证并同意与客户端共享资源。
-
User-Agent - 用户使用的浏览器。
-
Client - 请求访问令牌的应用。
-
Authorization Server - 用于对用户/客户端进行身份验证的服务器。它颁发访问令牌并在其整个生命周期内对其进行跟踪。
-
Resource Server - 提供对请求资源的访问的 API。它验证访问令牌并提供授权。
Getting Started
我们将使用 Spring Security 和 OAuth 2.0 开发一个 Spring Boot 应用来说明上述内容。我们现在将使用内存数据库开发一个基本应用来存储用户凭据。该应用将使我们很容易理解 OAuth 2.0 与 Spring Security 的工作原理。
让我们使用 Spring Initializer 在 Java 8 中创建一个 maven 项目。我们从访问 start.spring.io 开始。我们使用以下依赖项生成一个应用:
-
Spring Web
-
Spring Security
-
Cloud OAuth2
-
Spring Boot Devtools
利用上述配置,我们点击生成按钮来生成一个项目。项目将以一个 zip 文件的形式下载。我们解压它到一个文件夹中。然后我们可以在我们选择的 IDE 中打开项目。这里我使用 Spring Tools Suite,因为它针对 spring 应用进行了优化。我们也可以根据需要使用 Eclipse 或 IntelliJ Idea。
因此,我们在 STS 中打开项目,让它下载依赖项。然后我们可以在我们的包资源管理器窗口中看到项目结构。它应该与以下屏幕截图相似。
如果我们打开 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.tutorial</groupId>
<artifactId>spring.security.oauth2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring.security.oauth2</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.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.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot<groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement><build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
现在,到我们的应用程序的基础包,即 com.tutorial.spring.security.oauth2 ,让我们添加一个名为 config 的新包,我们将在其中添加我们的配置类。
让我们创建一个扩展 Spring Security 中的 WebSecurityConfigurerAdapter 类以管理客户端应用程序用户的第一个配置类 UserConfig 。我们使用 @Configuration 注解来注释类以告诉 Spring 它是一个配置类。
package com.tutorial.spring.security.oauth2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
@Configuration public class UserConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService() {
UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
UserDetails user = User.withUsername("john")
.password("12345") .authorities("read")
.build(); userDetailsManager.createUser(user); return userDetailsManager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
然后,我们添加一个 UserDetailsService 的 bean 来检索用于认证和授权的用户详细信息。为了将其放入 Spring 上下文中,我们使用 @Bean 对其进行注释。为了使本教程简单易懂,我们使用一个 InMemoryUserDetailsManager 实例。对于一个真实世界的应用程序,我们可以使用其他实现,比如 JdbcUserDetailsManager 来连接到数据库等等。为了能够轻松地为本示例创建用户,我们使用扩展 UserDetailsService 的 UserDetailsManager 接口,并且拥有诸如 createUser() 、 updateUser() 等等的方法。然后,使用构建器类创建一个用户。我们给他一个用户名、密码和一个 “read” 权限,暂时如此。然后,使用 createUser() 方法,我们添加新创建的用户并返回 UserDetailsManager 的实例,从而将其放入 Spring 上下文。
为了能够使用我们定义的 UserDetailsService,必须在 Spring 上下文中提供一个 PasswordEncoder bean。同样,为了现在保持简单,我们使用 NoOpPasswordEncoder。NoOpPasswordEncoder 应该不要在生产环境的真实世界应用程序中使用,因为它不安全。NoOpPasswordEncoder 不对密码进行编码,它仅在开发或测试场景或概念验证中有用。我们应该一直使用 Spring Security 提供的其他高度安全选项,其中最流行的是 BCryptPasswordEncoder,我们将在我们一系列的教程中稍后使用它。为了将其放入 Spring 上下文中,我们使用 @Bean 对这个方法进行注释。
然后,我们覆盖 WebSecurityConfigurerAdapter 的 AuthenticationManager bean 方法,它返回 authenticationManagerBean 以将认证管理者放入 Spring 上下文。
现在,为了添加客户端配置,我们添加一个名为 AuthorizationServerConfig 的新配置类,它扩展 Spring Security 中的 AuthorizationServerConfigurerAdapter 类。 AuthorizationServerConfigurerAdapter * class is used to configure the authorization server using the spring security oauth2 module. We annotate this class with @Configuration as well. To add the authorization server functionality to this class we need to add the *@EnableAuthorizationServer 注解以便应用程序可以作为一个授权服务器表现。
package com.tutorial.spring.security.oauth2.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; @Configuration @EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() .withClient("oauthclient1") .secret("oauthsecret1") .scopes("read") .authorizedGrantTypes("password") }
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
}
用于检查 oauth 令牌,Spring Security oauth 暴露两个端点—— /oauth/check_token and /oauth/token_key 。这些端点在缺省情况下受 denyAll() 限制。 tokenKeyAccess() 和 checkTokenAccess() 方法打开这些端点供使用。
我们将 UserConfig 类中配置的 AuthenticationManager bean 作为依赖项在这里自动装配,稍后我们将使用它。
然后,我们覆盖 AuthorizationServerConfigurerAdapter 的 configure() 方法中的两个以提供客户端详细信息服务的一个内存实现。第一个方法使用 ClientDetailsServiceConfigurer 作为参数,顾名思义,允许我们为授权服务器配置客户端。这些客户端表示将能够使用该授权服务器的功能的应用程序。由于这是一个用于学习 OAuth2 实现的基本应用程序,我们暂时将事情保持简单并使用具有以下属性的内存实现——
-
clientId —— 客户端的 id。必需。
-
secret —— 客户端密钥,受信任客户端必需。
-
scope —— 客户端的限制范围,换句话说,客户端权限。如果留空或者未定义,客户端不受到任何范围的限制。
-
authorizedGrantTypes —— 客户端被授权使用的授予类型。授予类型表示客户端从授权服务器获取令牌的方式。我们将使用 “password” 授予类型,因为它是最为简单的类型。稍后,我们将使用另一个授予类型用于另一个用例。
在 “password” 授权授予类型中,用户需要向我们的客户端应用程序提供他的/她的用户名、密码和范围,它然后使用那些凭证以及它的凭证用于我们想要令牌的授权服务器。
我们覆盖的另一个 configure() 方法,使用 AuthorizationServerEndpointsConfigurer 作为参数,用于将 AuthenticationManager 附加到授权服务器配置。
利用这些基本配置,我们的授权服务器可以使用了。让我们继续启动它并使用它。我们将使用 Postman ( h ttps://www.postman.com/downloads/ ) 来发出我们的请求。
使用 STS 时,我们能够启动我们的应用程序并开始在我们的控制台中看到日志。当该应用程序启动后,我们可以在控制台中找到我们的应用程序所公开的 oauth2 端点。在所有这些端点中,我们现在将使用以下令牌 -
/oauth/token – for obtaining the token.
如果我们在这里查看 postman 快照,我们可以注意到一些事情。我们列出它们。
-
URL - 我们的 Spring Boot 应用程序在我们的本机端口 8080 处运行,所以该请求指向 [role="bare"] [role="bare"]http://localhost:8080 。其中下一部分是 /oauth/token,它是我们所知的是 OAuth 所公开用于生成令牌的端点。
-
查询参数 - 由于这是一个“密码”授权授权类型,用户需要为我们的客户端应用程序提供他的用户名、密码和作用域,然后这些凭证与授权服务器的凭证一同使用来生成令牌。
-
客户端授权 - Oauth 系统要求客户端必须被授权才能能够提供该令牌。因此,在授权标头下,我们提供了客户端身份验证信息,即在我们的应用程序中配置的用户名和密码。
我们仔细查看查询参数和授权标头 -
查询参数
客户端凭证
如果一切正确,我们将能够在响应中看到我们生成的令牌以及 200 ok 状态。
响应
我们可以通过输入错误的凭证或不输入凭证来测试我们的服务器,我们将收到一个错误,说明该请求未经授权或具有错误的凭证。
这就是我们的基本 oauth 授权服务器,它使用密码授权类型生成并提供密码。
下一步,我们实现更安全且更常见的 oauth2 身份验证应用程序,即使用授权码授权类型。我们为此目的更新我们当前的应用程序。
授权授权类型在这样的意义上与密码授权类型不同,即用户不必与该客户端应用程序共享他的凭证。他仅与授权服务器共享凭证,作为回报,授权代码被发送到客户端,它使用该代码来验证客户端。它比密码授权类型更安全,因为用户凭证不会与客户端应用程序共享,因此用户的的信息保持安全。客户端应用程序无法访问任何重要的用户信息,除非得到用户的批准。
在一些简单的步骤中,我们便可以在我们的应用程序中设置具有基本 oauth 服务器和授权授权类型的服务器。我们来看看如何执行。
package com.tutorial.spring.security.oauth2.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("oauthclient1")
.secret("oauthsecret1")
.scopes("read") .authorizedGrantTypes("password")
.and() .withClient("oauthclient2") .secret("oauthsecret2")
.scopes("read") .authorizedGrantTypes("authorization_code")
.redirectUris("http://locahost:9090");
}
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
}
我们为该操作添加了一个第二个客户端 oauthclient2,指定新的机密和读取作用域。在该客户端中,我们已将授予类型更改为授权代码。我们还添加了一个重定向 URI,以便授权服务器能够回调客户端。所以,重定向 URI 基本上是客户端的 URI。
现在,我们必须在用户和授权服务器之间建立连接。我们必须为授权服务器设置一个接口,用户可以在其中输入凭证。我们使用 Spring Security 的 formLogin() 实施来实现该功能,同时使用保持简单。我们还要确保所有请求都得到身份验证。
package com.tutorial.spring.security.oauth2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
@SuppressWarnings("deprecation") @Configuration
public class UserConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService() {
UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
UserDetails user = User.withUsername("john")
.password("12345") .authorities("read") .build();
userDetailsManager.createUser(user); return userDetailsManager;
}
@Bean public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override protected void configure(HttpSecurity http) throws Exception {
http.formLogin(); http.authorizeRequests().anyRequest().authenticated();
}
}
这就完成了我们为授权授权类型所做的设置。现在,要测试我们的设置并启动我们的应用程序。我们使用 [role="bare"] [role="bare"]http://localhost:8080/oauth/authorize?response_type=code&client_id=oauthclient2&scope=read 启动我们的浏览器。我们将被重新定向到 Spring Security 的默认表单登录页面。
在此,响应类型代码表示授权服务器将返回访问代码,客户端将使用该代码登录。我们使用用户凭据时,我们将会被问及是否要授予客户端要求的权限,屏幕与下面所示的屏幕类似。
如果我们批准并点击授权,我们就应当看到我们被重定向到给定的重定向网址以及访问代码。在我们的案例中,我们被重定向到 [role="bare"] [role="bare"]http://locahost:9090/?code=7Hibnw ,正如我们在应用程序中指定的。我们现在可以使用 Postman 中的代码作为客户端登录到授权服务器。
正如我们这里所看到的,我们在 URL 中使用了从授权服务器收到的代码,将 grant_type 用作 authorization_code,将 scope 用作 read。我们充当客户端并提供了在我们的应用程序中配置的客户端凭据。在我们做出此请求时,我们收回了我们的 access_token,我们可以进一步使用它。
因此,我们已经看到了如何使用 OAuth 2.0 配置 Spring Security。应用程序非常简单易懂,有助于我们比较容易地理解该过程。我们已经使用了两类授权授权类型,并且已经看到了如何使用它们来为我们的客户端应用程序获取访问令牌。
Spring Security - JWT
Contents
-
JWT Introduction and overview
-
使用 JWT(实用指南)开始使用 Spring Security
JWT Introduction and overview
JSON Web Token or JWT ,正如它通常被称为的那样,是一种开放的 Internet 标准(RFC 7519),用于在各方之间以紧凑的方式安全地传输受信任的信息。令牌包含编码为 JSON 对象并且使用私有密钥或公钥/私钥对进行数字签名的声明。它们是自包含的和可验证的,因为它们是数字签名的。JWT 可以被签名和/或加密。签名令牌验证令牌中包含的声明的完整性,而加密令牌向其他方隐藏声明。
JWT 也可以用于交换信息,尽管它们更常用于授权,因为它们提供了比基于内存随机令牌的会话管理的更多优势。其中最大的优势是能够将身份验证逻辑委托给诸如 AuthO 等第三方服务器。
JWT 令牌分为 3 部分,即标题、有效载荷和签名,格式如下:
[Header].[Payload].[Signature]
-
Header − JWT 令牌的标题包含应用于 JWT 的加密操作列表。这可以是签名技术、关于内容类型的元数据信息等等。标题表示为编码成 base64URL 的 JSON 对象。有效 JWT 标头的示例如下:
{ "alg": "HS256", "typ": "JWT" }
在此,“ alg ”向我们提供了有关所用算法类型的的信息,并且 “typ 给我们该信息的类型。
-
Payload − JWT 的有效载荷部分包含使用令牌要传输的实际数据。此部分也称为 JWT 令牌的“声明”部分。声明可以有三种类型——注册、公有和私有。
-
注册声明是建议的声明但不是强制性声明,例如 iss(签发者)、sub(主体)、aud(受众)和其他声明。
-
公有声明是由那些使用 JWT 的人定义的声明。
-
私有声明或自定义声明是用户定义的声明,创建目的是在相关各方之间共享信息。
有效载荷对象的一个示例可能如下所示。
{ "sub": "12345", "name": "Johnny Hill", "admin": false }
有效载荷对象,如同标题对象一样,也被 base64Url 编码,并且此字符串形成 JWT 的第二部分。
-
Signature − JWT 的签名部分用于验证消息在传输过程中未被更改。如果令牌是由私钥签名的,它还将验证发送者就是它所说的那样。它使用编码的标题、编码的有效载荷、一个密钥和标题中指定的算法创建。签名的一个示例可能是这样的。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
如果我们放入标题、有效载荷和签名,我们就会得到一个如下所示的令牌。
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I
kpvaG4gRG9lIiwiYWRtaW4iOmZhbHNlfQ.gWDlJdpCTIHVYKkJSfAVNUn0ZkAjMxskDDm-5Fhe
WJ7xXgW8k5CllcGk4C9qPrfa1GdqfBrbX_1x1E39JY8BYLobAfAg1fs_Ky8Z7U1oCl6HL63yJq_
wVNBHp49hWzg3-ERxkqiuTv0tIuDOasIdZ5FtBdtIP5LM9Oc1tsuMXQXCGR8GqGf1Hl2qv8MCyn
NZJuVdJKO_L3WGBJouaTpK1u2SEleVFGI2HFvrX_jS2ySzDxoO9KjbydK0LNv_zOI7kWv-gAmA
j-v0mHdJrLbxD7LcZJEGRScCSyITzo6Z59_jG_97oNLFgBKJbh12nvvPibHpUYWmZuHkoGvuy5RLUA
现在,该令牌可以使用 Bearer 架构在 Authorization 标头中使用,如下所示。
Authorization − Bearer <令牌>
使用 JWT 令牌进行授权是其最常见的应用。令牌通常在服务器中生成,然后发送到存储在会话存储或本地存储中的客户端。若要访问受保护的资源,客户端会将 JWT 发送在上面给出的头部中。我们将在下面的部分中看到 Spring Security 中的 JWT 实现。
Getting Started with Spring Security using JWT
我们将要开发的应用程序将使用 JWT 处理基本的用用户身份验证和授权。让我们从前往 start.spring.io 开始,我们将在其中使用以下依赖项创建一个 Maven 应用程序。
-
Spring Web
-
Spring Security
我们生成项目,并在下载后将其解压缩到我们选择的文件夹中。然后,我们可以使用我们选择的任意 IDE。我将使用 Spring Tools Suite 4,因为它最适合 Spring 应用程序。
除了上述依赖项之外,我们还要从 Maven 中央存储库中包含来自 io.jsonwebtoken 的 jwt 依赖项,因为它不包含在 Spring 转换器中。此依赖项处理了涉及 JWT 的所有操作,包括构建令牌、解析它以索取所有权等等。
<dependency>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
我们的 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>jwtbasic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jwtbasic</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>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</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>
在设置完项目后,我们准备创建公开获取端点的 Hello Controller 控制器类。
package com.spring.security.jwtbasic.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
现在,我们准备创建一个名为 config 的包,在其中添加扩展 Spring Security 的 WebSecurityConfigurerAdapter 类的配置类。这将为我们提供项目配置和应用程序安全所需的所有必需函数和定义。目前,我们通过实现生成相同的方法提供 BcryptPasswordEncoder 实例。我们使用 @Bean 来注释该方法,以将其添加到我们的 Spring Context。
package com.spring.security.jwtbasic.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.spring.security.jwtbasic.jwtutils.JwtAuthenticationEntryPoint;
import com.spring.security.jwtbasic.jwtutils.JwtFilter;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
JWT 包括一个我们将在 application.properties 文件中定义的秘密,如下所示。
secret=somerandomsecret
现在,让我们创建一个名为 jwtutils 的包。此包将包含与 JWT 操作相关的所有类和接口,其中包括。
-
Generating token
-
Validating token
-
Checking the signature
-
Verifying claims and permissions
在此包中,我们创建第一个类,称为 Token Manager。此类将负责使用 io.jsonwebtoken.Jwts 创建和验证令牌。
package com.spring.security.jwtbasic.jwtutils;
import java.io.Serializable;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@Component
public class TokenManager implements Serializable {
/**
*
*/
private static final long serialVersionUID = 7008375124389347049L; public static final long TOKEN_VALIDITY = 10 * 60 * 60; @Value("${secret}")
private String jwtSecret;
public String generateJwtToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder().setClaims(claims).setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, jwtSecret).compact();
}
public Boolean validateJwtToken(String token, UserDetails userDetails) {
String username = getUsernameFromToken(token);
Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
Boolean isTokenExpired = claims.getExpiration().before(new Date());
return (username.equals(userDetails.getUsername()) && !isTokenExpired);
}
public String getUsernameFromToken(String token) {
final Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
return claims.getSubject();
}
}
在此处,由于所有令牌都应具有到期日期,因此我们从令牌有效期常量开始。在此,我们希望我们的令牌在生成后 10 分钟内有效。我们会在生成令牌时使用此值。然后,使用 @Value 注释从 application.properties 文件中将我们的私钥的值提取到我们的 jwtSecret 字段中。
我们在此有两种方法 −
-
generateJwtToken() − 此方法用于在用户成功验证后生成令牌。若要在此处创建令牌,我们使用用户名、令牌的签发日期和令牌的到期日期。这将形成令牌的有效负载部分或我们之前讨论过的所有权。我们使用 Jwts 的 builder() 方法来生成令牌。此方法返回一个新的 JwtBuilder 实例,可用于创建紧凑的 JWT 序列化字符串。
若要设置所有权,我们使用 setClaims() 方法,然后设置每个所有权。对此令牌,我们已设置 setSubject(username)、颁发日期和到期日期。我们还可以输入自定义所有权,就像我们上面讨论的那样。这可能是我们想要的任何值,其中可能包括用户角色、用户权限等等。
然后,我们设置令牌签名部分。这是使用 signWith() 方法完成的,我们设置我们希望使用的哈希算法和密钥。然后,我们使用 compact() 方法,该方法将生成 JWT 并将其序列化为根据 JWT 紧凑序列化规则生成的紧凑型 URL 安全字符串。
-
validateJwtToken() − 现在已解决令牌的生成问题,我们应重点关注令牌作为请求的一部分时验证令牌的过程。验证令牌意味着验证请求是一个经过身份验证的请求,并且令牌是生成并发送给用户的令牌。在此,我们需要解析令牌以获取所有权,例如用户名、角色、权限、有效期等。
若要验证令牌,我们首先需要解析它。这是通过使用 Jwts 的 parser() 方法来完成的。然后,我们需要设置用于生成令牌的加密密钥,然后使用 parseClaimsJws() 方法对令牌进行解析,以基于生成器的当前配置状态来解析紧凑型序列化 JWS 字符串,并返回结果 Claims JWS 实例。然后,使用 getBody() 方法来返回在生成令牌时使用的 allOwnership 实例。
从获取的 allOwnership 实例中,我们提取主体和到期日期来验证令牌的有效性。用户名应该是用户的用户名,并且令牌不应过期。如果满足这两个条件,我们将返回 true,表示令牌有效。
下面我们创建的类是 JwtUserDetailsService 。该类将扩展 Spring 安全的 UserDetailsService ,我们还将实现 loadUserByUsername() 方法,如下所示 −
package com.spring.security.jwtbasic.jwtutils;
import java.util.ArrayList;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class JwtUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("randomuser123".equals(username)) {
return new User("randomuser123",
"$2a$10$slYQmyNdGzTn7ZLBXBChFOC9f6kFjAqPhccnP6DxlWXx2lPk1C3G6",
new ArrayList<>());
} else {
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
}
这里,由于这是一个用于演示 JWT 身份验证的简单应用程序,所以我们使用了一组用户详情,而不是使用数据库。我们已经将用户名指定为“ randomuser123 ”并对密码进行编码,将其指定为“$2a$10$slYQmyNdGzTn7ZLBXBChFOC9f6kFjAqPhccnP6DxlWXx2lPk1C3G6”,以方便我们。
接下来,我们为请求和响应模型创建类。这些模型决定了身份验证的请求和响应格式。下面给出的第一个快照是请求模型。我们将会接受两个属性——请求中的用户名和密码。
package com.spring.security.jwtbasic.jwtutils.models;
import java.io.Serializable;
public class JwtRequestModel implements Serializable {
/**
*
*/
private static final long serialVersionUID = 2636936156391265891L;
private String username;
private String password;
public JwtRequestModel() {
}
public JwtRequestModel(String username, String password) {
super();
this.username = username; this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
以下是成功身份验证的响应模型的代码。我们将会在成功身份验证后将令牌发送回用户。
package com.spring.security.jwtbasic.jwtutils.models;
import java.io.Serializable;
public class JwtResponseModel implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private final String token;
public JwtResponseModel(String token) {
this.token = token;
}
public String getToken() {
return token;
}
}
对于身份验证,我们创建一个控制器,如下所示。
package com.spring.security.jwtbasic.jwtutils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.spring.security.jwtbasic.jwtutils.models.JwtRequestModel;
import com.spring.security.jwtbasic.jwtutils.models.JwtResponseModel;
@RestController
@CrossOrigin
public class JwtController {
@Autowired
private JwtUserDetailsService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenManager tokenManager;
@PostMapping("/login")
public ResponseEntity<> createToken(@RequestBody JwtRequestModel
request) throws Exception {
try {
authenticationManager.authenticate(
new
UsernamePasswordAuthenticationToken(request.getUsername(),
request.getPassword())
);
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
final String jwtToken = tokenManager.generateJwtToken(userDetails);
return ResponseEntity.ok(new JwtResponseModel(jwtToken));
}
}
如果我们浏览代码,会发现我们自动装配了三个依赖关系,即 JwtUserDetailsService 、AuthenticationManager 和 TokenManager 。虽然我们已经看到了上述 JwtUserDetailsService 和 TokenManager 类的实现,但身份验证管理器 bean 是我们将在 WebSecurityConfig 类中创建的 bean。
AuthenticationManager 类将负责我们的身份验证。我们将使用 UsernamePasswordAuthenticationToken 模型对请求进行身份验证。如果身份验证成功,我们将会为用户生成一个 JWT,该 JWT 可以发送在后续请求的 Authorization 标头中以获取任何资源。
我们使用 JwtUserDetailsService 类的 loadUserByUsername() 方法和 TokenManager 类的 generateJwtToken() 。
生成的 JWT 在身份验证成功后作为响应发送给用户,如上所述。
现在是时候创建 Filter。过滤器类将用于跟踪我们的请求并检测它们是否在标头中包含有效的令牌。如果令牌有效,我们将让请求继续;否则,我们会发送 401 错误 (Unauthorized) 。
package com.spring.security.jwtbasic.jwtutils;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.ExpiredJwtException;
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JwtUserDetailsService userDetailsService;
@Autowired
private TokenManager tokenManager;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String tokenHeader = request.getHeader("Authorization");
String username = null;
String token = null;
if (tokenHeader != null && tokenHeader.startsWith("Bearer ")) {
token = tokenHeader.substring(7);
try {
username = tokenManager.getUsernameFromToken(token);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
System.out.println("JWT Token has expired");
}
} else {
System.out.println("Bearer String not found in token");
}
if (null != username &&SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (tokenManager.validateJwtToken(token, userDetails)) {
UsernamePasswordAuthenticationToken
authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null,
userDetails.getAuthorities());
authenticationToken.setDetails(new
WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(request, response);
}
}
我们可以看到,我们在其中也自动装配了 JwtUserDetailsService 和 TokenManager 类。我们扩展了 SpringSecurity 的 OncePerRequestFilter ,该 OncePerRequestFilter 确保对每个请求运行该过滤器。我们已经向 OncePerRequestFilter 类的重写方法 doFilterInternal() 提供了我们的实现。
这里的方法从标头中提取令牌,并借助我们的 TokenManager 类的 validateJwtToken() 方法对其进行验证。在验证过程中,它会检查用户名和到期日期。如果这两个值都已验证,我们会在 Spring Security 上下文中保存身份验证,并让代码继续进行过滤器链中的下一个过滤器。如果任何验证失败,或者令牌有问题,或者找不到令牌,我们将抛出相应的异常,并在阻止请求继续的同时发送回适当的响应。
为我们的请求创建了过滤器后,现在我们创建 JwtAutheticationEntryPoint 类。该类扩展 Spring 的 AutenticationEntryPoint 类,并以发送回客户端的错误代码 401 拒绝每个未经身份验证的请求。我们重写了 AuthenticationEntryPoint 类的 commence() 方法来实现这一点。
package com.spring.security.jwtbasic.jwtutils;
import java.io.IOException;
import java.io.Serializable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint,
Serializable {
@Override
public void commence(HttpServletRequest request, HttpServletResponse
response,
AuthenticationException authException) throws
IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"Unauthorized");
}
}
现在,让我们回到 WebSecurityConfig 类,完成剩余的配置。如果我们记得,我们会在 Jwt 控制器类中用到 AuthenticationManager bean,并将刚才创建的过滤器添加到我们的配置中。我们还会配置需要身份验证的请求和不需要身份验证的请求。我们还要向请求添加 AuthenticationEntryPoint ,以发送回 401 错误响应。由于在使用 jwt 时我们不需要维护会话变量,因此我们可以使会话 STATELESS 。
package com.spring.security.jwtbasic.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.spring.security.jwtbasic.jwtutils.JwtAuthenticationEntryPoint;
import com.spring.security.jwtbasic.jwtutils.JwtFilter;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtFilter filter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws
Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
}
我们可以看到,我们已经完成了所有这些,现在我们的应用程序可以运行了。让我们启动应用程序,并使用 postman 来执行我们的请求。
这里我们已经发出了第一个请求以获取令牌,我们可以看到,在提供了正确的用户名/密码组合后我们就收到了令牌。
现在,在标头中使用该令牌,让我们调用 /hello 端点。
我们可以看到,由于请求已通过身份验证,所以我们收到了期望的响应。现在,如果我们篡改令牌或不发送 Authorization 标头,我们将收到应用程序中配置的 401 错误。这可确保使用 JWT 保护我们的请求。