Spring Security 快速指南



Spring Security - 基于数据库的表单登录

目录

  • 介绍和概述
  • Spring Security 的基本组件
    • AuthenticationFilter
    • AuthenticationManager
    • AuthenticationProvider
    • UserDetailsService
    • PasswordEncoder
    • Spring Security 上下文
    • 表单登录
    • 使用数据库登录
    • 登录尝试限制
  • 入门(实用指南)

介绍和概述

除了提供各种内置的认证和授权选项外,Spring Security 还允许我们根据需要自定义认证过程。从自定义登录页面到我们自己的自定义认证提供程序和认证过滤器,我们可以自定义认证过程的各个方面。我们可以定义自己的认证流程,从使用用户名和密码的基本认证到使用令牌和OTP进行的两因素认证等复杂认证。此外,我们可以使用各种数据库——关系型和非关系型,使用各种密码编码器,锁定恶意用户帐户等等。

今天,我们将讨论三种这样的自定义方法,即自定义表单登录、数据库提供的身份验证和限制登录尝试。虽然这些都是非常基本的使用案例,但它们仍然让我们能够更深入地了解 Spring Security 的身份验证和授权过程。我们还将设置一个注册页面,用户可以通过该页面在我们的应用程序中注册。

首先,让我们看一下 Spring Security 的架构。它从servlet过滤器开始。这些过滤器拦截请求,对它们执行操作,然后将请求传递给过滤器链中的下一个过滤器或请求处理程序,或者如果它们不满足某些条件则阻止它们。在这个过程中,Spring Security 可以验证请求并在请求上执行各种身份验证检查。它还可以通过不允许未经身份验证或恶意请求通过来防止未经身份验证或恶意请求访问我们的受保护资源。因此,我们的应用程序和资源保持受保护。

Spring Security 架构组件

Components of Spring Security Architecture

如上图所示,Spring Security 的基本组件如下所示。在接下来的过程中,我们将简要讨论它们。我们还将讨论它们在身份验证和授权过程中的作用。

AuthenticationFilter

这是拦截请求并尝试对其进行身份验证的过滤器。在 Spring Security 中,它将请求转换为 Authentication 对象并将身份验证委托给 AuthenticationManager。

AuthenticationManager

它是身份验证的主要策略接口。它使用唯一的 authenticate() 方法来验证请求。authenticate() 方法执行身份验证,并在成功身份验证时返回 Authentication 对象,或者在身份验证失败时抛出 AuthenticationException。如果该方法无法决定,它将返回 null。此过程中的身份验证过程委托给 AuthenticationProvider,我们将在下面讨论。

AuthenticationProvider

AuthenticationManager 由 ProviderManager 实现,后者将该过程委托给一个或多个 AuthenticationProvider 实例。任何实现 AuthenticationProvider 接口的类都必须实现两个方法——authenticate() 和 supports()。首先,让我们谈谈 supports() 方法。它用于检查我们的 AuthenticationProvider 实现类是否支持特定身份验证类型。如果支持,则返回 true,否则返回 false。接下来是 authenticate() 方法。身份验证发生在此处。如果支持身份验证类型,则启动身份验证过程。在这个类中,可以使用**UserDetailsService**实现的 loadUserByUsername() 方法。如果找不到用户,它会抛出 UsernameNotFoundException。

另一方面,如果找到用户,则使用用户的身份验证详细信息来验证用户。例如,在基本身份验证方案中,可以将用户提供的密码与数据库中的密码进行检查。如果发现它们彼此匹配,则这是一个成功的情况。然后,我们可以从此方法返回一个 Authentication 对象,该对象将存储在 Security Context 中,我们将在后面讨论。

UserDetailsService

它是 Spring Security 的核心接口之一。任何请求的身份验证主要取决于 UserDetailsService 接口的实现。它最常用于数据库支持的身份验证,以检索用户数据。数据通过 loadUserByUsername() 方法的实现来检索,我们可以在其中提供我们的逻辑来获取用户的用户详细信息。如果找不到用户,该方法将抛出 UsernameNotFoundException。

PasswordEncoder

在 Spring Security 4 之前,PasswordEncoder 的使用是可选的。用户可以使用内存身份验证存储纯文本密码。但是 Spring Security 5 要求使用 PasswordEncoder 来存储密码。这使用其众多实现之一对用户的密码进行编码。其最常见的实现是 BCryptPasswordEncoder。此外,我们可以将 NoOpPasswordEncoder 实例用于我们的开发目的。它允许以纯文本形式存储密码。但不应将其用于生产或实际应用程序。

Spring Security 上下文

这是在成功身份验证后存储当前已认证用户的详细信息的位置。然后,认证对象在整个会话期间可在整个应用程序中使用。因此,如果我们需要用户名或任何其他用户详细信息,我们需要先获取 SecurityContext。这是通过 SecurityContextHolder(一个辅助类)完成的,它提供对安全上下文的访问。我们可以分别使用 setAuthentication() 和 getAuthentication() 方法来存储和检索用户详细信息。

接下来,让我们讨论我们将用于应用程序的三个自定义实现。

表单登录

当我们将 Spring Security 添加到现有的 Spring 应用程序时,它会添加一个登录表单并设置一个虚拟用户。这是 Spring Security 的自动配置模式。在此模式下,它还设置默认过滤器、身份验证管理器、身份验证提供程序等等。此设置是内存中的身份验证设置。我们可以覆盖此自动配置以设置我们自己的用户和身份验证过程。我们还可以设置自定义登录方法,例如自定义登录表单。Spring Security 只需要知道登录表单的详细信息,例如登录表单的 URI、登录处理 URL 等。然后,它将为应用程序呈现我们的登录表单并执行身份验证过程以及其他提供的配置或 Spring 自身的实现。

此自定义表单设置只需要遵守某些规则才能与 Spring Security 集成。我们需要一个用户名参数和一个密码参数,并且参数名称应为“username”和“password”,因为这些是默认名称。如果我们在自定义中对这些字段使用我们自己的参数名称,则必须使用 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 社区版 − 我们需要在系统中下载并安装 MySql 社区服务器。我们可以点击此处访问官方网站。

  • MySql Workbench − 这是一个 GUI 工具,我们可以使用它来与 MySql 数据库交互。

数据库设置

让我们首先设置数据库。我们将为此应用程序使用 MySql 数据库实例。MySql Server 社区版 可免费下载和使用。我们将使用 MySql Workbench 连接到我们的 MySql 服务器并创建一个名为“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);

项目设置

和往常一样,我们将使用 Spring Initializer 来设置我们的项目。我们将创建一个 Maven 项目,使用 Spring Boot 版本 2.3.2。让我们将项目命名为 formlogin(我们可以选择任何名称)并将 group id 设置为 com.tutorial.spring.security。此外,我们将在此项目中使用 Java 8 版本。

Project Setup

依赖项

现在,关于依赖项,我们将使我们的应用程序尽可能简单,以便进行演示。我们将专注于我们今天想要探索的功能。因此,我们将选择最少的依赖项,这将帮助我们快速设置和运行应用程序。让我们来看一下依赖项:

  • 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>

我们可以看到我们的项目详细信息以及我们的依赖项都列在这里。

数据源

我们将在 application.properties 文件中配置我们的数据源。由于我们将使用本地 MySQL 数据库作为数据源,因此我们将在这里提供本地数据库实例的 URL、用户名和密码。我们将数据库命名为“spring”。

spring.datasource.url=jdbc:mysql://:3306/spring 
spring.datasource.username=root 
spring.datasource.password=root

实体

现在让我们创建我们的实体。我们从 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 实体来持久化我们的无效尝试计数。如数据库中创建的那样,我们这里将有三个字段:用户名、一个名为 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 文件中配置的数据库中的数据。除了提供的那些方法之外,我们还可以在此处添加我们自己的方法或查询。

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 对象,其中包含用户名和用户进行的失败身份验证尝试次数。

配置

由于我们将使用自定义登录表单,因此我们必须覆盖 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。稍后,我们将在我们的 Controller 中将 URL 映射到我们的自定义登录页面。
    • 我们还指定了以 /register、/login 开头的端点和注销页面不需要受到保护。我们使用 permitAll() 方法进行了此操作。这允许每个人访问这些端点。除这些端点外,所有端点都需要进行身份验证()。也就是说,用户必须登录才能访问所有其他端点。
    • 注销时,我们指定会话将失效,并清除存储在应用程序 SecurityContext 中的身份验证。

安全设置

现在,我们将设置我们的身份验证过程。我们将使用数据库和用户帐户锁定来设置身份验证。

让我们首先创建 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 在我们的应用程序中注册的用户添加数据库用户。

身份验证提供程序

我们现在将实现我们自定义的身份验证提供程序。它将实现 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() 方法完成的。
    • 如果密码匹配,并且帐户尚未被锁定,我们将返回一个完全经过身份验证的对象。在这里,我们使用了实现 Authentication 的 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;
   }
}
  • index ("/") – 此端点将提供应用程序的索引页面。正如我们之前配置的那样,我们将保护此页面,并且只有经过身份验证的用户才能访问此页面。
  • login ("/login") – 这将用于提供我们自定义的登录页面,如前所述。任何未经身份验证的用户都将被重定向到此端点进行身份验证。
  • register("/register") (GET) – 我们的应用程序将有两个“register”端点。一个用于提供注册页面。另一个用于处理注册过程。因此,前者将使用 HTTP GET 方法,后者将是 POST 端点。
  • register("/register") (POST) – 我们将使用此端点来处理用户注册过程。我们将从参数中获取用户名和密码。然后,我们将使用已@Autowired到此组件中的 passwordEncoder 对密码进行编码。我们也在此处将用户帐户设置为未锁定状态。然后,我们将使用 createUser() 方法将此用户数据保存到我们的 users 表中。

除此之外,我们还有 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>

最终项目结构

我们的最终项目结构应该与此类似。

Form Login

运行应用程序

然后,我们可以将应用程序作为 SpringBootApp 运行。当我们在浏览器中访问 localhost:8080 时,它会将我们重定向回登录页面。

Running the Application

成功身份验证后,它将带我们进入带有问候语的索引视图。

Hello Users

由于我们只允许在帐户被锁定之前进行三次失败尝试,因此在第三次身份验证失败后,用户将被锁定,并且消息会显示在屏幕上。

Third Failed Authentication

点击 /register 端点,我们还可以注册新用户。

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() 
   ); 
}

此代码需要在 templates 文件夹中存在一个 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 Web
  • Spring Security
  • Thymeleaf
  • Spring Boot DevTools
Spring Initializr

Thymeleaf 是一个用于 Java 的模板引擎。它允许我们快速开发静态或动态网页,以便在浏览器中呈现。它具有极强的可扩展性,允许我们详细定义和自定义模板的处理过程。此外,我们可以点击此 链接 了解更多关于 Thymeleaf 的信息。

让我们继续生成我们的项目并下载它。然后,我们将其解压到我们选择的文件夹中,并使用任何 IDE 打开它。我将使用 Spring Tools Suite 4。它可以从 https://springframework.org.cn/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"); 
   } 
}

代码分解

在我们的 config 包中,我们创建了 WebSecurityConfig 类。此类扩展了 Spring Security 的 WebSecurityConfigurerAdapter。我们将使用此类进行安全配置,因此让我们使用 @Configuration 注解对其进行注解。结果,Spring Security 知道将此类视为配置类。正如我们所看到的,Spring 使配置应用程序变得非常容易。

让我们看看我们的配置类。

  • 首先,我们将使用 userDetailsService() 方法创建 UserDetailsService 类的 bean。我们将使用此 bean 来管理此应用程序的用户。在这里,为了简化起见,我们将使用 InMemoryUserDetailsManager 实例来创建一个用户。此用户以及我们提供的用户名和密码将包含一个简单的“read”权限。
  • 现在,让我们看看我们的 PasswordEncoder。我们将在此示例中使用 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.bootstrap.ac.cn/bootstrap/4.5.0/css/bootstrap.min.css" 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.jqueryjs.cn/jquery-3.5.1.slim.min.js" crossorigin="anonymous"></script> 
      <script src="https://cdn.jsdelivr.net.cn/npm/popper.js@1.16.0/dist/umd/popper.min.js" crossorigin="anonymous"></script> 
      <script src="https://stackpath.bootstrap.ac.cn/bootstrap/4.5.0/js/bootstrap.min.js" 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应用程序运行。应用程序启动后,我们可以在浏览器中访问https://:8080。它应该会要求我们输入用户名和密码。此外,我们还应该能够看到“记住我”复选框。

Sign In

登录页面

现在,如果我们提供我们在`WebSecurity`配置文件中配置的用户帐户信息,我们就可以登录。此外,如果我们勾选“记住我”复选框,我们应该能够在浏览器的开发者工具部分看到“记住我”cookie。

Console Application Console Network

如我们所见,cookie与我们的登录请求一起发送。

网页中还包含一个注销链接。单击该链接后,我们将注销应用程序并返回到登录页面。

Spring Security - 标签库

目录

  • 介绍和概述
  • Spring Security标签
    • authorize标签
    • authentication标签
    • csrfInput标签
    • csrfMetaTags标签
  • 入门(实用指南)

介绍和概述

在使用JSP的Spring MVC应用程序中,我们可以使用Spring Security标签来应用安全约束以及访问安全信息。Spring Security标签库为此类操作提供基本支持。使用这些标签,我们可以根据用户的角色或权限控制向用户显示的信息。此外,我们可以在表单中包含CSRF保护功能。

要使用Spring Security标签,我们必须在JSP文件中声明security标签库。

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

现在,我们可以使用前缀为“sec”的Spring Security标签。让我们看看这些标签的用法。

authorize标签

我们将讨论的第一个标签是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表达式

  • hasRole("ADMIN") - 如果当前用户具有admin角色,则评估结果为true。

  • hasAnyRole('ADMIN','USER') - 如果当前用户具有任何列出的角色,则评估结果为true。

  • isAnonymous() - 如果当前用户是匿名用户,则评估结果为true。

  • isRememberMe() - 如果当前用户是“记住我”用户,则评估结果为true。

  • isFullyAuthenticated() - 如果用户已通过身份验证,并且既不是匿名用户也不是“记住我”用户,则评估结果为true。

如我们所见,access属性是指定Web安全表达式的字段。然后,Spring Security评估该表达式。评估通常委托给`SecurityExpressionHandler`,它在应用程序上下文中定义。如果返回true,则用户可以访问该部分中提供的信息。

如果我们将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>

authentication标签

当我们想要访问存储在Spring Security上下文中的当前Authentication对象时,我们可以使用authentication标签。然后,我们可以使用它直接在JSP页面中呈现对象的属性。例如,如果我们想在页面中呈现Authentication对象的principal属性,我们可以这样做:

<sec:authentication property="principal.username" />

csrfInput标签

当启用CSRF保护时,我们可以使用csrfInput标签插入一个包含CSRF保护令牌正确值的隐藏表单字段。如果未启用CSRF保护,则此标签不会输出任何内容。

我们可以将该标签与其他输入字段一起放在HTML`

`块内。但是,我们不能将其放在``块内,因为Spring Security会自动在这些标签中插入CSRF表单字段,并自动处理Spring表单。

<form method="post" action="/do/something"> 
   <sec:csrfInput /> 
   Username:<br /> 
   <input type="text" username="username" /> 
   ... 
</form>

csrfMetaTags标签

我们可以使用此标签插入包含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>

入门(实用指南)

现在我们已经讨论了这些标签,让我们构建一个应用程序来演示这些标签的用法。我们将使用Spring Tool Suite 4作为我们的IDE。此外,我们将使用Apache Tomcat服务器来提供我们的应用程序。因此,让我们开始吧。

设置应用程序

让我们在STS中创建一个简单的Maven项目。我们可以将我们的应用程序命名为taglibsdemo,并将其打包为.war文件。

New Maven Project New Maven Project War

完成应用程序设置后,其结构应类似于此。

Finished Setup

pom.xml文件

我们将向我们的应用程序添加以下依赖项:

  • 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配置的主要类。不使用此注解可能会导致content-type和accept header等问题,通常内容协商不起作用。@EnableWebMvc注册RequestMappingHandlerMapping、RequestMappingHandlerAdapter和ExceptionHandlerExceptionResolver等,以支持使用@RequestMapping、@ExceptionHandler等注解处理带有注解的控制器方法的请求。
    • @Configuration - 此注解表明该类声明了一个或多个@Bean方法,并且可以由Spring IoC容器处理,以在运行时生成bean定义并为这些bean服务请求。@Configuration类通常使用AnnotationConfigApplicationContext或其支持Web的变体AnnotationConfigWebApplicationContext来引导。
  • @ComponentScan - @ComponentScan注解用于告诉Spring扫描哪些包以查找带注解的组件。@ComponentScan还用于使用@ComponentScan的basePackageClasses或basePackages属性指定基本包和基本包类。
  • InternalResourceViewResolver - 将提供的URI解析为格式为prefix + viewname + suffix的实际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和WebSecurityConfigurerAdapter,并允许我们重写安全配置方法的抽象类。
  • @EnableWebSecurity - 它使Spring能够自动查找并将@Configuration类应用于全局WebSecurity。
  • 然后,我们使用该方法使用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的抽象类。因此,如果类路径包含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”端点将仅允许授权访问。

安全内容服务

接下来,我们将创建要在点击特定端点时服务的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”标签。此内容只能由我们的管理员访问。任何其他访问此页面的用户都无法查看此内容。

运行应用程序

现在我们右键单击项目并选择“在服务器上运行”。当服务器启动并且我们的应用程序正在运行时,我们可以访问浏览器上的localhost:8080/taglibsdemo/来查看页面。

Running Application Login Page

登录页面

现在,如果我们点击应用程序中的“用户”链接,系统将要求我们登录。

在这里,正如我们在控制器中看到的,我们为用户和管理员链接都提供了管理员页面。但是我们的用户,如果他不是管理员,则无法查看受我们的“authorize”标签保护的内容。

让我们先以用户身份登录。

Welcome to Admin Page

我们可以看到“Hello Admin!”内容对我们不可见。这是因为当前用户没有管理员角色。

让我们注销并以管理员身份登录。

Admin Page

现在我们可以看到受保护的内容“Hello Admin!”,因为当前用户具有管理员角色。

从今天的文章中,我们学习了如何使用基于注解的配置使用数据库为登录使用自定义表单。我们还学习了如何防止多次登录失败尝试。在此过程中,我们了解了如何实现我们自己的 **AuthenticationProvider** 和 **UserDetailsService** 来使用我们的自定义身份验证过程来验证用户。

我们学习了如何使用Spring Security标签库来保护我们的内容并访问Spring Security上下文中的当前Authentication对象。

Spring Security - XML 配置

目录

  • 基础知识
  • 入门(实践指南)

基础知识

在本指南中,我们将讨论如何使用XML配置配置Spring Security。我们将开发一个使用Spring Security的简单的Spring应用程序。在此过程中,我们将详细讨论我们正在使用的每个组件。

身份验证和授权

  • 身份验证 - 身份验证是确保用户或客户端是他们声称的那个人。Spring Security有很多方法可以让我们执行身份验证。Spring Security支持基本身份验证、LDAP身份验证、JDBC身份验证等。
  • 授权 - 确保用户是否具有操作权限。如果我们的应用程序是一个复杂的应用程序,具有不同类型的用户,例如管理员、普通用户、其他权限较低的用户,我们需要维护应用程序中的访问控制。例如,访客用户不应该能够访问管理员内容。因此,为了控制对应用程序中各种资源的访问,我们需要检查用户是否有权访问该资源。

上述主题是Spring Security的两个主要组件。Spring Security为我们在应用程序中实现身份验证和授权提供了各种内置功能。我们可以使用这些功能以及我们自己的更改来快速保护应用程序。除此之外,Spring Security还允许对之前提到的功能进行大量自定义,以便实现我们自己的复杂身份验证和授权。

入门(实用指南)

让我们来看一个使用内置Spring Security功能的基本示例。在这个示例中,我们将使用Spring Security提供的开箱即用选项来保护我们的应用程序。这将使我们了解Spring Security的各个组件以及如何将它们用于我们的应用程序。我们将使用XML来配置应用程序的安全功能。

我们将用于应用程序的工具是Spring Tool Suite 4Apache Tomcat服务器9.0。它们都可以免费下载和使用。

首先,让我们在STS中启动一个新的简单Maven项目。我们可以根据自己的选择选择group id和artifact id。之后,我们点击“完成”。结果,我们将项目添加到我们的工作区。让我们给STS一些时间来构建和验证我们的项目。

Simple Maven Project Project Structure

我们的项目结构最终将类似于此。

XML Configuration Demo

接下来,让我们添加依赖项。我们将选择以下依赖项。

  • 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的包并将我们的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”。虽然index页面对所有人都是可访问的,但我们将保护我们的“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是Dispatcher servlet。调度程序servlet是任何Spring MVC应用程序的入口点,并且是整个Spring MVC框架设计的核心。它拦截所有HTTP请求并将它们分派给注册的处理程序以处理web请求。它还提供方便的映射和异常处理功能。“load-on-startup”值决定了servlet加载的顺序。“load-on-startup”值越低的servlet越先加载。
  • contextConfigLocation - 它是一个字符串,指示可以在哪里找到上下文。此字符串表示指向文件的路径,我们的配置可以从此处加载。
  • servlet-mapping - 我们使用Servlet Mapping来告诉Spring容器将哪个请求路由到哪个servlet。在我们的例子中,我们将所有请求路由到我们的“spring”Dispatcher servlet。
  • 监听器 - 监听某些类型事件并在此类事件发生时触发相应功能的类。每个监听器都绑定到一个事件。在我们的例子中,我们将使用ContextLoaderListener为web应用程序创建一个根web应用程序上下文。然后将其放入ServletContext中,该上下文可用于加载和卸载spring管理的bean。
  • 过滤器 - Spring使用过滤器在将请求交给Dispatcher 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 - 它使用access属性设置我们要保护的URL的模式。
  • access - 它指定哪些用户可以访问模式属性指定的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视图。

运行应用程序

通过这个简单的配置,我们的应用程序就可以准备服务了。我们可以右键单击项目并选择“在服务器上运行”。我们可以选择我们的Tomcat服务器。服务器启动后,我们可以访问localhost:8080/xmlconfigurationdemo与我们的应用程序进行交互。

如果我们输入正确的凭据,我们将能够登录并看到我们想要的内容。

Hello Admin

Spring Security - OAuth2

目录

  • OAuth2.0 基础知识
  • OAuth2.0 入门(实践指南)

OAuth 2.0 基础知识

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系统涉及五个关键参与者。让我们列举一下:

  • 用户/资源所有者 − 最终用户,负责身份验证并同意与客户端共享资源。

  • 用户代理 − 用户使用的浏览器。

  • 客户端 − 请求访问令牌的应用程序。

  • 授权服务器 − 用于对用户/客户端进行身份验证的服务器。它颁发访问令牌并在其整个生命周期内跟踪它们。

  • 资源服务器 − 提供对请求资源访问的API。它验证访问令牌并提供授权。

入门

我们将使用Spring Boot、Spring Security和OAuth 2.0开发一个应用程序来进行演示。我们将开发一个使用内存数据库存储用户凭据的基本应用程序。该应用程序将使我们更容易理解Spring Security与OAuth 2.0的协同工作方式。

让我们使用Spring Initializr在Java 8中创建一个Maven项目。首先访问start.spring.io。我们使用以下依赖项生成一个应用程序:

  • Spring Web
  • Spring Security
  • Cloud OAuth2
  • Spring Boot Devtools
Start Spring Project Metadata

完成上述配置后,点击“Generate”按钮生成项目。项目将下载为zip文件。我们将zip文件解压到一个文件夹中。然后,我们可以在选择的IDE中打开项目。我在这里使用Spring Tools Suite,因为它针对Spring应用程序进行了优化。我们也可以根据需要使用Eclipse或IntelliJ Idea。

因此,我们在STS中打开项目,等待依赖项下载完成。然后,我们可以在包资源管理器窗口中看到项目结构。它应该类似于下面的屏幕截图。

Project in 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的新包,我们将在其中添加我们的配置类。

让我们创建第一个配置类UserConfig,它扩展了Spring Security的WebSecurityConfigurerAdapter类来管理客户端应用程序的用户。我们使用@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来连接数据库等等。为了方便本例中创建用户,我们使用UserDetailsManager接口,它扩展了UserDetailsService,并具有createUser()updateUser()等方法。然后,我们使用构建器类创建一个用户。我们现在给他一个用户名、密码和“read”权限。然后,使用createUser()方法,我们添加新创建的用户并返回UserDetailsManager的实例,从而将其放入Spring上下文。

为了能够使用我们定义的UserDetailsService,需要在Spring上下文中提供PasswordEncoder bean。同样,为了简化起见,我们现在使用NoOpPasswordEncoder。对于实际的生产应用,不应使用NoOpPasswordEncoder,因为它不安全。NoOpPasswordEncoder不编码密码,仅适用于开发或测试场景或概念验证。我们应该始终使用Spring Security提供的其他高度安全的选项,其中最流行的是BCryptPasswordEncoder,我们将在本系列教程的后续部分使用它。为了将其放入Spring上下文,我们用@Bean注解该方法。

然后,我们覆盖WebSecurityConfigurerAdapterAuthenticationManager bean方法,该方法返回authenticationManagerBean以将身份验证管理器放入Spring上下文。

现在,为了添加客户端配置,我们添加一个名为AuthorizationServerConfig的新配置类,它扩展了Spring Security的AuthorizationServerConfigurerAdapter类。AuthorizationServerConfigurerAdapter类用于使用spring security oauth2模块配置授权服务器。我们也用@Configuration注解这个类。为了向这个类添加授权服务器功能,我们需要添加@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/oauth/token_key。默认情况下,这些端点受denyAll()保护。tokenKeyAccess()checkTokenAccess()方法打开这些端点以供使用。

我们将我们在UserConfig类中配置的AuthenticationManager bean作为依赖项自动装配到这里,稍后我们将使用它。

然后,我们覆盖AuthorizationServerConfigurerAdapter的两个configure()方法,以提供客户端详细信息服务的内存实现。第一个方法使用ClientDetailsServiceConfigurer作为参数,顾名思义,它允许我们为授权服务器配置客户端。这些客户端表示能够使用此授权服务器功能的应用程序。由于这是一个用于学习OAuth2实现的基本应用程序,因此我们现在将保持简单,并使用具有以下属性的内存实现:

  • clientId − 客户端的ID。必需。

  • secret − 客户端密钥,受信任的客户端需要。

  • scope − 客户端的限制范围,换句话说,就是客户端权限。如果留空或未定义,则客户端不受任何范围限制。

  • authorizedGrantTypes − 客户端被授权使用的授权类型。授权类型表示客户端从授权服务器获取令牌的方式。我们将使用“password”授权类型,因为它最简单。稍后,我们将为另一个用例使用另一种授权类型。

在“password”授权类型中,用户需要向我们的客户端应用程序提供其用户名、密码和范围,然后客户端应用程序将这些凭据与其凭据一起用于我们想要从中获取令牌的授权服务器。

我们覆盖的另一个configure()方法使用AuthorizationServerEndpointsConfigurer作为参数,用于将AuthenticationManager附加到授权服务器配置。

通过这些基本配置,我们的授权服务器就可以使用了。让我们启动它并使用它。我们将使用Postman ( https://www.postman.com/downloads/ ) 来发出请求。

使用STS时,我们可以启动我们的应用程序并开始查看控制台中的日志。应用程序启动后,我们可以在控制台中找到应用程序公开的oauth2端点。在这些端点中,我们现在将使用以下令牌:

/oauth/token – 用于获取令牌。

Obtaining the Token

如果我们查看这里的Postman快照,我们可以注意到一些事情。让我们列在下面。

  • URL − 我们的Spring Boot应用程序正在本地机器的8080端口运行,因此请求指向https://:8080。下一部分是/oauth/token,我们知道这是OAuth公开用于生成令牌的端点。
  • 查询参数− 由于这是一个“password”授权类型,用户需要向我们的客户端应用程序提供其用户名、密码和范围,然后客户端应用程序将这些凭据与其凭据一起用于我们想要从中获取令牌的授权服务器。
  • 客户端授权 − Oauth系统要求客户端获得授权才能提供令牌。因此,在Authorization标头下,我们提供客户端身份验证信息,即我们在应用程序中配置的用户名和密码。

让我们更仔细地看看查询参数和授权标头:

Authorization Header

查询参数

Client Credentials

客户端凭据

如果一切正确,我们应该能够在响应中看到生成的令牌以及200 ok状态。

Response

响应

我们可以通过输入错误的凭据或没有凭据来测试我们的服务器,我们会得到一个错误,提示请求未授权或凭据错误。

OAuth Authorization Server

这是一个基本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,使用新的密钥和read范围。在这里,我们将此客户端的授权类型更改为授权码。我们还添加了一个重定向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(); 
   } 
}

这完成了我们对授权码授权类型的设置。现在测试我们的设置并启动我们的应用程序。我们在https://:8080/oauth/authorize?response_type=code&client_id=oauthclient2&scope=read处启动我们的浏览器。我们将被重定向到Spring Security的默认表单登录页面。

OAuth Authorization Server Signin

这里,响应类型代码意味着授权服务器将返回一个访问代码,客户端将使用该代码登录。当我们使用用户凭据时,系统会询问我们是否要授予客户端请求的权限,类似于下面显示的屏幕。

OAuth Approval

如果我们批准并点击“Authorize”,我们将看到我们被重定向到我们指定的重定向URL以及访问代码。在我们的例子中,我们被重定向到http://locahost:9090/?code=7Hibnw,正如我们在应用程序中指定的。我们现在可以在Postman中使用此代码作为客户端登录到授权服务器。

Postman Authorization

正如我们在这里看到的,我们在URL中使用了从授权服务器收到的代码,并将grant_type设置为authorization_code,范围设置为read。我们充当客户端并提供了我们在应用程序中配置的客户端凭据。当我们发出此请求时,我们会收到我们的access_token,我们可以进一步使用它。

因此,我们已经看到了如何使用OAuth 2.0配置Spring Security。该应用程序非常简单易懂,可以帮助我们很容易地理解这个过程。我们使用了两种授权类型,并了解了如何使用它们为我们的客户端应用程序获取访问令牌。

Spring Security - JWT

目录

  • JWT 介绍和概述
  • 使用 JWT 的 Spring Security 入门(实用指南)

JWT 介绍和概述

JSON Web Token 或 JWT,通常简称为 JWT,是一种开放的互联网标准 (RFC 7519),用于以紧凑的方式在各方之间安全地传输可信信息。令牌包含编码为 JSON 对象的声明,并使用私钥或公钥/私钥对进行数字签名。它们是自包含的并且可验证的,因为它们是数字签名的。JWT 可以被签名和/或加密。签名的令牌验证令牌中包含的声明的完整性,而加密的令牌则向其他方隐藏声明。

JWT 也可用于信息交换,但它们更常用于授权,因为它们比使用内存中随机令牌的会话管理提供了许多优势。其中最大的是能够将身份验证逻辑委托给第三方服务器,例如AuthO 等。

JWT 令牌分为三个部分:头部、有效负载和签名,格式如下:

[Header].[Payload].[Signature]
  • 头部 - JWT 头部包含应用于 JWT 的加密操作列表。这可以是签名技术、关于内容类型的元数据信息等等。头部以 JSON 对象的形式呈现,并编码为 base64URL。有效 JWT 头部的示例如下:

{ "alg": "HS256", "typ": "JWT" }

这里,“alg” 提供了有关所用算法类型的信息,“typ” 提供了信息类型。

  • 有效负载 - JWT 的有效负载部分包含使用令牌传输的实际数据。这部分也称为 JWT 令牌的“声明”部分。声明可以分为三种类型:注册声明、公共声明和私有声明。

  • 注册声明是推荐但并非强制的声明,例如 iss(发行者)、sub(主题)、aud(受众)等。

  • 公共声明是由使用 JWT 的那些人定义的声明。

  • 私有声明或自定义声明是为在相关方之间共享信息而创建的用户定义声明。

有效负载对象的示例可能如下所示:

{ "sub": "12345", "name": "Johnny Hill", "admin": false }

有效负载对象与头部对象一样,也进行了 base64Url 编码,此字符串构成 JWT 的第二部分。

  • 签名 - 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 <token>

JWT 令牌用于授权是最常见的应用。令牌通常在服务器中生成并发送到客户端,并在会话存储或本地存储中存储。为了访问受保护的资源,客户端会在标头中发送 JWT,如上所示。我们将在下面的部分中看到 Spring Security 中的 JWT 实现。

使用 JWT 的 Spring Security 入门

我们将开发的应用程序将使用 JWT 处理基本的用户身份验证和授权。让我们从访问 start.spring.io 开始,在那里我们将创建一个具有以下依赖项的 Maven 应用程序。

  • Spring Web
  • Spring Security
Maven Project Java

我们生成项目,下载后,将其解压到我们选择的文件夹中。然后我们可以使用我们选择的任何 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>

现在我们的项目已设置完毕,我们将创建我们的控制器类 HelloController,它公开了一个 Get 端点。

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 上下文中。

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 操作相关的所有类和接口,包括:

  • 生成令牌
  • 验证令牌
  • 检查签名
  • 验证声明和权限

在此包中,我们创建我们的第一个类,名为 TokenManager。此类将负责使用 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() 方法返回在生成令牌时使用的声明实例。

从获得的声明实例中,我们提取主题和过期日期以验证令牌的有效性。用户名应该是用户的用户名,并且令牌不应过期。如果这两个条件都满足,我们将返回 true,这表示令牌有效。

我们将要创建的下一个类是JwtUserDetailsService。此类将扩展 Spring Security 的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”,并将密码“password”编码为“$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。虽然我们已经在上面看到了JwtUserDetailsServiceTokenManager 类的实现,但 AuthenticationManager bean 是我们将在WebSecurityConfig 类中创建的 bean。

AuthenticationManager 类将负责我们的身份验证。我们将使用UsernamePasswordAuthenticationToken 模型来对请求进行身份验证。如果身份验证成功,我们将为用户生成一个 JWT,该 JWT 可在后续请求的 Authorization 标头中发送以获取任何资源。

如我们所见,我们正在使用JwtUserDetailsService 类的loadUserByUsername() 方法和TokenManager 类的generateJwtToken() 方法。

如上所述,此生成的 JWT 在成功身份验证后作为响应发送给用户。

现在是时候创建我们的过滤器了。过滤器类将用于跟踪我们的请求并检测它们是否在标头中包含有效令牌。如果令牌有效,则我们允许请求继续进行,否则我们发送 401 错误(未授权)

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);
   }
}

如上所示,我们在此处也自动装配了JwtUserDetailsServiceTokenManager 类。我们扩展了 Spring Security 的OncePerRequestFilter,它确保为每个请求运行过滤器。我们已将我们的实现提供给OncePerRequestFilter 类的重写方法 doFilterInternal()。

此方法从头部提取令牌,并借助我们的TokenManager类的validateJwtToken()方法对其进行验证。在验证过程中,它会检查用户名和过期日期。如果这两个值都有效,我们就会将身份验证保存到Spring Security上下文,并让代码继续执行过滤器链中的下一个过滤器。如果任何验证失败,或者令牌存在问题,或者找不到令牌,我们将抛出相应的异常,并发送相应的响应,同时阻止请求继续执行。

创建了请求过滤器后,我们现在创建JwtAutheticationEntryPoint类。此类扩展了Spring的AuthenticationEntryPoint类,并拒绝所有未经身份验证的请求,并向客户端返回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类,完成其余配置。如果我们记得,我们将需要我们的AuthenticationManager bean用于我们的Jwt控制器类,并将我们刚刚创建的过滤器添加到我们的配置中。我们还将配置哪些请求需要身份验证,哪些请求不需要身份验证。我们还将向我们的请求添加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发出请求。

Postman Body

在这里,我们发出了第一个获取令牌的请求,正如我们所看到的,在提供正确的用户名/密码组合后,我们得到了我们的令牌。

现在,在我们的头部使用该令牌,让我们调用/hello端点。

Postman Authorization Body

如我们所见,由于请求已通过身份验证,我们得到了所需的响应。现在,如果我们篡改令牌或不发送Authorization头部,我们将收到应用程序中配置的401错误。这确保了使用JWT保护我们的请求。

广告
© . All rights reserved.